Implement stable Sable gravity frame physics
This commit is contained in:
1 parent
e1aa22858e
commit
c186a35c72
38 files changed
+3113
-215
No files matched your search
@@ -0,0 +1,8 @@
|
||||
[mcp_servers.minecraft]
|
||||
# The development bridge is reachable through IPv4 loopback only.
|
||||
url = "http://127.0.0.1:8765/mcp"
|
||||
enabled = true
|
||||
required = false
|
||||
startup_timeout_sec = 10
|
||||
tool_timeout_sec = 120
|
||||
default_tools_approval_mode = "writes"
|
||||
+10
@@ -43,3 +43,13 @@ repo/
|
||||
!**/src/**/repo/
|
||||
|
||||
### Local reference checkouts ###
|
||||
SableGravity-oriented-aabb-test18-server-authoritative.zip
|
||||
SableGravity-test21-rotated-obb-recovery-source.zip
|
||||
|
||||
### Local MCP development artifacts ###
|
||||
.codex/recovery/
|
||||
runclient.*.log
|
||||
|
||||
### Local reverse-engineering output ###
|
||||
verification/*.javap.txt
|
||||
verification/sable-*-inspect/
|
||||
@@ -1,12 +1,51 @@
|
||||
# Sable Gravity 0.6.0 test16
|
||||
# Sable Gravity 0.7.4
|
||||
|
||||
Built on the working test13 Sable OBB collision path.
|
||||
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
|
||||
pose and a local `-Y` gravity direction.
|
||||
|
||||
Changes from test15:
|
||||
- never imports Sable-mutated world deltaMovement back into local velocity; removes tilted-floor drift;
|
||||
- uses Sable local floor contact as the authoritative ground state;
|
||||
- keeps creative flight independent from stale world-axis onGround state;
|
||||
- captures flight ascend/descend against the exact previous post-tick velocity, so ship rotation cannot become fake acceleration;
|
||||
- updates walk animation directly from the actual local displacement because cancelled Player.travel never reaches vanilla calculateEntityAnimation;
|
||||
- replaces Sable/vanilla sneak edge backoff with a full local plot-space floor query;
|
||||
- leaves Entity.bb axis-aligned only for Minecraft broadphase. It is no longer used as the source of local ground, flight or sneak behaviour.
|
||||
## Physics model
|
||||
|
||||
- Players use an oriented body and plot-local feet anchor. Crouching, standing, jumping,
|
||||
creative flight, edge sneaking and wall sliding are resolved in the ship frame without using
|
||||
the parent-world AABB as the source of truth.
|
||||
- Entities always remain in physical parent-world coordinates. Sable is the only owner of their
|
||||
OBB collision, tracking and packet conversion; Sable Gravity rotates gravity, locomotion and
|
||||
drag into the selected frame without exposing the hidden plot grid to entity networking.
|
||||
- Non-player entities keep their ordinary parent-world/network position. Their oriented body is
|
||||
foot-anchored only inside Sable's collision transform, so items and mobs do not need a spawn-time
|
||||
coordinate rebase and cannot leak plot coordinates into packets.
|
||||
- Client particle attachment and packet conversion remain on Sable's native path. This mod does
|
||||
not install a second universal particle lifecycle on top of Sable's particle mixins.
|
||||
- For the local player, plot-local feet position and relative velocity are authoritative. A
|
||||
translating or rotating ship is reconstructed from them every tick instead of accumulating pose
|
||||
deltas.
|
||||
|
||||
## Dimensions and Cosmonautics
|
||||
|
||||
`GravityFrameId` contains provider, dimension and local UUID. Frame lookup is always performed in
|
||||
the entity's current `Level`, so an identical SubLevel UUID in another dimension cannot alias the
|
||||
old ship. A recreated destination frame is resolved again from that level's Sable container.
|
||||
|
||||
Gravity magnitude comes from the final parent-world `Entity#getGravity()` result while the entity
|
||||
stays physical. Dimension/altitude modifiers such as Cosmonautics therefore compose with Sable
|
||||
direction and never evaluate against the plot's synthetic Y coordinate.
|
||||
|
||||
There is no hardcoded list of planets, dimensions or entity types. The built-in provider accepts
|
||||
Sable SubLevels from any `Level`, including Cosmonautics dimensions. Other mods can register a
|
||||
higher-priority `GravityFrameProvider` through `SableGravityApi` for directional gravity and frame
|
||||
consumers; arbitrary-orientation ship collision additionally requires a Sable-backed frame.
|
||||
|
||||
## Development
|
||||
|
||||
The development client loads Create, Aeronautics, Cosmonautics and the local Minecraft MCP bridge.
|
||||
The bridge is configured for `127.0.0.1:8765` only; it is not packaged into the release jar.
|
||||
|
||||
Build with:
|
||||
|
||||
```powershell
|
||||
.\gradlew.bat build
|
||||
```
|
||||
|
||||
Player controls remain available through `/sablegravity enable|disable`,
|
||||
`/sablegravity strength <value>` and `/sablegravity test <state|frame|support|all>`.
|
||||
@@ -12,6 +12,13 @@ base {
|
||||
|
||||
java.toolchain.languageVersion = JavaLanguageVersion.of(21)
|
||||
|
||||
sourceSets {
|
||||
// Cosmonautics 26.07.270 advertises a relocated Orekit javac plugin that is absent from its
|
||||
// published jar. The dev-only provider keeps Minecraft MCP's in-process Java compiler usable.
|
||||
// This source set is placed on runClient's classpath and is never included in sablegravity.jar.
|
||||
mcpCompat
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
@@ -61,7 +68,12 @@ dependencies {
|
||||
// Development client mod set. Aeronautics requires Create and Sable.
|
||||
runtimeOnly "maven.modrinth:create:${project.create_version}"
|
||||
runtimeOnly "maven.modrinth:create-aeronautics:${project.aeronautics_version}"
|
||||
runtimeOnly "maven.modrinth:micg1iTH:${project.cosmonautics_version}"
|
||||
runtimeOnly "maven.modrinth:the-one-probe:${project.the_one_probe_version}"
|
||||
|
||||
// Local-only game control/inspection bridge used by the development client.
|
||||
runtimeOnly files('libs/minecraft_mcp-0.1.0.jar')
|
||||
runtimeOnly sourceSets.mcpCompat.output
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
|
||||
+2
-1
@@ -2,11 +2,12 @@ org.gradle.jvmargs=-Xmx2G
|
||||
org.gradle.daemon=false
|
||||
org.gradle.parallel=true
|
||||
mod_id=sablegravity
|
||||
mod_version=0.6.0-test16-ground-flight-sneak
|
||||
mod_version=0.7.4-player-recovery
|
||||
minecraft_version=1.21.1
|
||||
neo_version=21.1.228
|
||||
sable_version=2.0.3+mc1.21.1
|
||||
sable_companion_version=1.6.0
|
||||
create_version=6.0.10+mc1.21.1
|
||||
aeronautics_version=1.3.0+mc1.21.1
|
||||
cosmonautics_version=c6IOumoH
|
||||
the_one_probe_version=1.21_neo-12.0.8
|
||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
||||
package dev.nightly.sablegravity;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Runtime state shared by every entity that can be bound to a gravity frame.
|
||||
*
|
||||
* <p>The frame identity is deliberately dimension-qualified. Cosmonautics recreates Sable
|
||||
* SubLevels while warping a ship between dimensions, so a bare SubLevel UUID is not a stable
|
||||
* cross-dimensional identity.</p>
|
||||
*/
|
||||
public interface GravityEntityState {
|
||||
boolean sablegravity$isInLocalSimulation();
|
||||
void sablegravity$setInLocalSimulation(boolean value);
|
||||
|
||||
boolean sablegravity$isLocalStateValid();
|
||||
void sablegravity$setLocalStateValid(boolean value);
|
||||
Vec3 sablegravity$getLocalPosition();
|
||||
void sablegravity$setLocalPosition(Vec3 value);
|
||||
Vec3 sablegravity$getLocalVelocity();
|
||||
void sablegravity$setLocalVelocity(Vec3 value);
|
||||
|
||||
UUID sablegravity$getFrameId();
|
||||
void sablegravity$setFrameId(UUID value);
|
||||
ResourceLocation sablegravity$getFrameProvider();
|
||||
void sablegravity$setFrameProvider(ResourceLocation value);
|
||||
ResourceLocation sablegravity$getFrameDimension();
|
||||
void sablegravity$setFrameDimension(ResourceLocation value);
|
||||
|
||||
boolean sablegravity$isGravitySampleValid();
|
||||
void sablegravity$setGravitySampleValid(boolean value);
|
||||
double sablegravity$getGravitySample();
|
||||
void sablegravity$setGravitySample(double value);
|
||||
|
||||
boolean sablegravity$isFrameHandoffPending();
|
||||
void sablegravity$setFrameHandoffPending(boolean value);
|
||||
int sablegravity$getFrameHandoffExpiryTick();
|
||||
void sablegravity$setFrameHandoffExpiryTick(int value);
|
||||
}
|
||||
@@ -4,16 +4,24 @@ import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||
import dev.nightly.sablegravity.physics.SableLocalCollisionProbe;
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.arguments.EntityArgument;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.fml.ModContainer;
|
||||
import net.neoforged.fml.common.Mod;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -23,12 +31,16 @@ import java.util.Map;
|
||||
@Mod(SableGravityMod.MOD_ID)
|
||||
public final class SableGravityMod {
|
||||
public static final String MOD_ID = "sablegravity";
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
public static final float VANILLA_STRENGTH = 9.8F;
|
||||
|
||||
private static volatile float globalStrength = VANILLA_STRENGTH;
|
||||
|
||||
private static final Map<String, TestFunction> TEST_FUNCTIONS = Map.of(
|
||||
"axistestb", SableGravityMod::axisTestB
|
||||
"state", SableGravityMod::testState,
|
||||
"frame", SableGravityMod::testFrame,
|
||||
"support", SableGravityMod::testSupport,
|
||||
"all", SableGravityMod::testAll
|
||||
);
|
||||
|
||||
private static final SuggestionProvider<CommandSourceStack> TEST_SUGGESTIONS =
|
||||
@@ -185,7 +197,11 @@ public final class SableGravityMod {
|
||||
TEST_FUNCTIONS.keySet().stream().sorted().toList()
|
||||
);
|
||||
|
||||
source.sendFailure( Component.literal("Unknown test function: " + functionName + ". Available: " + functions) );
|
||||
source.sendFailure(
|
||||
Component.literal(
|
||||
"Unknown test function: " + functionName + ". Available: " + functions
|
||||
)
|
||||
);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -213,37 +229,103 @@ public final class SableGravityMod {
|
||||
}
|
||||
}
|
||||
|
||||
private static void axisTestB(CommandSourceStack source) {
|
||||
try {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
|
||||
if (player == null) {
|
||||
source.sendFailure(Component.literal("This test must be executed by a player"));
|
||||
return;
|
||||
}
|
||||
|
||||
player.getBoundingBox();
|
||||
// Тут вызываешь свою void-функцию:
|
||||
//
|
||||
// SomeClass.axisTestB(player);
|
||||
// someVoidFunction();
|
||||
|
||||
source.sendSuccess(() -> Component.literal("Axis test B executed"),false );
|
||||
} catch (Exception e) {
|
||||
source.sendFailure(Component.literal("ERROR:\n " + e.toString()));
|
||||
}
|
||||
private static void testState(CommandSourceStack source) throws Exception {
|
||||
ServerPlayer player = source.getPlayerOrException();
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
SubLevel tracked = Sable.HELPER.getTrackingSubLevel(player);
|
||||
source.sendSuccess(
|
||||
() -> Component.literal(
|
||||
"server-state: enabled=" + state.sablegravity$isEnabled()
|
||||
+ ", strength=" + format(state.sablegravity$getStrength())
|
||||
+ ", trackedFrame=" + (tracked == null ? "null" : tracked.getUniqueId())
|
||||
+ ". Local physics lives on LocalPlayer; run /sablegravityclient all"
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private static String format(float value) {
|
||||
private static void testFrame(CommandSourceStack source) throws Exception {
|
||||
ServerPlayer player = source.getPlayerOrException();
|
||||
SubLevel frame = requireTrackedFrame(player);
|
||||
|
||||
Quaterniondc q = frame.logicalPose().orientation();
|
||||
Vec3 localPosition = frame.logicalPose().transformPositionInverse(player.position());
|
||||
source.sendSuccess(
|
||||
() -> Component.literal(
|
||||
"frame: id=" + frame.getUniqueId()
|
||||
+ ", q=(" + format(q.x()) + ", " + format(q.y()) + ", "
|
||||
+ format(q.z()) + ", " + format(q.w()) + ")"
|
||||
+ ", localPos=" + formatVec(localPosition)
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private static void testSupport(CommandSourceStack source) throws Exception {
|
||||
ServerPlayer player = source.getPlayerOrException();
|
||||
SubLevel frame = requireTrackedFrame(player);
|
||||
|
||||
Vec3 localFeet = SableLocalCollisionProbe.getLocalFeet(player, frame);
|
||||
double halfWidth = player.getBbWidth() * 0.5D;
|
||||
double epsilon = 1.0E-5D;
|
||||
AABB floorProbe = new AABB(
|
||||
localFeet.x - halfWidth,
|
||||
localFeet.y - player.maxUpStep() - epsilon,
|
||||
localFeet.z - halfWidth,
|
||||
localFeet.x + halfWidth,
|
||||
localFeet.y + epsilon,
|
||||
localFeet.z + halfWidth
|
||||
);
|
||||
boolean supported = SableLocalCollisionProbe.hasSupport(player, frame, floorProbe);
|
||||
source.sendSuccess(
|
||||
() -> Component.literal(
|
||||
"support: " + supported
|
||||
+ ", localFeet=" + formatVec(localFeet)
|
||||
+ ", probe=" + formatBox(floorProbe)
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private static void testAll(CommandSourceStack source) throws Exception {
|
||||
testState(source);
|
||||
testFrame(source);
|
||||
testSupport(source);
|
||||
source.sendSuccess(
|
||||
() -> Component.literal(
|
||||
"Server diagnostics completed. For real local velocity/bounds/collision use /sablegravityclient all"
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private static SubLevel requireTrackedFrame(ServerPlayer player) {
|
||||
SubLevel frame = Sable.HELPER.getTrackingSubLevel(player);
|
||||
if (frame == null || frame.isRemoved()) {
|
||||
throw new IllegalStateException("no tracked Sable SubLevel");
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
private static String formatVec(Vec3 value) {
|
||||
return "(" + format(value.x) + ", " + format(value.y) + ", " + format(value.z) + ")";
|
||||
}
|
||||
|
||||
private static String formatBox(AABB box) {
|
||||
return "[" + format(box.minX) + ", " + format(box.minY) + ", " + format(box.minZ)
|
||||
+ " -> " + format(box.maxX) + ", " + format(box.maxY) + ", " + format(box.maxZ) + "]";
|
||||
}
|
||||
|
||||
private static String format(double value) {
|
||||
if (value == (long) value) {
|
||||
return Long.toString((long) value);
|
||||
}
|
||||
|
||||
return Float.toString(value);
|
||||
return String.format(Locale.ROOT, "%.6f", value);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface TestFunction {
|
||||
void run(CommandSourceStack source) throws Exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,8 @@
|
||||
package dev.nightly.sablegravity;
|
||||
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface SableGravityState {
|
||||
public interface SableGravityState extends GravityEntityState {
|
||||
boolean sablegravity$isEnabled();
|
||||
void sablegravity$setEnabled(boolean enabled);
|
||||
float sablegravity$getStrength();
|
||||
void sablegravity$setStrength(float strength);
|
||||
|
||||
boolean sablegravity$isInLocalSimulation();
|
||||
void sablegravity$setInLocalSimulation(boolean value);
|
||||
|
||||
boolean sablegravity$isLocalStateValid();
|
||||
void sablegravity$setLocalStateValid(boolean value);
|
||||
Vec3 sablegravity$getLocalPosition();
|
||||
void sablegravity$setLocalPosition(Vec3 value);
|
||||
Vec3 sablegravity$getLocalVelocity();
|
||||
void sablegravity$setLocalVelocity(Vec3 value);
|
||||
UUID sablegravity$getFrameId();
|
||||
void sablegravity$setFrameId(UUID value);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package dev.nightly.sablegravity.api;
|
||||
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Quaterniondc;
|
||||
|
||||
/**
|
||||
* A moving, rotating coordinate system whose local -Y axis is gravity down.
|
||||
*
|
||||
* <p>Positions and velocities are intentionally separate: a relative velocity must not inherit
|
||||
* the frame translation twice. Consumers that need inertial velocity can add
|
||||
* {@link #frameVelocityAt(Vec3)} explicitly.</p>
|
||||
*/
|
||||
public interface GravityFrame {
|
||||
GravityFrameId id();
|
||||
Level level();
|
||||
boolean isValid();
|
||||
|
||||
Vec3 toLocalPosition(Vec3 worldPosition);
|
||||
Vec3 toWorldPosition(Vec3 localPosition);
|
||||
Vec3 toLocalVelocity(Vec3 worldVelocity);
|
||||
Vec3 toWorldVelocity(Vec3 localVelocity);
|
||||
Quaterniondc orientation(float partialTick);
|
||||
|
||||
default Vec3 down(float partialTick) {
|
||||
Quaterniondc rotation = orientation(partialTick);
|
||||
org.joml.Vector3d down = rotation.transform(
|
||||
new org.joml.Vector3d(0.0D, -1.0D, 0.0D)
|
||||
);
|
||||
return new Vec3(down.x, down.y, down.z);
|
||||
}
|
||||
|
||||
/** Movement contributed by the frame between its previous and current physics poses. */
|
||||
Vec3 frameVelocityAt(Vec3 worldPosition);
|
||||
|
||||
/**
|
||||
* Final gravity magnitude after vanilla attributes and other mods (including Cosmonautics)
|
||||
* have had a chance to modify {@link Entity#getGravity()}.
|
||||
*/
|
||||
default double gravityPerTick(Entity entity) {
|
||||
return Math.max(0.0D, entity.getGravity());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package dev.nightly.sablegravity.api;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Globally unambiguous identity of one gravity frame in one dimension. */
|
||||
public record GravityFrameId(
|
||||
ResourceLocation provider,
|
||||
ResourceLocation dimension,
|
||||
UUID localId
|
||||
) {
|
||||
public GravityFrameId {
|
||||
Objects.requireNonNull(provider, "provider");
|
||||
Objects.requireNonNull(dimension, "dimension");
|
||||
Objects.requireNonNull(localId, "localId");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package dev.nightly.sablegravity.api;
|
||||
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/** Extension point for Sable ships, artificial fields, planets, and other reference frames. */
|
||||
public interface GravityFrameProvider {
|
||||
Optional<? extends GravityFrame> resolve(Entity entity, GravityFrameId previousFrame);
|
||||
|
||||
default Optional<? extends GravityFrame> resolveAt(
|
||||
Level level,
|
||||
Vec3 worldPosition,
|
||||
GravityFrameId previousFrame
|
||||
) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package dev.nightly.sablegravity.api;
|
||||
|
||||
import dev.nightly.sablegravity.physics.GravityFrames;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/** Public, dependency-light entry point for gravity providers and consumers. */
|
||||
public final class SableGravityApi {
|
||||
private SableGravityApi() {}
|
||||
|
||||
/** Higher priority providers win. Registration is safe during parallel mod construction. */
|
||||
public static void registerFrameProvider(GravityFrameProvider provider, int priority) {
|
||||
GravityFrames.register(provider, priority);
|
||||
}
|
||||
|
||||
public static Optional<GravityFrame> resolve(Entity entity) {
|
||||
return GravityFrames.resolve(entity);
|
||||
}
|
||||
|
||||
public static Optional<GravityFrame> resolveAt(Level level, Vec3 position) {
|
||||
return GravityFrames.resolveAt(level, position, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package dev.nightly.sablegravity.client;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import dev.nightly.sablegravity.SableGravityMod;
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
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.mixinterface.entity.entity_sublevel_collision.EntityMovementExtension;
|
||||
import dev.ryanhcode.sable.mixinterface.entity.entity_sublevel_collision.LivingEntityMovementExtension;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.entity_collision.SubLevelEntityCollision;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.neoforged.api.distmarker.Dist;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.client.event.RegisterClientCommandsEvent;
|
||||
import org.joml.Quaterniondc;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/** Diagnostics that execute on the actual LocalPlayer instead of the integrated server copy. */
|
||||
@EventBusSubscriber(modid = SableGravityMod.MOD_ID, value = Dist.CLIENT)
|
||||
public final class SableGravityClientCommands {
|
||||
private SableGravityClientCommands() {}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void register(RegisterClientCommandsEvent event) {
|
||||
CommandDispatcher<CommandSourceStack> dispatcher = event.getDispatcher();
|
||||
dispatcher.register(
|
||||
Commands.literal("sablegravityclient")
|
||||
.executes(context -> run("all"))
|
||||
.then(Commands.literal("state").executes(context -> run("state")))
|
||||
.then(Commands.literal("frame").executes(context -> run("frame")))
|
||||
.then(Commands.literal("bounds").executes(context -> run("bounds")))
|
||||
.then(Commands.literal("support").executes(context -> run("support")))
|
||||
.then(Commands.literal("collision").executes(context -> run("collision")))
|
||||
.then(Commands.literal("velocity").executes(context -> run("velocity")))
|
||||
.then(Commands.literal("all").executes(context -> run("all")))
|
||||
);
|
||||
}
|
||||
|
||||
private static int run(String function) {
|
||||
Player player = Minecraft.getInstance().player;
|
||||
if (player == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (function) {
|
||||
case "state" -> state(player);
|
||||
case "frame" -> frame(player);
|
||||
case "bounds" -> bounds(player);
|
||||
case "support" -> support(player);
|
||||
case "collision" -> collision(player);
|
||||
case "velocity" -> velocity(player);
|
||||
case "all" -> {
|
||||
state(player);
|
||||
frame(player);
|
||||
bounds(player);
|
||||
support(player);
|
||||
collision(player);
|
||||
velocity(player);
|
||||
}
|
||||
default -> throw new IllegalArgumentException("unknown function: " + function);
|
||||
}
|
||||
message(player, "client diagnostic completed: " + function);
|
||||
return 1;
|
||||
} catch (Exception exception) {
|
||||
message(
|
||||
player,
|
||||
"client diagnostic " + function + " failed: "
|
||||
+ exception.getClass().getSimpleName() + ": " + exception.getMessage()
|
||||
);
|
||||
exception.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void state(Player player) {
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
SubLevel latched = SableLocalPlayerPhysics.getAttachedFrameIfPresent(player);
|
||||
SubLevel tracked = dev.ryanhcode.sable.Sable.HELPER.getTrackingSubLevel(player);
|
||||
message(
|
||||
player,
|
||||
"client-state: enabled=" + state.sablegravity$isEnabled()
|
||||
+ ", strength=" + format(state.sablegravity$getStrength())
|
||||
+ ", localValid=" + state.sablegravity$isLocalStateValid()
|
||||
+ ", grounded=" + SableLocalPlayerPhysics.isLocalGrounded(player)
|
||||
+ ", stateFrame=" + state.sablegravity$getFrameId()
|
||||
+ ", latchedFrame=" + frameId(latched)
|
||||
+ ", sableTracked=" + frameId(tracked)
|
||||
+ ", localVelocity=" + formatVec(state.sablegravity$getLocalVelocity())
|
||||
);
|
||||
}
|
||||
|
||||
private static void frame(Player player) {
|
||||
SubLevel frame = requireFrame(player);
|
||||
Quaterniondc q = SableOrientedBounds.logicalOrientation(frame);
|
||||
message(
|
||||
player,
|
||||
"client-frame: id=" + frame.getUniqueId()
|
||||
+ ", q=" + formatQuat(q)
|
||||
+ ", localFeet=" + formatVec(SableLocalCollisionProbe.getLocalFeet(player, frame))
|
||||
);
|
||||
}
|
||||
|
||||
private static void bounds(Player player) {
|
||||
SubLevel frame = requireFrame(player);
|
||||
Quaterniondc q = SableOrientedBounds.logicalOrientation(frame);
|
||||
AABB canonical = SableOrientedBounds.canonicalAabb(player);
|
||||
AABB envelope = SableOrientedBounds.worldEnvelope(player, q);
|
||||
Vec3 center = SableOrientedBounds.worldCenter(player, q);
|
||||
Vec3 up = toVec(q.transform(
|
||||
new org.joml.Vector3d(0.0D, 1.0D, 0.0D),
|
||||
new org.joml.Vector3d()
|
||||
));
|
||||
message(
|
||||
player,
|
||||
"bounds: pose=" + player.getPose()
|
||||
+ ", entityAabb=" + formatBox(player.getBoundingBox())
|
||||
+ ", canonical=" + formatBox(canonical)
|
||||
+ ", obbCenter=" + formatVec(center)
|
||||
+ ", obbUp=" + formatVec(up)
|
||||
+ ", physicalFeet=" + formatVec(SableOrientedBounds.worldFeet(player, q))
|
||||
+ ", envelope=" + formatBox(envelope)
|
||||
);
|
||||
}
|
||||
|
||||
private static void support(Player player) {
|
||||
SubLevel frame = requireFrame(player);
|
||||
Vec3 localFeet = SableLocalCollisionProbe.getLocalFeet(player, frame);
|
||||
AABB probe = SableLocalCollisionProbe.createFloorProbe(player, localFeet, 0.08D);
|
||||
message(
|
||||
player,
|
||||
"support: " + SableLocalCollisionProbe.hasSupport(player, frame, probe)
|
||||
+ ", localFeet=" + formatVec(localFeet)
|
||||
+ ", probe=" + formatBox(probe)
|
||||
);
|
||||
}
|
||||
|
||||
private static void collision(Player player) {
|
||||
SubLevelEntityCollision.CollisionInfo info =
|
||||
((EntityMovementExtension) player).sable$getCollisionInfo();
|
||||
if (info == null) {
|
||||
message(player, "collision: null");
|
||||
return;
|
||||
}
|
||||
int contacts = info.firstCollisions == null ? 0 : info.firstCollisions.size();
|
||||
SableParentWorldCollision.CollisionResult parent =
|
||||
SableParentWorldCollision.getLastResult(player);
|
||||
message(
|
||||
player,
|
||||
"collision: horizontal=" + info.horizontalCollision
|
||||
+ ", subHorizontal=" + info.subLevelHorizontalCollision
|
||||
+ ", vertical=" + info.verticalCollision
|
||||
+ ", below=" + info.verticalCollisionBelow
|
||||
+ ", contacts=" + contacts
|
||||
+ ", motion=" + formatVec(info.motion)
|
||||
+ ", parentContacts=" + parent.contacts()
|
||||
+ ", parentHorizontal=" + parent.horizontalCollision()
|
||||
+ ", parentVertical=" + parent.verticalCollision()
|
||||
+ ", parentBelow=" + parent.verticalCollisionBelow()
|
||||
+ ", parentCorrection=" + format(parent.maxCorrection())
|
||||
+ ", parentMotion=" + formatVec(parent.motion())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static void velocity(Player player) {
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
org.joml.Vector3dc inherited =
|
||||
((LivingEntityMovementExtension) player).sable$getInheritedVelocity();
|
||||
SubLevelEntityCollision.CollisionInfo info =
|
||||
((EntityMovementExtension) player).sable$getCollisionInfo();
|
||||
Vec3 inheritedMotion = info == null ? null : info.inheritedMotion;
|
||||
Vec3 inheritedVelocity = new Vec3(inherited.x(), inherited.y(), inherited.z());
|
||||
Vec3 correctedFrameMotion = SableLocalPlayerPhysics.getLastFrameMotion(player);
|
||||
Vec3 rejectedSableMotion =
|
||||
SableLocalPlayerPhysics.getLastRejectedSableInheritedMotion(player);
|
||||
message(
|
||||
player,
|
||||
"velocity: local=" + formatVec(state.sablegravity$getLocalVelocity())
|
||||
+ " |local|=" + format(state.sablegravity$getLocalVelocity().length())
|
||||
+ ", deltaMovement=" + formatVec(player.getDeltaMovement())
|
||||
+ " |delta|=" + format(player.getDeltaMovement().length())
|
||||
+ ", correctedFrameMotion=" + formatVec(correctedFrameMotion)
|
||||
+ " |frame|=" + format(correctedFrameMotion.length())
|
||||
+ ", inheritedVelocity=" + formatVec(inheritedVelocity)
|
||||
+ " |inherited|=" + format(inheritedVelocity.length())
|
||||
+ ", liveSableInherited=" + formatVec(inheritedMotion)
|
||||
+ ", rejectedSableInherited=" + formatVec(rejectedSableMotion)
|
||||
);
|
||||
}
|
||||
|
||||
private static SubLevel requireFrame(Player player) {
|
||||
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrame(player);
|
||||
if (frame == null || frame.isRemoved()) {
|
||||
throw new IllegalStateException("no attached client Sable frame");
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
|
||||
private static String frameId(SubLevel frame) {
|
||||
return frame == null ? "null" : frame.getUniqueId().toString();
|
||||
}
|
||||
|
||||
private static Vec3 toVec(org.joml.Vector3dc value) {
|
||||
return new Vec3(value.x(), value.y(), value.z());
|
||||
}
|
||||
|
||||
private static void message(Player player, String text) {
|
||||
player.displayClientMessage(Component.literal(text), false);
|
||||
}
|
||||
|
||||
private static String formatQuat(Quaterniondc q) {
|
||||
return "(" + format(q.x()) + ", " + format(q.y()) + ", "
|
||||
+ format(q.z()) + ", " + format(q.w()) + ")";
|
||||
}
|
||||
|
||||
private static String formatVec(Vec3 value) {
|
||||
if (value == null) {
|
||||
return "null";
|
||||
}
|
||||
return "(" + format(value.x) + ", " + format(value.y) + ", " + format(value.z) + ")";
|
||||
}
|
||||
|
||||
private static String formatBox(AABB box) {
|
||||
return "[" + format(box.minX) + ", " + format(box.minY) + ", " + format(box.minZ)
|
||||
+ " -> " + format(box.maxX) + ", " + format(box.maxY) + ", " + format(box.maxZ) + "]";
|
||||
}
|
||||
|
||||
private static String format(double value) {
|
||||
return String.format(Locale.ROOT, "%.6f", value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
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;
|
||||
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
|
||||
* before Sable wraps the same on-ground invocation.
|
||||
*/
|
||||
@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)
|
||||
private void sablegravity$replaceUprightCollision(
|
||||
Vec3 motion,
|
||||
CallbackInfoReturnable<Vec3> cir
|
||||
) {
|
||||
Entity self = (Entity) (Object) this;
|
||||
if (self instanceof Player player) {
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
if (state.sablegravity$isEnabled()) {
|
||||
if (player.isLocalPlayer()) {
|
||||
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrameIfPresent(player);
|
||||
if (validFrame(player, frame)) {
|
||||
cir.setReturnValue(SableParentWorldCollision.collideOrTrust(
|
||||
player,
|
||||
motion,
|
||||
frame,
|
||||
sablegravity$insideMove
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "move", at = @At("HEAD"))
|
||||
private void sablegravity$beginMove(CallbackInfo ci) {
|
||||
sablegravity$insideMove = true;
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "move",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/entity/Entity;setOnGroundWithMovement(ZLnet/minecraft/world/phys/Vec3;)V",
|
||||
shift = At.Shift.BEFORE
|
||||
)
|
||||
)
|
||||
private void sablegravity$restoreLocalCollisionSemanticsBeforeGroundUpdate(
|
||||
CallbackInfo ci
|
||||
) {
|
||||
Entity self = (Entity) (Object) this;
|
||||
if (self instanceof Player player) {
|
||||
SableParentWorldCollision.restoreSableFlags(player);
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "move", at = @At("TAIL"))
|
||||
private void sablegravity$endMove(CallbackInfo ci) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import dev.nightly.sablegravity.GravityEntityState;
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
import dev.nightly.sablegravity.physics.GravityFrames;
|
||||
import dev.nightly.sablegravity.physics.SableFrameOrientation;
|
||||
import dev.nightly.sablegravity.physics.SableLocalPlayerPhysics;
|
||||
import dev.ryanhcode.sable.api.entity.EntitySubLevelUtil;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
@@ -12,14 +15,7 @@ import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/**
|
||||
* Exposes one authoritative ship-frame orientation to all Sable consumers.
|
||||
*
|
||||
* Sable's camera helper and its world-space OBB collision resolver both read
|
||||
* EntitySubLevelUtil's custom orientation. The direct logical-pose quaternion
|
||||
* maps player-local axes into world axes; Sable applies the inverse itself when
|
||||
* constructing the view matrix.
|
||||
*/
|
||||
/** Supplies Sable with a substep-interpolated full quaternion OBB orientation. */
|
||||
@Mixin(value = EntitySubLevelUtil.class, priority = 3000, remap = false)
|
||||
public abstract class EntityCustomOrientationMixin {
|
||||
@Inject(
|
||||
@@ -33,21 +29,19 @@ public abstract class EntityCustomOrientationMixin {
|
||||
float partialTicks,
|
||||
CallbackInfoReturnable<Quaterniondc> cir
|
||||
) {
|
||||
if (!(entity instanceof Player player)) {
|
||||
GravityEntityState state = (GravityEntityState) entity;
|
||||
if (state.sablegravity$isInLocalSimulation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
if (!state.sablegravity$isEnabled() || state.sablegravity$isInLocalSimulation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrame(player);
|
||||
SubLevel frame = resolveFrame(entity);
|
||||
if (frame == null || frame.isRemoved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
cir.setReturnValue(frame.logicalPose().orientation());
|
||||
cir.setReturnValue(
|
||||
SableFrameOrientation.getFrameOrientation(frame, partialTicks)
|
||||
);
|
||||
}
|
||||
|
||||
@Inject(
|
||||
@@ -60,20 +54,38 @@ public abstract class EntityCustomOrientationMixin {
|
||||
Entity entity,
|
||||
CallbackInfoReturnable<Boolean> cir
|
||||
) {
|
||||
if (!(entity instanceof Player player)) {
|
||||
GravityEntityState state = (GravityEntityState) entity;
|
||||
if (state.sablegravity$isInLocalSimulation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
if (!state.sablegravity$isEnabled() || state.sablegravity$isInLocalSimulation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrame(player);
|
||||
SubLevel frame = resolveFrame(entity);
|
||||
if (frame == null || frame.isRemoved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
cir.setReturnValue(true);
|
||||
}
|
||||
|
||||
private static SubLevel resolveFrame(Entity entity) {
|
||||
if (entity instanceof Player player) {
|
||||
if (!player.isLocalPlayer()) {
|
||||
return null;
|
||||
}
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
if (!state.sablegravity$isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
SubLevel attached = SableLocalPlayerPhysics.getAttachedFrameIfPresent(player);
|
||||
if (attached != null
|
||||
&& !attached.isRemoved()
|
||||
&& attached.getLevel() == player.level()) {
|
||||
return attached;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return GravityFrames.resolveSable(entity)
|
||||
.map(dev.nightly.sablegravity.physics.SableSubLevelGravityFrame::subLevel)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import dev.nightly.sablegravity.physics.DirectionalGravity;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Covers modded calls to applyGravity that occur outside the wrapped entity tick. */
|
||||
@Mixin(Entity.class)
|
||||
public abstract class EntityDirectionalGravityMixin {
|
||||
@Inject(method = "applyGravity", at = @At("HEAD"), cancellable = true)
|
||||
private void sablegravity$applyFrameGravity(CallbackInfo ci) {
|
||||
if (DirectionalGravity.apply((Entity) (Object) this)) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import dev.nightly.sablegravity.GravityEntityState;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Per-entity frame state. It is runtime state, not saved NBT: every load/warp re-resolves safely. */
|
||||
@Mixin(Entity.class)
|
||||
public abstract class EntityGravityStateMixin implements GravityEntityState {
|
||||
@Unique private boolean sablegravity$inLocalSimulation;
|
||||
@Unique private boolean sablegravity$localStateValid;
|
||||
@Unique private Vec3 sablegravity$localPosition = Vec3.ZERO;
|
||||
@Unique private Vec3 sablegravity$localVelocity = Vec3.ZERO;
|
||||
@Unique private UUID sablegravity$frameId;
|
||||
@Unique private ResourceLocation sablegravity$frameProvider;
|
||||
@Unique private ResourceLocation sablegravity$frameDimension;
|
||||
@Unique private boolean sablegravity$gravitySampleValid;
|
||||
@Unique private double sablegravity$gravitySample;
|
||||
@Unique private boolean sablegravity$frameHandoffPending;
|
||||
@Unique private int sablegravity$frameHandoffExpiryTick;
|
||||
|
||||
@Override public boolean sablegravity$isInLocalSimulation() { return sablegravity$inLocalSimulation; }
|
||||
@Override public void sablegravity$setInLocalSimulation(boolean value) { sablegravity$inLocalSimulation = value; }
|
||||
@Override public boolean sablegravity$isLocalStateValid() { return sablegravity$localStateValid; }
|
||||
@Override public void sablegravity$setLocalStateValid(boolean value) { sablegravity$localStateValid = value; }
|
||||
@Override public Vec3 sablegravity$getLocalPosition() { return sablegravity$localPosition; }
|
||||
@Override public void sablegravity$setLocalPosition(Vec3 value) { sablegravity$localPosition = value; }
|
||||
@Override public Vec3 sablegravity$getLocalVelocity() { return sablegravity$localVelocity; }
|
||||
@Override public void sablegravity$setLocalVelocity(Vec3 value) { sablegravity$localVelocity = value; }
|
||||
@Override public UUID sablegravity$getFrameId() { return sablegravity$frameId; }
|
||||
@Override public void sablegravity$setFrameId(UUID value) { sablegravity$frameId = value; }
|
||||
@Override public ResourceLocation sablegravity$getFrameProvider() { return sablegravity$frameProvider; }
|
||||
@Override public void sablegravity$setFrameProvider(ResourceLocation value) { sablegravity$frameProvider = value; }
|
||||
@Override public ResourceLocation sablegravity$getFrameDimension() { return sablegravity$frameDimension; }
|
||||
@Override public void sablegravity$setFrameDimension(ResourceLocation value) { sablegravity$frameDimension = value; }
|
||||
@Override public boolean sablegravity$isGravitySampleValid() { return sablegravity$gravitySampleValid; }
|
||||
@Override public void sablegravity$setGravitySampleValid(boolean value) { sablegravity$gravitySampleValid = value; }
|
||||
@Override public double sablegravity$getGravitySample() { return sablegravity$gravitySample; }
|
||||
@Override public void sablegravity$setGravitySample(double value) { sablegravity$gravitySample = value; }
|
||||
@Override public boolean sablegravity$isFrameHandoffPending() { return sablegravity$frameHandoffPending; }
|
||||
@Override public void sablegravity$setFrameHandoffPending(boolean value) { sablegravity$frameHandoffPending = value; }
|
||||
@Override public int sablegravity$getFrameHandoffExpiryTick() { return sablegravity$frameHandoffExpiryTick; }
|
||||
@Override public void sablegravity$setFrameHandoffExpiryTick(int value) { sablegravity$frameHandoffExpiryTick = value; }
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import dev.nightly.sablegravity.api.GravityFrame;
|
||||
import dev.nightly.sablegravity.physics.GravityFrames;
|
||||
import dev.nightly.sablegravity.physics.SableFrameOrientation;
|
||||
import dev.nightly.sablegravity.physics.SableOrientedBounds;
|
||||
import dev.nightly.sablegravity.physics.SableSubLevelGravityFrame;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.effect.MobEffects;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.animal.FlyingAnimal;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
/**
|
||||
* Expresses ordinary living-entity locomotion, gravity and drag in the selected frame while the
|
||||
* entity itself always remains in physical parent-world coordinates. Sable therefore stays the
|
||||
* sole owner of OBB collision and entity packet coordinate conversion.
|
||||
*/
|
||||
@Mixin(value = LivingEntity.class, priority = 3000)
|
||||
public abstract class LivingEntityDirectionalGravityMixin {
|
||||
private static final double AIR_VERTICAL_DRAG = 0.9800000190734863D;
|
||||
private static final double MOVEMENT_DAMPING = 0.91D;
|
||||
private static final double EPSILON = 1.0E-10D;
|
||||
|
||||
@WrapOperation(
|
||||
method = "handleRelativeFrictionAndCalculateMovement",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/entity/LivingEntity;moveRelative(FLnet/minecraft/world/phys/Vec3;)V"
|
||||
)
|
||||
)
|
||||
private void sablegravity$accelerateInFramePlane(
|
||||
LivingEntity self,
|
||||
float speed,
|
||||
Vec3 input,
|
||||
Operation<Void> original
|
||||
) {
|
||||
if (self instanceof Player) {
|
||||
original.call(self, speed, input);
|
||||
return;
|
||||
}
|
||||
|
||||
GravityFrame frame = GravityFrames.resolve(self).orElse(null);
|
||||
if (frame == null) {
|
||||
original.call(self, speed, input);
|
||||
return;
|
||||
}
|
||||
|
||||
Vec3 worldBefore = self.getDeltaMovement();
|
||||
self.setDeltaMovement(frame.toLocalVelocity(worldBefore));
|
||||
try {
|
||||
// Vanilla yaw and travel input describe heading in the entity's local floor plane.
|
||||
original.call(self, speed, input);
|
||||
self.setDeltaMovement(frame.toWorldVelocity(self.getDeltaMovement()));
|
||||
} catch (RuntimeException | Error failure) {
|
||||
self.setDeltaMovement(worldBefore);
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
@WrapOperation(
|
||||
method = "travel",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/entity/LivingEntity;setDeltaMovement(DDD)V",
|
||||
ordinal = 2
|
||||
)
|
||||
)
|
||||
private void sablegravity$finishDiscardFrictionTravelInFrame(
|
||||
LivingEntity self,
|
||||
double vanillaX,
|
||||
double vanillaY,
|
||||
double vanillaZ,
|
||||
Operation<Void> original
|
||||
) {
|
||||
sablegravity$finishOrdinaryTravel(
|
||||
self,
|
||||
vanillaX,
|
||||
vanillaY,
|
||||
vanillaZ,
|
||||
original,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
@WrapOperation(
|
||||
method = "travel",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/entity/LivingEntity;setDeltaMovement(DDD)V",
|
||||
ordinal = 3
|
||||
)
|
||||
)
|
||||
private void sablegravity$finishOrdinaryTravelInFrame(
|
||||
LivingEntity self,
|
||||
double vanillaX,
|
||||
double vanillaY,
|
||||
double vanillaZ,
|
||||
Operation<Void> original
|
||||
) {
|
||||
sablegravity$finishOrdinaryTravel(
|
||||
self,
|
||||
vanillaX,
|
||||
vanillaY,
|
||||
vanillaZ,
|
||||
original,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
@Unique
|
||||
private static void sablegravity$finishOrdinaryTravel(
|
||||
LivingEntity self,
|
||||
double vanillaX,
|
||||
double vanillaY,
|
||||
double vanillaZ,
|
||||
Operation<Void> original,
|
||||
boolean discardFriction
|
||||
) {
|
||||
if (self instanceof Player) {
|
||||
original.call(self, vanillaX, vanillaY, vanillaZ);
|
||||
return;
|
||||
}
|
||||
|
||||
GravityFrame frame = GravityFrames.resolve(self).orElse(null);
|
||||
if (frame == null) {
|
||||
original.call(self, vanillaX, vanillaY, vanillaZ);
|
||||
return;
|
||||
}
|
||||
|
||||
Vec3 localVelocity = frame.toLocalVelocity(self.getDeltaMovement());
|
||||
double localY = localVelocity.y;
|
||||
if (self.hasEffect(MobEffects.LEVITATION)) {
|
||||
int amplifier = self.getEffect(MobEffects.LEVITATION).getAmplifier();
|
||||
localY += (0.05D * (amplifier + 1) - localY) * 0.2D;
|
||||
} else {
|
||||
double gravity = Math.max(0.0D, self.getGravity());
|
||||
if (localY <= 0.0D && self.hasEffect(MobEffects.SLOW_FALLING)) {
|
||||
gravity = Math.min(gravity, 0.01D);
|
||||
self.fallDistance = 0.0F;
|
||||
}
|
||||
localY -= gravity;
|
||||
}
|
||||
|
||||
double horizontalDrag = discardFriction
|
||||
? 1.0D
|
||||
: sablegravity$getHorizontalDrag(self, frame);
|
||||
double verticalDrag = self instanceof FlyingAnimal
|
||||
? horizontalDrag
|
||||
: AIR_VERTICAL_DRAG;
|
||||
Vec3 finalLocalVelocity = new Vec3(
|
||||
localVelocity.x * horizontalDrag,
|
||||
localY * verticalDrag,
|
||||
localVelocity.z * horizontalDrag
|
||||
);
|
||||
Vec3 finalWorldVelocity = frame.toWorldVelocity(finalLocalVelocity);
|
||||
original.call(
|
||||
self,
|
||||
finalWorldVelocity.x,
|
||||
finalWorldVelocity.y,
|
||||
finalWorldVelocity.z
|
||||
);
|
||||
}
|
||||
|
||||
@Unique
|
||||
private static double sablegravity$getHorizontalDrag(
|
||||
LivingEntity self,
|
||||
GravityFrame frame
|
||||
) {
|
||||
if (!self.onGround()) {
|
||||
return MOVEMENT_DAMPING;
|
||||
}
|
||||
|
||||
Vec3 worldFeet = frame instanceof SableSubLevelGravityFrame sableFrame
|
||||
? SableOrientedBounds.worldFeet(
|
||||
self,
|
||||
SableFrameOrientation.getFrameOrientation(sableFrame.subLevel(), 1.0F)
|
||||
)
|
||||
: self.position();
|
||||
Vec3 localPosition = frame.toLocalPosition(worldFeet);
|
||||
BlockPos localBelow = BlockPos.containing(
|
||||
localPosition.x,
|
||||
localPosition.y - 0.5000001D,
|
||||
localPosition.z
|
||||
);
|
||||
var blockState = self.level().getBlockState(localBelow);
|
||||
float friction = blockState.getFriction(self.level(), localBelow, self);
|
||||
if (!Float.isFinite(friction) || friction <= EPSILON) {
|
||||
friction = 0.6F;
|
||||
}
|
||||
return friction * MOVEMENT_DAMPING;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
/** Exposes the normalized jump input already copied by LocalPlayer.serverAiStep. */
|
||||
@Mixin(LivingEntity.class)
|
||||
public interface LivingEntityInputAccessor {
|
||||
@Accessor("jumping")
|
||||
boolean sablegravity$isJumpingInput();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
import dev.nightly.sablegravity.physics.SableLocalPlayerPhysics;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/** Gives Shift its crouch pose and dimensions while creative flight uses it for local descent. */
|
||||
@Mixin(targets = "net.minecraft.client.player.LocalPlayer", priority = 4000)
|
||||
public abstract class LocalPlayerFlightCrouchMixin {
|
||||
@Inject(method = "isCrouching", at = @At("HEAD"), cancellable = true)
|
||||
private void sablegravity$crouchWhileFlying(CallbackInfoReturnable<Boolean> cir) {
|
||||
Player player = (Player) (Object) this;
|
||||
if (!player.getAbilities().flying
|
||||
|| !player.isShiftKeyDown()
|
||||
|| !((SableGravityState) player).sablegravity$isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrameIfPresent(player);
|
||||
if (frame != null && !frame.isRemoved() && frame.getLevel() == player.level()) {
|
||||
cir.setReturnValue(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
import dev.nightly.sablegravity.physics.SableLocalCollisionProbe;
|
||||
import dev.nightly.sablegravity.physics.SableLocalPlayerPhysics;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3dc;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.world.entity.MoverType;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
@@ -14,10 +14,7 @@ import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/**
|
||||
* Replaces vanilla/Sable edge sneaking with a full ship-local query.
|
||||
* The synthetic AABB exists only in hidden plot coordinates and is never written to Entity.bb.
|
||||
*/
|
||||
/** Replaces world-axis edge sneaking with a tracked-frame local collision-shape query. */
|
||||
@Mixin(value = Player.class, priority = 4000)
|
||||
public abstract class PlayerEdgeSneakMixin {
|
||||
private static final double STEP = 0.05D;
|
||||
@@ -36,7 +33,6 @@ public abstract class PlayerEdgeSneakMixin {
|
||||
|| !state.sablegravity$isEnabled()
|
||||
|| player.getAbilities().flying
|
||||
|| !player.isShiftKeyDown()
|
||||
|| !SableLocalPlayerPhysics.isLocalGrounded(player)
|
||||
|| (moverType != MoverType.SELF && moverType != MoverType.PLAYER)) {
|
||||
return;
|
||||
}
|
||||
@@ -46,64 +42,68 @@ public abstract class PlayerEdgeSneakMixin {
|
||||
return;
|
||||
}
|
||||
|
||||
Pose3dc pose = frame.logicalPose();
|
||||
Vec3 localMovement = pose.transformNormalInverse(movement);
|
||||
// The player position still belongs to lastPose here, but the requested displacement was
|
||||
// encoded by the local travel kernel with logicalPose. Decode/encode the vector with that
|
||||
// same basis while probing the body's start-of-sweep feet from lastPose.
|
||||
Pose3dc movementPose = frame.logicalPose();
|
||||
Vec3 localMovement = movementPose.transformNormalInverse(movement);
|
||||
if (localMovement.y > 0.0D) {
|
||||
cir.setReturnValue(movement);
|
||||
return;
|
||||
}
|
||||
|
||||
Vec3 localFeet = pose.transformPositionInverse(player.position());
|
||||
double halfWidth = player.getBbWidth() * 0.5D;
|
||||
double height = player.getBbHeight();
|
||||
AABB localBounds = new AABB(
|
||||
localFeet.x - halfWidth,
|
||||
localFeet.y,
|
||||
localFeet.z - halfWidth,
|
||||
localFeet.x + halfWidth,
|
||||
localFeet.y + height,
|
||||
localFeet.z + halfWidth
|
||||
);
|
||||
Vec3 localFeet = SableLocalCollisionProbe.getLocalFeetAtCollisionStart(player, frame);
|
||||
if (!SableLocalPlayerPhysics.isLocalGrounded(player)
|
||||
&& !SableLocalCollisionProbe.hasSupport(
|
||||
player,
|
||||
frame,
|
||||
SableLocalCollisionProbe.createFloorProbe(player, localFeet, 0.08D)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
AABB localBounds = SableLocalCollisionProbe.createLocalBody(player, localFeet);
|
||||
|
||||
Level level = player.level();
|
||||
double fallDistance = player.maxUpStep();
|
||||
double x = localMovement.x;
|
||||
double z = localMovement.z;
|
||||
|
||||
while (x != 0.0D && wouldSlideOff(level, player, localBounds, x, 0.0D, fallDistance)) {
|
||||
while (x != 0.0D && wouldSlideOff(player, frame, localBounds, x, 0.0D, fallDistance)) {
|
||||
x = reduceTowardsZero(x);
|
||||
}
|
||||
while (z != 0.0D && wouldSlideOff(level, player, localBounds, 0.0D, z, fallDistance)) {
|
||||
while (z != 0.0D && wouldSlideOff(player, frame, localBounds, 0.0D, z, fallDistance)) {
|
||||
z = reduceTowardsZero(z);
|
||||
}
|
||||
while (x != 0.0D
|
||||
&& z != 0.0D
|
||||
&& wouldSlideOff(level, player, localBounds, x, z, fallDistance)) {
|
||||
&& wouldSlideOff(player, frame, localBounds, x, z, fallDistance)) {
|
||||
x = reduceTowardsZero(x);
|
||||
z = reduceTowardsZero(z);
|
||||
}
|
||||
|
||||
cir.setReturnValue(pose.transformNormal(new Vec3(x, localMovement.y, z)));
|
||||
cir.setReturnValue(
|
||||
movementPose.transformNormal(new Vec3(x, localMovement.y, z))
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean wouldSlideOff(
|
||||
Level level,
|
||||
Player player,
|
||||
SubLevel frame,
|
||||
AABB localBounds,
|
||||
double localX,
|
||||
double localZ,
|
||||
double fallDistance
|
||||
) {
|
||||
AABB future = localBounds.move(localX, 0.0D, localZ);
|
||||
double inset = Math.min(1.0E-5D, Math.max(0.0D, future.getXsize() * 0.25D));
|
||||
AABB floorProbe = new AABB(
|
||||
future.minX,
|
||||
future.minX + inset,
|
||||
future.minY - fallDistance - FLOOR_EPSILON,
|
||||
future.minZ,
|
||||
future.maxX,
|
||||
future.minY,
|
||||
future.maxZ
|
||||
future.minZ + inset,
|
||||
future.maxX - inset,
|
||||
future.minY + FLOOR_EPSILON,
|
||||
future.maxZ - inset
|
||||
);
|
||||
return level.noCollision(player, floorProbe);
|
||||
return !SableLocalCollisionProbe.hasSupport(player, frame, floorProbe);
|
||||
}
|
||||
|
||||
private static double reduceTowardsZero(double value) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
import dev.nightly.sablegravity.physics.SableLocalPlayerPhysics;
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
/** Lets local-frame creative descent use the real crouch pose and 1.5-block body. */
|
||||
@Mixin(Player.class)
|
||||
public abstract class PlayerFlightCrouchPoseMixin {
|
||||
@ModifyExpressionValue(
|
||||
method = "updatePlayerPose",
|
||||
at = @At(
|
||||
value = "FIELD",
|
||||
target = "Lnet/minecraft/world/entity/player/Abilities;flying:Z"
|
||||
)
|
||||
)
|
||||
private boolean sablegravity$allowCrouchPoseDuringLocalFlight(boolean flying) {
|
||||
if (!flying) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Player player = (Player) (Object) this;
|
||||
if (!player.isShiftKeyDown()
|
||||
|| !((SableGravityState) player).sablegravity$isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
SubLevel frame = player.isLocalPlayer()
|
||||
? SableLocalPlayerPhysics.getAttachedFrameIfPresent(player)
|
||||
: Sable.HELPER.getTrackingSubLevel(player);
|
||||
return frame == null || frame.isRemoved() || frame.getLevel() != player.level();
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import net.minecraft.network.syncher.EntityDataAccessor;
|
||||
import net.minecraft.network.syncher.EntityDataSerializers;
|
||||
import net.minecraft.network.syncher.SynchedEntityData;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import java.util.UUID;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
@@ -24,12 +22,6 @@ public abstract class PlayerGravityDataMixin implements SableGravityState {
|
||||
private static final EntityDataAccessor<Float> SABLEGRAVITY_STRENGTH =
|
||||
SynchedEntityData.defineId(Player.class, EntityDataSerializers.FLOAT);
|
||||
|
||||
@Unique private boolean sablegravity$inLocalSimulation;
|
||||
@Unique private boolean sablegravity$localStateValid;
|
||||
@Unique private Vec3 sablegravity$localPosition = Vec3.ZERO;
|
||||
@Unique private Vec3 sablegravity$localVelocity = Vec3.ZERO;
|
||||
@Unique private UUID sablegravity$frameId;
|
||||
|
||||
@Inject(method = "defineSynchedData", at = @At("TAIL"))
|
||||
private void sablegravity$defineData(SynchedEntityData.Builder builder, CallbackInfo ci) {
|
||||
builder.define(SABLEGRAVITY_ENABLED, false);
|
||||
@@ -44,8 +36,10 @@ public abstract class PlayerGravityDataMixin implements SableGravityState {
|
||||
@Override
|
||||
public void sablegravity$setEnabled(boolean enabled) {
|
||||
((Player) (Object) this).getEntityData().set(SABLEGRAVITY_ENABLED, enabled);
|
||||
sablegravity$localStateValid = false;
|
||||
sablegravity$frameId = null;
|
||||
sablegravity$setLocalStateValid(false);
|
||||
sablegravity$setFrameId(null);
|
||||
sablegravity$setFrameProvider(null);
|
||||
sablegravity$setFrameDimension(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -58,14 +52,4 @@ public abstract class PlayerGravityDataMixin implements SableGravityState {
|
||||
((Player) (Object) this).getEntityData().set(SABLEGRAVITY_STRENGTH, strength);
|
||||
}
|
||||
|
||||
@Override public boolean sablegravity$isInLocalSimulation() { return sablegravity$inLocalSimulation; }
|
||||
@Override public void sablegravity$setInLocalSimulation(boolean value) { sablegravity$inLocalSimulation = value; }
|
||||
@Override public boolean sablegravity$isLocalStateValid() { return sablegravity$localStateValid; }
|
||||
@Override public void sablegravity$setLocalStateValid(boolean value) { sablegravity$localStateValid = value; }
|
||||
@Override public Vec3 sablegravity$getLocalPosition() { return sablegravity$localPosition; }
|
||||
@Override public void sablegravity$setLocalPosition(Vec3 value) { sablegravity$localPosition = value; }
|
||||
@Override public Vec3 sablegravity$getLocalVelocity() { return sablegravity$localVelocity; }
|
||||
@Override public void sablegravity$setLocalVelocity(Vec3 value) { sablegravity$localVelocity = value; }
|
||||
@Override public UUID sablegravity$getFrameId() { return sablegravity$frameId; }
|
||||
@Override public void sablegravity$setFrameId(UUID value) { sablegravity$frameId = value; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
import dev.nightly.sablegravity.physics.SableFrameOrientation;
|
||||
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.sublevel.SubLevel;
|
||||
import net.minecraft.world.entity.EntityDimensions;
|
||||
import net.minecraft.world.entity.Pose;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/** Uses the exact proposed OBB for crouch/stand clearance instead of an upright temporary AABB. */
|
||||
@Mixin(value = Player.class, priority = 4500)
|
||||
public abstract class PlayerPoseFitMixin {
|
||||
@Inject(method = "canPlayerFitWithinBlocksAndEntitiesWhen", at = @At("HEAD"), cancellable = true)
|
||||
private void sablegravity$canOrientedBodyFit(
|
||||
Pose pose,
|
||||
CallbackInfoReturnable<Boolean> cir
|
||||
) {
|
||||
Player player = (Player) (Object) this;
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
if (!state.sablegravity$isEnabled() || !player.isLocalPlayer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrameIfPresent(player);
|
||||
if (frame == null || frame.isRemoved() || frame.getLevel() != player.level()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Quaterniondc orientation = SableFrameOrientation.getFrameOrientation(frame, 1.0F);
|
||||
Vec3 physicalFeet = SableOrientedBounds.worldFeet(player, orientation);
|
||||
EntityDimensions dimensions = player.getDimensions(pose);
|
||||
boolean plotFit = SableLocalCollisionProbe.canFit(player, frame, dimensions);
|
||||
boolean parentFit = SableParentWorldCollision.canFit(
|
||||
player,
|
||||
physicalFeet,
|
||||
dimensions,
|
||||
orientation
|
||||
);
|
||||
cir.setReturnValue(plotFit && parentFit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import dev.nightly.sablegravity.GravityEntityState;
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
import dev.nightly.sablegravity.physics.SableLocalPlayerPhysics;
|
||||
import dev.ryanhcode.sable.ActiveSableCompanion;
|
||||
import dev.ryanhcode.sable.api.math.LevelReusedVectors;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3dc;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.entity_collision.SubLevelEntityCollision;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
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.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Prevents Sable from stacking its residual world-Y hitbox yaw on top of our complete
|
||||
* quaternion orientation.
|
||||
*
|
||||
* The vanilla Sable path computes getHitBoxYaw() from a reusable pose before that pose is
|
||||
* updated for the current substep. That is harmless for ordinary upright entities, but a
|
||||
* custom full-orientation OBB would receive both the complete frame quaternion and this
|
||||
* stale extra yaw. For gravity players the custom quaternion is authoritative, so yaw=0.
|
||||
*/
|
||||
@Mixin(value = SubLevelEntityCollision.class, priority = 3500, remap = false)
|
||||
public abstract class SableCollisionYawMixin {
|
||||
|
||||
@Redirect(
|
||||
method = "collide",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Ldev/ryanhcode/sable/sublevel/entity_collision/SubLevelEntityCollision;getHitBoxYaw(Ldev/ryanhcode/sable/companion/math/Pose3dc;)D"
|
||||
),
|
||||
remap = false
|
||||
)
|
||||
private static double sablegravity$useAuthoritativeCustomOrientation(
|
||||
Pose3dc pose,
|
||||
Entity entity,
|
||||
Vec3 collisionMotion,
|
||||
Vec3 velocityMotion,
|
||||
LevelReusedVectors sink
|
||||
) {
|
||||
if (entity instanceof Player player) {
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrameIfPresent(player);
|
||||
if (state.sablegravity$isEnabled()
|
||||
&& !state.sablegravity$isInLocalSimulation()
|
||||
&& frame != null
|
||||
&& !frame.isRemoved()) {
|
||||
return 0.0D;
|
||||
}
|
||||
} else {
|
||||
GravityEntityState state = (GravityEntityState) entity;
|
||||
if (!state.sablegravity$isInLocalSimulation()
|
||||
&& state.sablegravity$getFrameId() != null) {
|
||||
return 0.0D;
|
||||
}
|
||||
}
|
||||
|
||||
return SubLevelEntityCollision.getHitBoxYaw(pose);
|
||||
}
|
||||
|
||||
/** Rotates non-player bodies around their invariant raw feet position, never around the eye. */
|
||||
@Inject(
|
||||
method = "transformEntityBoundsCenter",
|
||||
at = @At("HEAD"),
|
||||
cancellable = true,
|
||||
remap = false
|
||||
)
|
||||
private static void sablegravity$useFeetAnchoredBodyCenter(
|
||||
LevelReusedVectors sink,
|
||||
Quaterniondc orientation,
|
||||
Entity entity,
|
||||
Vector3d center,
|
||||
CallbackInfo ci
|
||||
) {
|
||||
if (entity instanceof Player || orientation == null) {
|
||||
return;
|
||||
}
|
||||
GravityEntityState state = (GravityEntityState) entity;
|
||||
if (state.sablegravity$isInLocalSimulation()
|
||||
|| state.sablegravity$getFrameId() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
double halfHeight = entity.getBoundingBox().getYsize() * 0.5D;
|
||||
Vector3d offset = orientation.transform(
|
||||
new Vector3d(0.0D, halfHeight, 0.0D),
|
||||
new Vector3d()
|
||||
);
|
||||
center.set(
|
||||
entity.getX() + offset.x,
|
||||
entity.getY() + offset.y,
|
||||
entity.getZ() + offset.z
|
||||
);
|
||||
ci.cancel();
|
||||
}
|
||||
|
||||
/** Keeps Sable's inherited-motion samples on the same feet-anchored convention. */
|
||||
@Redirect(
|
||||
method = "collide",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Ldev/ryanhcode/sable/ActiveSableCompanion;getFeetPos(Lnet/minecraft/world/entity/Entity;FLorg/joml/Quaterniondc;)Lorg/joml/Vector3d;"
|
||||
),
|
||||
remap = false
|
||||
)
|
||||
private static Vector3d sablegravity$getFeetPosWithoutEyeOffset(
|
||||
ActiveSableCompanion companion,
|
||||
Entity entity,
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
return companion.getFeetPos(entity, partialTicks, orientation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package dev.nightly.sablegravity.physics;
|
||||
|
||||
import dev.nightly.sablegravity.GravityEntityState;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Applies non-player gravity along the selected frame without changing coordinate spaces. */
|
||||
public final class DirectionalGravity {
|
||||
private DirectionalGravity() {}
|
||||
|
||||
public static boolean apply(Entity entity) {
|
||||
GravityEntityState state = (GravityEntityState) entity;
|
||||
if (entity instanceof Player || state.sablegravity$isInLocalSimulation()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var frame = GravityFrames.resolve(entity).orElse(null);
|
||||
if (frame == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double magnitude = frame.gravityPerTick(entity);
|
||||
if (magnitude == 0.0D) {
|
||||
return true;
|
||||
}
|
||||
Vec3 down = frame.down(1.0F);
|
||||
entity.setDeltaMovement(entity.getDeltaMovement().add(down.scale(magnitude)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package dev.nightly.sablegravity.physics;
|
||||
|
||||
import dev.nightly.sablegravity.GravityEntityState;
|
||||
import dev.nightly.sablegravity.SableGravityMod;
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
import dev.nightly.sablegravity.api.GravityFrame;
|
||||
import dev.nightly.sablegravity.api.GravityFrameId;
|
||||
import dev.nightly.sablegravity.api.GravityFrameProvider;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/** Thread-safe provider registry and the only place where a gravity frame is selected. */
|
||||
public final class GravityFrames {
|
||||
private static final CopyOnWriteArrayList<Entry> PROVIDERS = new CopyOnWriteArrayList<>();
|
||||
private static final AtomicLong NEXT_ORDER = new AtomicLong();
|
||||
|
||||
static {
|
||||
register(new SableSubLevelFrameProvider(), 0);
|
||||
}
|
||||
|
||||
private GravityFrames() {}
|
||||
|
||||
public static void register(GravityFrameProvider provider, int priority) {
|
||||
if (provider == null) {
|
||||
throw new IllegalArgumentException("provider cannot be null");
|
||||
}
|
||||
PROVIDERS.add(new Entry(provider, priority, NEXT_ORDER.getAndIncrement()));
|
||||
PROVIDERS.sort(
|
||||
Comparator.comparingInt(Entry::priority).reversed()
|
||||
.thenComparingLong(Entry::order)
|
||||
);
|
||||
}
|
||||
|
||||
public static Optional<GravityFrame> resolve(Entity entity) {
|
||||
if (!isEnabledFor(entity)) {
|
||||
clearBinding(entity);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
GravityFrameId previous = previousId(entity);
|
||||
GravityEntityState state = (GravityEntityState) entity;
|
||||
if (previous != null
|
||||
&& !previous.dimension().equals(entity.level().dimension().location())
|
||||
&& state.sablegravity$isLocalStateValid()) {
|
||||
markDimensionHandoff(entity);
|
||||
}
|
||||
for (Entry entry : PROVIDERS) {
|
||||
try {
|
||||
Optional<? extends GravityFrame> result =
|
||||
entry.provider().resolve(entity, previous);
|
||||
if (result.isPresent() && validFor(result.get(), entity.level())) {
|
||||
GravityFrame frame = result.get();
|
||||
bind(entity, frame.id());
|
||||
return Optional.of(frame);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
SableGravityMod.LOGGER.warn(
|
||||
"Gravity frame provider {} failed for {}",
|
||||
entry.provider().getClass().getName(),
|
||||
entity,
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
clearBinding(entity);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static Optional<GravityFrame> resolveAt(
|
||||
Level level,
|
||||
Vec3 position,
|
||||
GravityFrameId previous
|
||||
) {
|
||||
for (Entry entry : PROVIDERS) {
|
||||
try {
|
||||
Optional<? extends GravityFrame> result =
|
||||
entry.provider().resolveAt(level, position, previous);
|
||||
if (result.isPresent() && validFor(result.get(), level)) {
|
||||
return Optional.of(result.get());
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
SableGravityMod.LOGGER.warn(
|
||||
"Gravity frame provider {} failed at {} in {}",
|
||||
entry.provider().getClass().getName(),
|
||||
position,
|
||||
level.dimension().location(),
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static Optional<SableSubLevelGravityFrame> resolveSable(Entity entity) {
|
||||
return resolve(entity)
|
||||
.filter(SableSubLevelGravityFrame.class::isInstance)
|
||||
.map(SableSubLevelGravityFrame.class::cast);
|
||||
}
|
||||
|
||||
private static boolean isEnabledFor(Entity entity) {
|
||||
if (entity instanceof Player player) {
|
||||
return ((SableGravityState) player).sablegravity$isEnabled();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean validFor(GravityFrame frame, Level level) {
|
||||
return frame.isValid() && frame.level() == level;
|
||||
}
|
||||
|
||||
private static GravityFrameId previousId(Entity entity) {
|
||||
GravityEntityState state = (GravityEntityState) entity;
|
||||
if (state.sablegravity$getFrameId() == null
|
||||
|| state.sablegravity$getFrameProvider() == null
|
||||
|| state.sablegravity$getFrameDimension() == null) {
|
||||
return null;
|
||||
}
|
||||
return new GravityFrameId(
|
||||
state.sablegravity$getFrameProvider(),
|
||||
state.sablegravity$getFrameDimension(),
|
||||
state.sablegravity$getFrameId()
|
||||
);
|
||||
}
|
||||
|
||||
private static void bind(Entity entity, GravityFrameId id) {
|
||||
GravityEntityState state = (GravityEntityState) entity;
|
||||
boolean changed = !id.localId().equals(state.sablegravity$getFrameId())
|
||||
|| !id.provider().equals(state.sablegravity$getFrameProvider())
|
||||
|| !id.dimension().equals(state.sablegravity$getFrameDimension());
|
||||
state.sablegravity$setFrameId(id.localId());
|
||||
state.sablegravity$setFrameProvider(id.provider());
|
||||
state.sablegravity$setFrameDimension(id.dimension());
|
||||
if (changed) {
|
||||
state.sablegravity$setLocalStateValid(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearBinding(Entity entity) {
|
||||
GravityEntityState state = (GravityEntityState) entity;
|
||||
state.sablegravity$setFrameId(null);
|
||||
state.sablegravity$setFrameProvider(null);
|
||||
state.sablegravity$setFrameDimension(null);
|
||||
state.sablegravity$setLocalStateValid(false);
|
||||
}
|
||||
|
||||
public static void markDimensionHandoff(Entity entity) {
|
||||
GravityEntityState state = (GravityEntityState) entity;
|
||||
if (!state.sablegravity$isLocalStateValid()) {
|
||||
return;
|
||||
}
|
||||
state.sablegravity$setFrameHandoffPending(true);
|
||||
state.sablegravity$setFrameHandoffExpiryTick(entity.tickCount + 100);
|
||||
}
|
||||
|
||||
public static boolean consumeDimensionHandoff(Entity entity, GravityFrame destination) {
|
||||
GravityEntityState state = (GravityEntityState) entity;
|
||||
if (!state.sablegravity$isFrameHandoffPending()) {
|
||||
return false;
|
||||
}
|
||||
state.sablegravity$setFrameHandoffPending(false);
|
||||
if (entity.tickCount > state.sablegravity$getFrameHandoffExpiryTick()) {
|
||||
return false;
|
||||
}
|
||||
Vec3 localVelocity = state.sablegravity$getLocalVelocity();
|
||||
if (localVelocity == null
|
||||
|| !Double.isFinite(localVelocity.x)
|
||||
|| !Double.isFinite(localVelocity.y)
|
||||
|| !Double.isFinite(localVelocity.z)) {
|
||||
return false;
|
||||
}
|
||||
entity.setDeltaMovement(destination.toWorldVelocity(localVelocity));
|
||||
return true;
|
||||
}
|
||||
|
||||
public static List<GravityFrameProvider> providers() {
|
||||
return PROVIDERS.stream().map(Entry::provider).toList();
|
||||
}
|
||||
|
||||
private record Entry(GravityFrameProvider provider, int priority, long order) {}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package dev.nightly.sablegravity.physics;
|
||||
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import org.joml.Quaterniond;
|
||||
|
||||
/**
|
||||
* Produces the exact player-body orientation for each of Sable's collision substeps.
|
||||
*
|
||||
* Sable sweeps the local player in eight interpolated poses. Returning the logical-pose
|
||||
* endpoint for every request makes the player OBB rotate ahead of the ship collision OBB,
|
||||
* so SAT resolves a different penetration direction on every substep. The interpolation
|
||||
* here keeps both bodies in the same rotating frame for arbitrary quaternions.
|
||||
*/
|
||||
public final class SableFrameOrientation {
|
||||
private SableFrameOrientation() {}
|
||||
|
||||
public static Quaterniond getFrameOrientation(SubLevel frame, float partialTicks) {
|
||||
double t = clamp(partialTicks, 0.0D, 1.0D);
|
||||
return new Quaterniond(frame.lastPose().orientation())
|
||||
.slerp(frame.logicalPose().orientation(), t)
|
||||
.normalize();
|
||||
}
|
||||
|
||||
private static double clamp(double value, double min, double max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package dev.nightly.sablegravity.physics;
|
||||
|
||||
import dev.ryanhcode.sable.physics.impl.SubLevelEntityCollisionContext;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.util.LevelAccelerator;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.entity.EntityDimensions;
|
||||
import net.minecraft.world.level.block.ScaffoldingBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
/** Collision probes expressed entirely in the tracked SubLevel's local block coordinates. */
|
||||
public final class SableLocalCollisionProbe {
|
||||
private static final double BLOCK_RANGE_EPSILON = 1.0E-7D;
|
||||
private static final double FOOTPRINT_INSET = 1.0E-5D;
|
||||
private static final double FLOOR_EPSILON = 1.0E-5D;
|
||||
|
||||
private SableLocalCollisionProbe() {}
|
||||
|
||||
/** Returns the physical oriented feet position at the current logical pose. */
|
||||
public static Vec3 getLocalFeet(Player player, SubLevel frame) {
|
||||
return SableOrientedBounds.localFeet(
|
||||
player,
|
||||
frame.logicalPose(),
|
||||
SableFrameOrientation.getFrameOrientation(frame, 1.0F)
|
||||
);
|
||||
}
|
||||
|
||||
/** Returns physical feet at the start of Sable's next lastPose -> logicalPose sweep. */
|
||||
public static Vec3 getLocalFeetAtCollisionStart(Player player, SubLevel frame) {
|
||||
return SableOrientedBounds.localFeet(
|
||||
player,
|
||||
frame.lastPose(),
|
||||
SableFrameOrientation.getFrameOrientation(frame, 0.0F)
|
||||
);
|
||||
}
|
||||
|
||||
public static AABB createLocalBody(Player player, Vec3 localFeet) {
|
||||
double halfWidth = player.getBbWidth() * 0.5D;
|
||||
return new AABB(
|
||||
localFeet.x - halfWidth,
|
||||
localFeet.y,
|
||||
localFeet.z - halfWidth,
|
||||
localFeet.x + halfWidth,
|
||||
localFeet.y + player.getBbHeight(),
|
||||
localFeet.z + halfWidth
|
||||
);
|
||||
}
|
||||
|
||||
/** Thin support slab directly beneath the body footprint, inset to reject adjacent walls. */
|
||||
public static AABB createFloorProbe(Player player, Vec3 localFeet, double depth) {
|
||||
double halfWidth = Math.max(
|
||||
0.0D,
|
||||
player.getBbWidth() * 0.5D - FOOTPRINT_INSET
|
||||
);
|
||||
return new AABB(
|
||||
localFeet.x - halfWidth,
|
||||
localFeet.y - Math.max(0.0D, depth) - FLOOR_EPSILON,
|
||||
localFeet.z - halfWidth,
|
||||
localFeet.x + halfWidth,
|
||||
localFeet.y + FLOOR_EPSILON,
|
||||
localFeet.z + halfWidth
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean hasSupportAtCollisionStart(Player player, SubLevel frame) {
|
||||
Vec3 localFeet = getLocalFeetAtCollisionStart(player, frame);
|
||||
return hasSupport(player, frame, createFloorProbe(player, localFeet, 0.08D));
|
||||
}
|
||||
|
||||
public static boolean hasSupportNow(Player player, SubLevel frame) {
|
||||
Vec3 localFeet = getLocalFeet(player, frame);
|
||||
return hasSupport(player, frame, createFloorProbe(player, localFeet, 0.08D));
|
||||
}
|
||||
|
||||
/** Exact pose-fit test in plot coordinates; Entity.bb never participates. */
|
||||
public static boolean canFit(Player player, SubLevel frame, EntityDimensions dimensions) {
|
||||
return canFitAtLocalFeet(player, frame, dimensions, getLocalFeet(player, frame));
|
||||
}
|
||||
|
||||
/** Exact pose-fit test at an explicit plot-local feet point. */
|
||||
public static boolean canFitAtLocalFeet(
|
||||
Player player,
|
||||
SubLevel frame,
|
||||
EntityDimensions dimensions,
|
||||
Vec3 localFeet
|
||||
) {
|
||||
double halfWidth = Math.max(0.0D, dimensions.width() * 0.5D - FLOOR_EPSILON);
|
||||
double height = Math.max(0.0D, dimensions.height() - FLOOR_EPSILON * 2.0D);
|
||||
AABB localBody = new AABB(
|
||||
localFeet.x - halfWidth,
|
||||
localFeet.y + FLOOR_EPSILON,
|
||||
localFeet.z - halfWidth,
|
||||
localFeet.x + halfWidth,
|
||||
localFeet.y + FLOOR_EPSILON + height,
|
||||
localFeet.z + halfWidth
|
||||
);
|
||||
return !intersectsCollisionShape(player, frame, localBody);
|
||||
}
|
||||
|
||||
|
||||
/** True when at least one tracked-frame collision shape overlaps the local probe. */
|
||||
public static boolean hasSupport(Player player, SubLevel frame, AABB localProbe) {
|
||||
return intersectsCollisionShape(player, frame, localProbe);
|
||||
}
|
||||
|
||||
private static boolean intersectsCollisionShape(
|
||||
Player player,
|
||||
SubLevel frame,
|
||||
AABB localProbe
|
||||
) {
|
||||
LevelAccelerator level = new LevelAccelerator(player.level());
|
||||
|
||||
int minX = Mth.floor(localProbe.minX);
|
||||
int minY = Mth.floor(localProbe.minY);
|
||||
int minZ = Mth.floor(localProbe.minZ);
|
||||
int maxX = Mth.floor(localProbe.maxX - BLOCK_RANGE_EPSILON);
|
||||
int maxY = Mth.floor(localProbe.maxY - BLOCK_RANGE_EPSILON);
|
||||
int maxZ = Mth.floor(localProbe.maxZ - BLOCK_RANGE_EPSILON);
|
||||
|
||||
if (maxX < minX || maxY < minY || maxZ < minZ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (BlockPos pos : BlockPos.betweenClosed(minX, minY, minZ, maxX, maxY, maxZ)) {
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (state.isAir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
VoxelShape shape;
|
||||
if (state.getBlock() instanceof ScaffoldingBlock) {
|
||||
shape = state.getCollisionShape(
|
||||
level,
|
||||
pos,
|
||||
new SubLevelEntityCollisionContext(player)
|
||||
);
|
||||
} else {
|
||||
shape = state.getCollisionShape(level, pos, CollisionContext.of(player));
|
||||
}
|
||||
|
||||
for (AABB box : shape.toAabbs()) {
|
||||
if (localProbe.intersects(box.move(pos.getX(), pos.getY(), pos.getZ()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,42 +2,64 @@ package dev.nightly.sablegravity.physics;
|
||||
|
||||
import dev.nightly.sablegravity.SableGravityMod;
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
import dev.nightly.sablegravity.mixin.LivingEntityInputAccessor;
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3dc;
|
||||
import dev.ryanhcode.sable.mixinterface.EntityExtension;
|
||||
import dev.ryanhcode.sable.mixinterface.entity.entity_sublevel_collision.EntityMovementExtension;
|
||||
import dev.ryanhcode.sable.mixinterface.entity.entity_sublevel_collision.LivingEntityMovementExtension;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.entity_collision.SubLevelEntityCollision;
|
||||
import net.minecraft.world.entity.MoverType;
|
||||
import net.minecraft.world.effect.MobEffects;
|
||||
import net.minecraft.world.entity.player.Abilities;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/**
|
||||
* Player movement in a SubLevel-local basis while the real entity remains in parent-world space.
|
||||
* Sable remains the sole owner of OBB collision and inherited ship motion.
|
||||
* Sable owns the OBB sweep; this class owns the custom-orientation foot anchor and frame carry.
|
||||
*/
|
||||
public final class SableLocalPlayerPhysics {
|
||||
private static final double EPSILON = 1.0E-7D;
|
||||
private static final double COLLISION_EPSILON = 1.0E-5D;
|
||||
private static final double BLOCKED_MOTION_RATIO = 0.20D;
|
||||
private static final double POSITION_CONSTRAINED_MOTION_RATIO = 0.80D;
|
||||
private static final double VANILLA_GRAVITY_PER_TICK = 0.08D;
|
||||
private static final double AIR_DRAG = 0.98D;
|
||||
private static final double AIR_HORIZONTAL_FRICTION = 0.91D;
|
||||
/**
|
||||
* Normal-block horizontal damping used by vanilla: slipperiness 0.6 * movement damping 0.91.
|
||||
*
|
||||
* Do not feed arbitrary modded Block#getFriction() values directly into the vanilla
|
||||
* acceleration compensation formula. Values below 0.4 create enormous input impulses and
|
||||
* values above 1 / 0.91 make damping exceed one, which becomes exponential acceleration.
|
||||
* Block-specific movement needs the complete vanilla speed-factor/slipperiness hook chain;
|
||||
* until that chain is reproduced, the stable normal-block pair is the only safe contract.
|
||||
*/
|
||||
private static final double DEFAULT_GROUND_FRICTION = 0.6D * 0.91D;
|
||||
private static final double FLIGHT_VERTICAL_DRAG = 0.6D;
|
||||
private static final double MAX_CAPTURED_FLIGHT_INPUT = 0.35D;
|
||||
private static final double GROUND_STATIC_FRICTION_LIMIT = 0.03D;
|
||||
private static final float AIR_ACCELERATION = 0.02F;
|
||||
|
||||
private static final Map<Player, SubLevel> ATTACHED_FRAMES =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
Collections.synchronizedMap(new IdentityHashMap<>());
|
||||
private static final Map<Player, Boolean> LOCAL_GROUNDED =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
Collections.synchronizedMap(new IdentityHashMap<>());
|
||||
private static final Map<Player, Vec3> LAST_POST_TICK_VELOCITY =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
Collections.synchronizedMap(new IdentityHashMap<>());
|
||||
private static final Map<Player, Vec3> LAST_FRAME_MOTION =
|
||||
Collections.synchronizedMap(new IdentityHashMap<>());
|
||||
private static final Map<Player, Vec3> LAST_SABLE_INHERITED_MOTION =
|
||||
Collections.synchronizedMap(new IdentityHashMap<>());
|
||||
private static final Map<Player, Boolean> WAS_FLYING =
|
||||
Collections.synchronizedMap(new IdentityHashMap<>());
|
||||
|
||||
private SableLocalPlayerPhysics() {}
|
||||
|
||||
@@ -49,55 +71,155 @@ public final class SableLocalPlayerPhysics {
|
||||
return null;
|
||||
}
|
||||
|
||||
SubLevel latched = ATTACHED_FRAMES.get(player);
|
||||
if (latched != null) {
|
||||
if (latched.isRemoved() || latched.getLevel() != player.level()) {
|
||||
detach(player);
|
||||
latched = null;
|
||||
} else if (state.sablegravity$getFrameId() != null
|
||||
&& state.sablegravity$getFrameId().equals(latched.getUniqueId())) {
|
||||
((EntityMovementExtension) player).sable$setTrackingSubLevel(latched);
|
||||
return latched;
|
||||
} else {
|
||||
ATTACHED_FRAMES.remove(player);
|
||||
latched = null;
|
||||
// Read Sable's result before restoring our latch. Sable changes this field when the player
|
||||
// lands on a different ship. The previous implementation immediately wrote the old frame
|
||||
// back and could therefore keep collision, gravity and Shift attached to the wrong ship.
|
||||
SubLevel tracked = validFrame(player, Sable.HELPER.getTrackingSubLevel(player));
|
||||
SubLevel rawLatched = ATTACHED_FRAMES.get(player);
|
||||
SubLevel latched = validFrame(player, rawLatched);
|
||||
|
||||
if (latched == null && ATTACHED_FRAMES.containsKey(player)) {
|
||||
if (rawLatched != null
|
||||
&& rawLatched.getLevel() != player.level()
|
||||
&& state.sablegravity$isLocalStateValid()) {
|
||||
GravityFrames.markDimensionHandoff(player);
|
||||
}
|
||||
detach(player);
|
||||
}
|
||||
|
||||
SubLevel tracked = Sable.HELPER.getTrackingSubLevel(player);
|
||||
if (tracked == null || tracked.isRemoved() || tracked.getLevel() != player.level()) {
|
||||
if (tracked != null && !sameFrame(tracked, latched)) {
|
||||
return attach(player, state, tracked);
|
||||
}
|
||||
|
||||
if (latched != null) {
|
||||
if (state.sablegravity$getFrameId() == null
|
||||
|| !state.sablegravity$getFrameId().equals(latched.getUniqueId())) {
|
||||
state.sablegravity$setFrameId(latched.getUniqueId());
|
||||
state.sablegravity$setFrameProvider(SableSubLevelGravityFrame.PROVIDER_ID);
|
||||
state.sablegravity$setFrameDimension(player.level().dimension().location());
|
||||
state.sablegravity$setLocalStateValid(false);
|
||||
}
|
||||
((EntityMovementExtension) player).sable$setTrackingSubLevel(latched);
|
||||
return latched;
|
||||
}
|
||||
|
||||
if (tracked == null) {
|
||||
return null;
|
||||
}
|
||||
return attach(player, state, tracked);
|
||||
}
|
||||
|
||||
ATTACHED_FRAMES.put(player, tracked);
|
||||
LOCAL_GROUNDED.put(player, player.onGround());
|
||||
state.sablegravity$setFrameId(tracked.getUniqueId());
|
||||
state.sablegravity$setLocalStateValid(false);
|
||||
state.sablegravity$setLocalPosition(tracked.logicalPose().transformPositionInverse(player.position()));
|
||||
((EntityMovementExtension) player).sable$setTrackingSubLevel(tracked);
|
||||
return tracked;
|
||||
|
||||
private static boolean sameFrame(SubLevel first, SubLevel second) {
|
||||
return first != null
|
||||
&& second != null
|
||||
&& first.getUniqueId().equals(second.getUniqueId());
|
||||
}
|
||||
|
||||
private static SubLevel validFrame(Player player, SubLevel frame) {
|
||||
if (frame == null || frame.isRemoved() || frame.getLevel() != player.level()) {
|
||||
return null;
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
private static SubLevel attach(
|
||||
Player player,
|
||||
SableGravityState state,
|
||||
SubLevel frame
|
||||
) {
|
||||
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.
|
||||
Vec3 physicalFeet;
|
||||
if (previousFrame != null) {
|
||||
physicalFeet = SableOrientedBounds.worldFeet(
|
||||
player,
|
||||
SableFrameOrientation.getFrameOrientation(previousFrame, 1.0F)
|
||||
);
|
||||
} else {
|
||||
physicalFeet = player.position();
|
||||
}
|
||||
|
||||
Vec3 rebasedRawPosition = SableOrientedBounds.rawPositionForFeet(
|
||||
player,
|
||||
physicalFeet,
|
||||
SableFrameOrientation.getFrameOrientation(frame, 1.0F)
|
||||
);
|
||||
if (rebasedRawPosition.distanceToSqr(player.position()) > EPSILON) {
|
||||
player.setPos(rebasedRawPosition);
|
||||
}
|
||||
|
||||
ATTACHED_FRAMES.put(player, frame);
|
||||
LAST_FRAME_MOTION.put(player, Vec3.ZERO);
|
||||
LAST_SABLE_INHERITED_MOTION.remove(player);
|
||||
LAST_POST_TICK_VELOCITY.remove(player);
|
||||
WAS_FLYING.put(player, player.getAbilities().flying);
|
||||
((LivingEntityMovementExtension) player).sable$getInheritedVelocity().zero();
|
||||
|
||||
Vec3 localFeet = frame.logicalPose().transformPositionInverse(physicalFeet);
|
||||
boolean supported = SableLocalCollisionProbe.hasSupportNow(player, frame);
|
||||
Vec3 handoffLocalVelocity = state.sablegravity$getLocalVelocity();
|
||||
boolean consumedHandoff = GravityFrames.consumeDimensionHandoff(
|
||||
player,
|
||||
new SableSubLevelGravityFrame(frame)
|
||||
);
|
||||
Vec3 initialLocalVelocity = consumedHandoff
|
||||
? handoffLocalVelocity
|
||||
: supported
|
||||
? Vec3.ZERO
|
||||
: frame.logicalPose().transformNormalInverse(player.getDeltaMovement());
|
||||
|
||||
state.sablegravity$setFrameId(frame.getUniqueId());
|
||||
state.sablegravity$setFrameProvider(SableSubLevelGravityFrame.PROVIDER_ID);
|
||||
state.sablegravity$setFrameDimension(player.level().dimension().location());
|
||||
state.sablegravity$setLocalPosition(localFeet);
|
||||
state.sablegravity$setLocalVelocity(initialLocalVelocity);
|
||||
state.sablegravity$setLocalStateValid(true);
|
||||
player.setDeltaMovement(frame.logicalPose().transformNormal(initialLocalVelocity));
|
||||
LOCAL_GROUNDED.put(player, supported);
|
||||
((EntityMovementExtension) player).sable$setTrackingSubLevel(frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static SubLevel getAttachedFrame(Player player) {
|
||||
return resolveFrame(player);
|
||||
}
|
||||
|
||||
/** Returns the already latched frame without querying or mutating Sable tracking state. */
|
||||
public static SubLevel getAttachedFrameIfPresent(Player player) {
|
||||
return ATTACHED_FRAMES.get(player);
|
||||
}
|
||||
|
||||
public static boolean isLocalGrounded(Player player) {
|
||||
return Boolean.TRUE.equals(LOCAL_GROUNDED.get(player));
|
||||
}
|
||||
|
||||
/** Called at LocalPlayer.aiStep HEAD so vanilla cannot cancel flight from a stale ground flag. */
|
||||
public static Vec3 getLastFrameMotion(Player player) {
|
||||
return LAST_FRAME_MOTION.getOrDefault(player, Vec3.ZERO);
|
||||
}
|
||||
|
||||
public static Vec3 getLastRejectedSableInheritedMotion(Player player) {
|
||||
return LAST_SABLE_INHERITED_MOTION.get(player);
|
||||
}
|
||||
|
||||
/** Restores the previous local support flag before vanilla processes jump/flight toggles. */
|
||||
public static void beforeLocalPlayerTick(Player player) {
|
||||
if (!player.getAbilities().flying) {
|
||||
return;
|
||||
}
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
if (!state.sablegravity$isEnabled()) {
|
||||
return;
|
||||
}
|
||||
SubLevel frame = getAttachedFrame(player);
|
||||
if (frame != null && !frame.isRemoved()) {
|
||||
player.setOnGround(false);
|
||||
// LocalPlayer consumes onGround before travel for both normal jumping and creative
|
||||
// flight toggles. Feed it the frame-local result from the previous sweep. In
|
||||
// particular, do not force this false while flying: vanilla intentionally ends
|
||||
// creative flight when the player touches the local floor.
|
||||
player.setOnGround(isLocalGrounded(player));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,20 +230,38 @@ public final class SableLocalPlayerPhysics {
|
||||
LAST_POST_TICK_VELOCITY.remove(player);
|
||||
return;
|
||||
}
|
||||
if (player.getAbilities().flying) {
|
||||
player.setOnGround(false);
|
||||
}
|
||||
LAST_POST_TICK_VELOCITY.put(player, player.getDeltaMovement());
|
||||
}
|
||||
|
||||
public static void detach(Player player) {
|
||||
SubLevel attached = ATTACHED_FRAMES.get(player);
|
||||
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.
|
||||
Vec3 physicalFeet = SableOrientedBounds.worldFeet(
|
||||
player,
|
||||
SableFrameOrientation.getFrameOrientation(attached, 1.0F)
|
||||
);
|
||||
if (isFinite(physicalFeet)) {
|
||||
player.setPos(physicalFeet);
|
||||
}
|
||||
}
|
||||
|
||||
ATTACHED_FRAMES.remove(player);
|
||||
LOCAL_GROUNDED.remove(player);
|
||||
LAST_POST_TICK_VELOCITY.remove(player);
|
||||
LAST_FRAME_MOTION.remove(player);
|
||||
LAST_SABLE_INHERITED_MOTION.remove(player);
|
||||
WAS_FLYING.remove(player);
|
||||
((LivingEntityMovementExtension) player).sable$getInheritedVelocity().zero();
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
state.sablegravity$setLocalStateValid(false);
|
||||
state.sablegravity$setLocalPosition(Vec3.ZERO);
|
||||
state.sablegravity$setFrameId(null);
|
||||
state.sablegravity$setFrameProvider(null);
|
||||
state.sablegravity$setFrameDimension(null);
|
||||
}
|
||||
|
||||
public static boolean simulate(Player player, Vec3 input, SubLevel frame) {
|
||||
@@ -130,7 +270,11 @@ public final class SableLocalPlayerPhysics {
|
||||
EntityMovementExtension movement = (EntityMovementExtension) player;
|
||||
Abilities abilities = player.getAbilities();
|
||||
boolean flying = abilities.flying;
|
||||
boolean wasFlying = Boolean.TRUE.equals(WAS_FLYING.get(player));
|
||||
boolean groundedBeforeMove = isLocalGrounded(player);
|
||||
double flightControlImpulse = flying
|
||||
? getFlightControlImpulse(player, abilities)
|
||||
: 0.0D;
|
||||
|
||||
Vec3 localVelocity;
|
||||
if (!state.sablegravity$isLocalStateValid()
|
||||
@@ -140,47 +284,76 @@ public final class SableLocalPlayerPhysics {
|
||||
state.sablegravity$setLocalVelocity(localVelocity);
|
||||
state.sablegravity$setLocalStateValid(true);
|
||||
state.sablegravity$setFrameId(frame.getUniqueId());
|
||||
state.sablegravity$setLocalPosition(pose.transformPositionInverse(player.position()));
|
||||
state.sablegravity$setFrameProvider(SableSubLevelGravityFrame.PROVIDER_ID);
|
||||
state.sablegravity$setFrameDimension(player.level().dimension().location());
|
||||
state.sablegravity$setLocalPosition(
|
||||
SableLocalCollisionProbe.getLocalFeet(player, frame)
|
||||
);
|
||||
} else {
|
||||
localVelocity = state.sablegravity$getLocalVelocity();
|
||||
|
||||
if (flying) {
|
||||
// LocalPlayer adds creative ascend/descend to deltaMovement before travel.
|
||||
// Compare against the exact final velocity captured at the previous aiStep TAIL,
|
||||
// not against a velocity rotated by the current ship pose. This prevents ship
|
||||
// rotation from being mistaken for flight input and creating runaway acceleration.
|
||||
Vec3 previousFinalWorld = LAST_POST_TICK_VELOCITY.get(player);
|
||||
if (previousFinalWorld != null) {
|
||||
Vec3 preTravelChangeWorld = player.getDeltaMovement().subtract(previousFinalWorld);
|
||||
if (preTravelChangeWorld.lengthSqr() > EPSILON) {
|
||||
Vec3 preTravelChangeLocal = pose.transformNormalInverse(preTravelChangeWorld);
|
||||
double verticalInput = clamp(
|
||||
preTravelChangeLocal.y,
|
||||
-MAX_CAPTURED_FLIGHT_INPUT,
|
||||
MAX_CAPTURED_FLIGHT_INPUT
|
||||
);
|
||||
if (Math.abs(verticalInput) > EPSILON) {
|
||||
localVelocity = localVelocity.add(0.0D, verticalInput, 0.0D);
|
||||
}
|
||||
}
|
||||
// Import external world-space changes only for ordinary movement. During creative
|
||||
// flight LocalPlayer and Sable both rewrite deltaMovement as bookkeeping, and
|
||||
// hasImpulse also remains set across jump/pose transitions. Treating those writes as
|
||||
// knockback creates a false local X/Z acceleration. Flight controls and the initial
|
||||
// jump are already written directly to local state by their dedicated paths.
|
||||
Vec3 previousFinalWorld = LAST_POST_TICK_VELOCITY.get(player);
|
||||
if (!flying && !wasFlying && previousFinalWorld != null) {
|
||||
Vec3 externalWorldChange = player.getDeltaMovement().subtract(previousFinalWorld);
|
||||
if (externalWorldChange.lengthSqr() > EPSILON) {
|
||||
Vec3 externalLocalChange = pose.transformNormalInverse(externalWorldChange);
|
||||
localVelocity = localVelocity.add(externalLocalChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (flying && Math.abs(flightControlImpulse) > EPSILON) {
|
||||
localVelocity = localVelocity.add(0.0D, flightControlImpulse, 0.0D);
|
||||
}
|
||||
|
||||
Vec3 frameAnchorLocal = state.sablegravity$getLocalPosition();
|
||||
Vec3 frameMotion = computeFrameCorrection(player, frame, frameAnchorLocal);
|
||||
|
||||
// Sable's inheritedVelocity is a separate release-momentum channel. Its built-in custom
|
||||
// orientation feet calculation assumes a different raw-position convention and produced
|
||||
// the 2.323654-block false impulse from the supplied log. Keep it out of this sweep; the
|
||||
// corrected frame carry is reconstructed immediately below.
|
||||
((LivingEntityMovementExtension) player).sable$getInheritedVelocity().zero();
|
||||
|
||||
// Reconstruct the frame anchor absolutely before applying this tick's local movement.
|
||||
// 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.
|
||||
Vec3 actualFrameMotion = applyCorrectedFrameMotion(player, frameMotion);
|
||||
LAST_FRAME_MOTION.put(player, actualFrameMotion);
|
||||
Vector3d inheritedVelocity =
|
||||
((LivingEntityMovementExtension) player).sable$getInheritedVelocity();
|
||||
inheritedVelocity.set(actualFrameMotion.x, actualFrameMotion.y, actualFrameMotion.z);
|
||||
|
||||
float acceleration;
|
||||
if (flying) {
|
||||
acceleration = abilities.getFlyingSpeed() * (player.isSprinting() ? 2.0F : 1.0F);
|
||||
} else {
|
||||
// On a normal 0.6-slipperiness block vanilla's 0.216 / friction^3 compensation is
|
||||
// effectively 1.0, so getSpeed() is the correct ground impulse. test22 queried a raw
|
||||
// modded friction value and reused it without vanilla's speed-factor hooks. A value
|
||||
// below 0.4 produced a huge impulse; a value above 1/0.91 produced damping > 1 and
|
||||
// therefore exponential acceleration. Keep the normal-block contract here.
|
||||
acceleration = groundedBeforeMove ? player.getSpeed() : AIR_ACCELERATION;
|
||||
}
|
||||
|
||||
// Creative vertical input has already been added to deltaMovement by LocalPlayer.aiStep.
|
||||
// Only horizontal input is handled here, otherwise ascend/descend would be applied twice.
|
||||
// Creative vertical input was removed from vanilla's world Y and re-applied to local Y
|
||||
// above. Only horizontal travel input belongs in the ordinary acceleration path.
|
||||
Vec3 travelInput = flying ? new Vec3(input.x, 0.0D, input.z) : input;
|
||||
localVelocity = localVelocity.add(getInputVector(travelInput, acceleration, player.getYRot()));
|
||||
Vec3 requestedLocalVelocity = localVelocity;
|
||||
|
||||
movement.sable$setTrackingSubLevel(frame);
|
||||
// Sable's arbitrary-orientation step-up checks Entity.onGround() inside its OBB sweep.
|
||||
// Feed it the previous local ground state instead of the stale world-axis vanilla flag.
|
||||
player.setOnGround(!flying && groundedBeforeMove);
|
||||
|
||||
Vec3 requestedWorldMotion = pose.transformNormal(requestedLocalVelocity);
|
||||
Vec3 positionBeforeMove = player.position();
|
||||
player.setDeltaMovement(requestedWorldMotion);
|
||||
@@ -188,20 +361,57 @@ public final class SableLocalPlayerPhysics {
|
||||
|
||||
SubLevelEntityCollision.CollisionInfo collision = movement.sable$getCollisionInfo();
|
||||
Vec3 actualWorldMotion = player.position().subtract(positionBeforeMove);
|
||||
Vec3 actualLocalMotion = pose.transformNormalInverse(actualWorldMotion);
|
||||
|
||||
// Do not let Sable's LivingEntity.travel RETURN hook apply its custom-orientation
|
||||
// inheritedMotion after our cancellable Player.travel injection returns. The value is
|
||||
// based on SableCompanion#getFeetPos, whose custom-orientation branch treats raw position
|
||||
// as an eye anchor; this mod keeps vanilla's raw-position field and rebases it explicitly.
|
||||
Vec3 rejectedSableInherited = collision == null ? null : collision.inheritedMotion;
|
||||
if (rejectedSableInherited == null) {
|
||||
LAST_SABLE_INHERITED_MOTION.remove(player);
|
||||
} else {
|
||||
LAST_SABLE_INHERITED_MOTION.put(player, rejectedSableInherited);
|
||||
collision.inheritedMotion = null;
|
||||
}
|
||||
|
||||
Vec3 actualLocalMotion;
|
||||
if (collision != null && collision.motion != null && collision.subLevelHorizontalCollision) {
|
||||
// Sable's returned motion already contains every OBB MTV but excludes inherited ship
|
||||
// movement. That makes it the stable source for wall/step displacement on a moving
|
||||
// frame; comparing feet before Sable applies inheritedMotion creates a fake impulse.
|
||||
actualLocalMotion = pose.transformNormalInverse(collision.motion);
|
||||
} else {
|
||||
// Parent-world vanilla collision is applied after collision.motion is stored, so use
|
||||
// the real displacement when no blocking SubLevel wall participated.
|
||||
actualLocalMotion = pose.transformNormalInverse(actualWorldMotion);
|
||||
}
|
||||
|
||||
// Never import Sable's collision-mutated deltaMovement back into local state. At a tilted
|
||||
// floor that mutation contains tiny tangent components and caused the persistent drift.
|
||||
// Preserve the requested local velocity and only remove axes that were actually blocked.
|
||||
localVelocity = resolveBlockedVelocity(requestedLocalVelocity, actualLocalMotion, collision);
|
||||
localVelocity = resolveBlockedVelocity(
|
||||
requestedLocalVelocity,
|
||||
actualLocalMotion,
|
||||
collision,
|
||||
pose
|
||||
);
|
||||
|
||||
boolean localGrounded = collision != null && collision.verticalCollisionBelow;
|
||||
LOCAL_GROUNDED.put(player, !flying && localGrounded);
|
||||
boolean localGrounded = collision != null
|
||||
&& collision.verticalCollisionBelow
|
||||
&& requestedLocalVelocity.y <= COLLISION_EPSILON;
|
||||
if (!localGrounded && requestedLocalVelocity.y <= COLLISION_EPSILON) {
|
||||
// Keep the ground latch stable when SAT reports no fresh penetration on an exact rest.
|
||||
localGrounded = SableLocalCollisionProbe.hasSupportNow(player, frame);
|
||||
}
|
||||
LOCAL_GROUNDED.put(player, localGrounded);
|
||||
|
||||
// inheritedMotion/inheritedVelocity are intentionally left to Sable's travel RETURN mixin.
|
||||
// Local velocity is independent from frame carry. The corrected carry above is stored in
|
||||
// Sable's inheritedVelocity only as release momentum and is cleared before the next sweep.
|
||||
if (flying) {
|
||||
player.fallDistance = 0.0F;
|
||||
player.setOnGround(false);
|
||||
// Preserve local contact semantics so LocalPlayer.aiStep can turn flight off on
|
||||
// landing, exactly like vanilla does on an ordinary world-axis floor.
|
||||
player.setOnGround(localGrounded);
|
||||
localVelocity = new Vec3(
|
||||
localVelocity.x * AIR_HORIZONTAL_FRICTION,
|
||||
localVelocity.y * FLIGHT_VERTICAL_DRAG,
|
||||
@@ -210,8 +420,12 @@ public final class SableLocalPlayerPhysics {
|
||||
} else {
|
||||
player.setOnGround(localGrounded);
|
||||
if (!player.isNoGravity()) {
|
||||
double gravity = VANILLA_GRAVITY_PER_TICK
|
||||
* (Math.max(0.0F, state.sablegravity$getStrength()) / SableGravityMod.VANILLA_STRENGTH);
|
||||
double gravity = Math.max(0.0D, player.getGravity())
|
||||
* (Math.max(0.0F, state.sablegravity$getStrength())
|
||||
/ SableGravityMod.VANILLA_STRENGTH);
|
||||
if (localVelocity.y <= 0.0D && player.hasEffect(MobEffects.SLOW_FALLING)) {
|
||||
gravity = Math.min(gravity, 0.01D);
|
||||
}
|
||||
localVelocity = localVelocity.add(0.0D, -gravity, 0.0D);
|
||||
}
|
||||
|
||||
@@ -236,47 +450,275 @@ public final class SableLocalPlayerPhysics {
|
||||
}
|
||||
|
||||
updateLocalWalkAnimation(player, actualLocalMotion);
|
||||
state.sablegravity$setLocalPosition(pose.transformPositionInverse(player.position()));
|
||||
Vec3 measuredLocalFeet = SableLocalCollisionProbe.getLocalFeet(player, frame);
|
||||
if (collision != null && collision.horizontalCollision) {
|
||||
boolean constrainX = axisWasPositionConstrained(
|
||||
requestedLocalVelocity.x,
|
||||
actualLocalMotion.x
|
||||
);
|
||||
boolean constrainZ = axisWasPositionConstrained(
|
||||
requestedLocalVelocity.z,
|
||||
actualLocalMotion.z
|
||||
);
|
||||
if (constrainX || constrainZ) {
|
||||
// Sable occasionally returns a small inward displacement at a rotated block seam
|
||||
// even though it marks the axis as horizontally constrained. Keep constrained
|
||||
// plot axes on their pre-sweep anchor and preserve the other axis verbatim. This
|
||||
// is deterministic wall sliding and cannot turn a floor MTV into sideways drift.
|
||||
measuredLocalFeet = new Vec3(
|
||||
constrainX ? frameAnchorLocal.x : measuredLocalFeet.x,
|
||||
measuredLocalFeet.y,
|
||||
constrainZ ? frameAnchorLocal.z : measuredLocalFeet.z
|
||||
);
|
||||
localVelocity = new Vec3(
|
||||
constrainX ? 0.0D : localVelocity.x,
|
||||
localVelocity.y,
|
||||
constrainZ ? 0.0D : localVelocity.z
|
||||
);
|
||||
|
||||
Vec3 correctedWorldFeet = pose.transformPosition(measuredLocalFeet);
|
||||
Vec3 correctedRaw = SableOrientedBounds.rawPositionForFeet(
|
||||
player,
|
||||
correctedWorldFeet,
|
||||
SableFrameOrientation.getFrameOrientation(frame, 1.0F)
|
||||
);
|
||||
if (isFinite(correctedRaw)) {
|
||||
player.setPos(correctedRaw);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldLockRestingTangents(
|
||||
input,
|
||||
requestedLocalVelocity,
|
||||
collision,
|
||||
SableParentWorldCollision.getLastResult(player)
|
||||
)) {
|
||||
// SAT may return a floor MTV with a microscopic tangent component when the frame is
|
||||
// tilted or interpolated. Velocity clipping alone cannot fix that: the displacement
|
||||
// has already happened, so its residue accumulated into a visible downhill slide
|
||||
// while local X/Z velocity remained exactly zero. For a pure resting floor contact,
|
||||
// the prior local feet X/Z are authoritative; retain only the measured vertical fit.
|
||||
measuredLocalFeet = new Vec3(
|
||||
frameAnchorLocal.x,
|
||||
measuredLocalFeet.y,
|
||||
frameAnchorLocal.z
|
||||
);
|
||||
Vec3 correctedWorldFeet = pose.transformPosition(measuredLocalFeet);
|
||||
Vec3 correctedRaw = SableOrientedBounds.rawPositionForFeet(
|
||||
player,
|
||||
correctedWorldFeet,
|
||||
SableFrameOrientation.getFrameOrientation(frame, 1.0F)
|
||||
);
|
||||
if (isFinite(correctedRaw)) {
|
||||
player.setPos(correctedRaw);
|
||||
}
|
||||
}
|
||||
state.sablegravity$setLocalPosition(measuredLocalFeet);
|
||||
state.sablegravity$setLocalVelocity(localVelocity);
|
||||
player.setDeltaMovement(pose.transformNormal(localVelocity));
|
||||
|
||||
// Entity.move may have selected another SubLevel after a floor contact. Do not overwrite
|
||||
// that decision with the old latch on the next tick. Mark local velocity invalid so it is
|
||||
// cleanly re-expressed in the new frame from the already-correct world velocity.
|
||||
if (collision != null) {
|
||||
SubLevel collisionFrame = validFrame(player, collision.trackingSubLevel);
|
||||
if (collisionFrame != null && !sameFrame(collisionFrame, frame)) {
|
||||
attach(player, state, collisionFrame);
|
||||
}
|
||||
}
|
||||
WAS_FLYING.put(player, flying);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean shouldLockRestingTangents(
|
||||
Vec3 input,
|
||||
Vec3 requestedLocalVelocity,
|
||||
SubLevelEntityCollision.CollisionInfo collision,
|
||||
SableParentWorldCollision.CollisionResult parentCollision
|
||||
) {
|
||||
if (collision == null
|
||||
|| !collision.verticalCollisionBelow
|
||||
|| collision.subLevelHorizontalCollision
|
||||
|| parentCollision.horizontalCollision()) {
|
||||
return false;
|
||||
}
|
||||
double inputHorizontalSqr = input.x * input.x + input.z * input.z;
|
||||
double velocityHorizontalSqr = requestedLocalVelocity.x * requestedLocalVelocity.x
|
||||
+ requestedLocalVelocity.z * requestedLocalVelocity.z;
|
||||
return inputHorizontalSqr <= EPSILON && velocityHorizontalSqr <= EPSILON;
|
||||
}
|
||||
|
||||
private static Vec3 computeFrameCorrection(
|
||||
Player player,
|
||||
SubLevel frame,
|
||||
Vec3 localPhysicalFeet
|
||||
) {
|
||||
if (localPhysicalFeet == null) {
|
||||
return Vec3.ZERO;
|
||||
}
|
||||
|
||||
var orientation = SableFrameOrientation.getFrameOrientation(frame, 1.0F);
|
||||
Vec3 physicalFeet = frame.logicalPose().transformPosition(localPhysicalFeet);
|
||||
Vec3 targetRaw = SableOrientedBounds.rawPositionForFeet(
|
||||
player,
|
||||
physicalFeet,
|
||||
orientation
|
||||
);
|
||||
|
||||
Vec3 motion = targetRaw.subtract(player.position());
|
||||
if (!isFinite(motion)) {
|
||||
return Vec3.ZERO;
|
||||
}
|
||||
return motion;
|
||||
}
|
||||
|
||||
private static Vec3 applyCorrectedFrameMotion(Player player, Vec3 frameMotion) {
|
||||
if (frameMotion == null || frameMotion.lengthSqr() <= EPSILON) {
|
||||
return Vec3.ZERO;
|
||||
}
|
||||
Vec3 collidedMotion = ((EntityExtension) player).sable$vanillaCollide(frameMotion);
|
||||
if (!isFinite(collidedMotion)) {
|
||||
return Vec3.ZERO;
|
||||
}
|
||||
player.setPos(player.position().add(collidedMotion));
|
||||
return collidedMotion;
|
||||
}
|
||||
|
||||
private static boolean isFinite(Vec3 value) {
|
||||
return Double.isFinite(value.x)
|
||||
&& Double.isFinite(value.y)
|
||||
&& Double.isFinite(value.z);
|
||||
}
|
||||
|
||||
private static Vec3 resolveBlockedVelocity(
|
||||
Vec3 requested,
|
||||
Vec3 actualMotion,
|
||||
SubLevelEntityCollision.CollisionInfo collision
|
||||
SubLevelEntityCollision.CollisionInfo collision,
|
||||
Pose3dc pose
|
||||
) {
|
||||
double x = requested.x;
|
||||
double y = requested.y;
|
||||
double z = requested.z;
|
||||
Vec3 resolved = requested;
|
||||
boolean handledHorizontalContact = false;
|
||||
boolean handledVerticalContact = false;
|
||||
|
||||
if (collision != null) {
|
||||
if (collision.verticalCollisionBelow && y < 0.0D) {
|
||||
y = 0.0D;
|
||||
} else if (collision.verticalCollision && y > 0.0D) {
|
||||
y = 0.0D;
|
||||
if (collision != null && collision.firstCollisions != null) {
|
||||
for (Object value : collision.firstCollisions.values()) {
|
||||
if (!(value instanceof SubLevelEntityCollision.FirstCollisionInfo info)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sable records a wall before trying step-up. If the step succeeded, that first
|
||||
// wall is stale and must not consume any velocity.
|
||||
if (info.horizontal() && !collision.subLevelHorizontalCollision) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3dc worldNormalJoml = info.globalDirection();
|
||||
Vec3 worldNormal = new Vec3(
|
||||
worldNormalJoml.x(),
|
||||
worldNormalJoml.y(),
|
||||
worldNormalJoml.z()
|
||||
);
|
||||
Vec3 localNormal = pose.transformNormalInverse(worldNormal);
|
||||
double normalLengthSqr = localNormal.lengthSqr();
|
||||
if (normalLengthSqr <= EPSILON) {
|
||||
continue;
|
||||
}
|
||||
localNormal = localNormal.scale(1.0D / Math.sqrt(normalLengthSqr));
|
||||
|
||||
if (!info.horizontal()) {
|
||||
// A floor/ceiling MTV is never allowed to shave horizontal deck velocity.
|
||||
// On a rotating frame its sampled normal is not perfectly equal to final local
|
||||
// +/-Y; full vector projection therefore removed a little X/Z every tick and
|
||||
// made walking/sprinting speed collapse or pulse. Use the semantic contact
|
||||
// type and clip only local vertical velocity.
|
||||
if (localNormal.y > 0.0D && resolved.y < 0.0D) {
|
||||
resolved = new Vec3(resolved.x, 0.0D, resolved.z);
|
||||
} else if (localNormal.y < 0.0D && resolved.y > 0.0D) {
|
||||
resolved = new Vec3(resolved.x, 0.0D, resolved.z);
|
||||
}
|
||||
handledVerticalContact = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wall response lives in the local floor plane. Drop numerical vertical residue
|
||||
// from the SAT normal so a wall cannot eat jump/fall speed, then remove only the
|
||||
// inward horizontal component and preserve the tangent exactly.
|
||||
double horizontalNormalSqr =
|
||||
localNormal.x * localNormal.x + localNormal.z * localNormal.z;
|
||||
if (horizontalNormalSqr <= EPSILON) {
|
||||
continue;
|
||||
}
|
||||
double invHorizontalLength = 1.0D / Math.sqrt(horizontalNormalSqr);
|
||||
double nx = localNormal.x * invHorizontalLength;
|
||||
double nz = localNormal.z * invHorizontalLength;
|
||||
double inwardSpeed = resolved.x * nx + resolved.z * nz;
|
||||
if (inwardSpeed < -COLLISION_EPSILON) {
|
||||
resolved = new Vec3(
|
||||
resolved.x - nx * inwardSpeed,
|
||||
resolved.y,
|
||||
resolved.z - nz * inwardSpeed
|
||||
);
|
||||
}
|
||||
handledHorizontalContact = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (axisWasBlocked(requested.x, actualMotion.x)) {
|
||||
x = 0.0D;
|
||||
}
|
||||
if (axisWasBlocked(requested.z, actualMotion.z)) {
|
||||
z = 0.0D;
|
||||
double x = resolved.x;
|
||||
double y = resolved.y;
|
||||
double z = resolved.z;
|
||||
|
||||
if (collision != null) {
|
||||
if (!handledVerticalContact) {
|
||||
if (collision.verticalCollisionBelow && requested.y < 0.0D) {
|
||||
y = 0.0D;
|
||||
} else if (collision.verticalCollision
|
||||
&& !collision.verticalCollisionBelow
|
||||
&& requested.y > 0.0D) {
|
||||
y = 0.0D;
|
||||
}
|
||||
}
|
||||
|
||||
if (collision.horizontalCollision && !handledHorizontalContact) {
|
||||
if (axisWasBlocked(requested.x, actualMotion.x)) {
|
||||
x = 0.0D;
|
||||
}
|
||||
if (axisWasBlocked(requested.z, actualMotion.z)) {
|
||||
z = 0.0D;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Vec3(x, y, z);
|
||||
}
|
||||
|
||||
private static boolean axisWasBlocked(double requested, double actual) {
|
||||
if (Math.abs(requested) <= COLLISION_EPSILON) {
|
||||
double requestedAbs = Math.abs(requested);
|
||||
if (requestedAbs <= COLLISION_EPSILON) {
|
||||
return false;
|
||||
}
|
||||
if (Math.signum(requested) != Math.signum(actual) && Math.abs(actual) > COLLISION_EPSILON) {
|
||||
if (Math.signum(requested) != Math.signum(actual)
|
||||
&& Math.abs(actual) > COLLISION_EPSILON) {
|
||||
return true;
|
||||
}
|
||||
return Math.abs(actual) + COLLISION_EPSILON < Math.abs(requested);
|
||||
return Math.abs(actual) <= Math.max(
|
||||
COLLISION_EPSILON,
|
||||
requestedAbs * BLOCKED_MOTION_RATIO
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean axisWasPositionConstrained(double requested, double actual) {
|
||||
double requestedAbs = Math.abs(requested);
|
||||
if (requestedAbs <= COLLISION_EPSILON) {
|
||||
return false;
|
||||
}
|
||||
if (Math.signum(requested) != Math.signum(actual)
|
||||
&& Math.abs(actual) > COLLISION_EPSILON) {
|
||||
return true;
|
||||
}
|
||||
return Math.abs(actual) <= Math.max(
|
||||
COLLISION_EPSILON,
|
||||
requestedAbs * POSITION_CONSTRAINED_MOTION_RATIO
|
||||
);
|
||||
}
|
||||
|
||||
private static void updateLocalWalkAnimation(Player player, Vec3 actualLocalMotion) {
|
||||
@@ -310,7 +752,13 @@ public final class SableLocalPlayerPhysics {
|
||||
);
|
||||
}
|
||||
|
||||
private static double clamp(double value, double min, double max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
private static double getFlightControlImpulse(Player player, Abilities abilities) {
|
||||
int verticalInput = ((LivingEntityInputAccessor) player)
|
||||
.sablegravity$isJumpingInput() ? 1 : 0;
|
||||
if (player.isShiftKeyDown()) {
|
||||
verticalInput--;
|
||||
}
|
||||
return verticalInput * abilities.getFlyingSpeed() * 3.0D;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package dev.nightly.sablegravity.physics;
|
||||
|
||||
import dev.ryanhcode.sable.companion.math.Pose3dc;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
/**
|
||||
* Canonical dimensions and the exact eye-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)}
|
||||
* is only the smallest world-axis AABB containing all eight OBB corners.
|
||||
*/
|
||||
public final class SableOrientedBounds {
|
||||
private SableOrientedBounds() {}
|
||||
|
||||
public static AABB canonicalAabb(Entity entity) {
|
||||
double halfWidth = entity.getBbWidth() * 0.5D;
|
||||
double height = entity.getBbHeight();
|
||||
Vec3 position = entity.position();
|
||||
return new AABB(
|
||||
position.x - halfWidth,
|
||||
position.y,
|
||||
position.z - halfWidth,
|
||||
position.x + halfWidth,
|
||||
position.y + height,
|
||||
position.z + halfWidth
|
||||
);
|
||||
}
|
||||
|
||||
/** Exact world-space OBB center matching Sable's transformEntityBoundsCenter contract. */
|
||||
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()
|
||||
);
|
||||
Vec3 position = entity.position();
|
||||
return new Vec3(
|
||||
position.x - rotatedOffset.x,
|
||||
position.y + entity.getEyeHeight() - rotatedOffset.y,
|
||||
position.z - rotatedOffset.z
|
||||
);
|
||||
}
|
||||
|
||||
/** Physical feet point after rotating the canonical body around the eye anchor. */
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
*/
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
public static Vec3 localFeet(
|
||||
Entity entity,
|
||||
Pose3dc pose,
|
||||
Quaterniondc orientation
|
||||
) {
|
||||
return pose.transformPositionInverse(worldFeet(entity, orientation));
|
||||
}
|
||||
|
||||
public static Vec3[] worldCorners(Entity entity, Quaterniondc orientation) {
|
||||
Vec3 center = worldCenter(entity, orientation);
|
||||
double halfX = entity.getBbWidth() * 0.5D;
|
||||
double halfY = entity.getBbHeight() * 0.5D;
|
||||
double halfZ = entity.getBbWidth() * 0.5D;
|
||||
Vec3[] corners = new Vec3[8];
|
||||
int index = 0;
|
||||
|
||||
for (int xSign : new int[]{-1, 1}) {
|
||||
for (int ySign : new int[]{-1, 1}) {
|
||||
for (int zSign : new int[]{-1, 1}) {
|
||||
Vector3d offset = orientation.transform(
|
||||
new Vector3d(xSign * halfX, ySign * halfY, zSign * halfZ),
|
||||
new Vector3d()
|
||||
);
|
||||
corners[index++] = center.add(offset.x, offset.y, offset.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
return corners;
|
||||
}
|
||||
|
||||
/** Smallest world-axis AABB containing the exact rotated OBB. Not used as Sable's body. */
|
||||
public static AABB worldEnvelope(Entity entity, Quaterniondc orientation) {
|
||||
Vec3[] corners = worldCorners(entity, orientation);
|
||||
double minX = Double.POSITIVE_INFINITY;
|
||||
double minY = Double.POSITIVE_INFINITY;
|
||||
double minZ = Double.POSITIVE_INFINITY;
|
||||
double maxX = Double.NEGATIVE_INFINITY;
|
||||
double maxY = Double.NEGATIVE_INFINITY;
|
||||
double maxZ = Double.NEGATIVE_INFINITY;
|
||||
|
||||
for (Vec3 corner : corners) {
|
||||
minX = Math.min(minX, corner.x);
|
||||
minY = Math.min(minY, corner.y);
|
||||
minZ = Math.min(minZ, corner.z);
|
||||
maxX = Math.max(maxX, corner.x);
|
||||
maxY = Math.max(maxY, corner.y);
|
||||
maxZ = Math.max(maxZ, corner.z);
|
||||
}
|
||||
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
|
||||
public static Quaterniondc logicalOrientation(SubLevel frame) {
|
||||
return SableFrameOrientation.getFrameOrientation(frame, 1.0F);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
package dev.nightly.sablegravity.physics;
|
||||
|
||||
import dev.ryanhcode.sable.api.math.LevelReusedVectors;
|
||||
import dev.ryanhcode.sable.api.math.OrientedBoundingBox3d;
|
||||
import dev.ryanhcode.sable.mixinterface.entity.entity_sublevel_collision.EntityMovementExtension;
|
||||
import dev.ryanhcode.sable.mixinterface.entity.entity_sublevel_collision.LevelExtension;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.entity_collision.SubLevelEntityCollision;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.EntityDimensions;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/**
|
||||
* Replaces vanilla's second upright-AABB collision pass with the same quaternion body that is
|
||||
* used for Sable plot collision. Parent-world blocks remain solid without becoming fake walls
|
||||
* merely because the player's local up direction is tilted.
|
||||
*/
|
||||
public final class SableParentWorldCollision {
|
||||
private static final double EPSILON = 1.0E-7D;
|
||||
private static final double SAT_EPSILON_SQR = 1.0E-14D;
|
||||
private static final double MAX_SUBSTEP = 0.125D;
|
||||
private static final int MAX_SUBSTEPS = 64;
|
||||
private static final int MAX_RESOLUTION_ITERATIONS = 8;
|
||||
|
||||
private static final ThreadLocal<CollisionPass> ACTIVE_PASS = new ThreadLocal<>();
|
||||
private static final Map<Player, CollisionResult> LAST_RESULTS =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private SableParentWorldCollision() {}
|
||||
|
||||
/** Called from the redirected vanilla collision invocation inside Sable's merged move hook. */
|
||||
public static Vec3 collideOrTrust(
|
||||
Player player,
|
||||
Vec3 requestedMotion,
|
||||
SubLevel frame,
|
||||
boolean captureSablePass
|
||||
) {
|
||||
SubLevelEntityCollision.CollisionInfo info =
|
||||
((EntityMovementExtension) player).sable$getCollisionInfo();
|
||||
CollisionPass pass = captureSablePass ? CollisionPass.capture(info) : null;
|
||||
|
||||
// ServerPlayer receives an already OBB-resolved position from the authoritative local
|
||||
// client. Running an upright server AABB through collide() here only creates corrections.
|
||||
if (!player.isLocalPlayer() || frame == null || frame.isRemoved()) {
|
||||
CollisionResult result = CollisionResult.clear(requestedMotion);
|
||||
if (pass != null) {
|
||||
ACTIVE_PASS.set(pass.withResult(result));
|
||||
}
|
||||
LAST_RESULTS.put(player, result);
|
||||
return requestedMotion;
|
||||
}
|
||||
|
||||
Quaterniondc orientation = SableFrameOrientation.getFrameOrientation(frame, 1.0F);
|
||||
CollisionResult result = resolve(player, requestedMotion, orientation);
|
||||
if (pass != null) {
|
||||
ACTIVE_PASS.set(pass.withResult(result));
|
||||
}
|
||||
LAST_RESULTS.put(player, result);
|
||||
return result.motion();
|
||||
}
|
||||
|
||||
/** Restores semantic local-up flags after Sable compares the motions as world X/Y/Z axes. */
|
||||
public static void restoreSableFlags(Player player) {
|
||||
CollisionPass pass = ACTIVE_PASS.get();
|
||||
ACTIVE_PASS.remove();
|
||||
if (pass == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
EntityMovementExtension movement = (EntityMovementExtension) player;
|
||||
SubLevelEntityCollision.CollisionInfo info = movement.sable$getCollisionInfo();
|
||||
if (info == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
CollisionResult parent = pass.parentResult();
|
||||
info.horizontalCollision = pass.subLevelHorizontalCollision()
|
||||
|| pass.horizontalCollision()
|
||||
|| parent.horizontalCollision();
|
||||
info.verticalCollision = pass.verticalCollision() || parent.verticalCollision();
|
||||
info.verticalCollisionBelow = pass.verticalCollisionBelow()
|
||||
|| parent.verticalCollisionBelow();
|
||||
info.minorHorizontalCollision = pass.minorHorizontalCollision();
|
||||
info.trackingSubLevel = pass.trackingSubLevel();
|
||||
movement.sable$setTrackingSubLevel(pass.trackingSubLevel());
|
||||
}
|
||||
|
||||
public static CollisionResult getLastResult(Player player) {
|
||||
return LAST_RESULTS.getOrDefault(player, CollisionResult.clear(Vec3.ZERO));
|
||||
}
|
||||
|
||||
/** Exact parent-world fit test for crouch/stand pose selection. */
|
||||
public static boolean canFit(
|
||||
Player player,
|
||||
Vec3 physicalFeet,
|
||||
EntityDimensions dimensions,
|
||||
Quaterniondc orientation
|
||||
) {
|
||||
double width = Math.max(0.0D, dimensions.width() - EPSILON * 2.0D);
|
||||
double height = Math.max(0.0D, dimensions.height() - EPSILON * 2.0D);
|
||||
Vector3d up = orientation.transform(new Vector3d(0.0D, 1.0D, 0.0D));
|
||||
Vector3d center = new Vector3d(
|
||||
physicalFeet.x + up.x * (height * 0.5D + EPSILON),
|
||||
physicalFeet.y + up.y * (height * 0.5D + EPSILON),
|
||||
physicalFeet.z + up.z * (height * 0.5D + EPSILON)
|
||||
);
|
||||
return !hasCollision(player, center, width, height, orientation);
|
||||
}
|
||||
|
||||
private static CollisionResult resolve(
|
||||
Player player,
|
||||
Vec3 requestedMotion,
|
||||
Quaterniondc orientation
|
||||
) {
|
||||
if (!isFinite(requestedMotion)) {
|
||||
return CollisionResult.clear(Vec3.ZERO);
|
||||
}
|
||||
|
||||
Vector3d startCenter = toJoml(SableOrientedBounds.worldCenter(player, orientation));
|
||||
Vector3d resolvedCenter = new Vector3d(startCenter);
|
||||
Vector3d step = toJoml(requestedMotion);
|
||||
int substeps = Math.min(
|
||||
MAX_SUBSTEPS,
|
||||
Math.max(1, (int) Math.ceil(step.length() / MAX_SUBSTEP))
|
||||
);
|
||||
step.div(substeps);
|
||||
|
||||
Vector3d up = orientation.transform(new Vector3d(0.0D, 1.0D, 0.0D)).normalize();
|
||||
MutableCollision flags = new MutableCollision();
|
||||
|
||||
for (int substep = 0; substep < substeps; substep++) {
|
||||
resolvedCenter.add(step);
|
||||
resolvePenetration(player, resolvedCenter, step, up, orientation, flags);
|
||||
}
|
||||
|
||||
Vector3d resolved = resolvedCenter.sub(startCenter);
|
||||
Vec3 motion = new Vec3(resolved.x, resolved.y, resolved.z);
|
||||
if (!isFinite(motion)) {
|
||||
motion = Vec3.ZERO;
|
||||
}
|
||||
return new CollisionResult(
|
||||
requestedMotion,
|
||||
motion,
|
||||
flags.horizontal,
|
||||
flags.vertical,
|
||||
flags.below,
|
||||
flags.contacts,
|
||||
flags.maxCorrection
|
||||
);
|
||||
}
|
||||
|
||||
private static void resolvePenetration(
|
||||
Player player,
|
||||
Vector3d center,
|
||||
Vector3d requestedStep,
|
||||
Vector3d up,
|
||||
Quaterniondc orientation,
|
||||
MutableCollision flags
|
||||
) {
|
||||
LevelReusedVectors sink = ((LevelExtension) player.level()).sable$getJOMLSink();
|
||||
OrientedBoundingBox3d entity = new OrientedBoundingBox3d(
|
||||
center,
|
||||
new Vector3d(player.getBbWidth(), player.getBbHeight(), player.getBbWidth()),
|
||||
orientation,
|
||||
sink
|
||||
);
|
||||
OrientedBoundingBox3d block = new OrientedBoundingBox3d(sink);
|
||||
block.getOrientation().identity();
|
||||
Vector3d mtv = new Vector3d();
|
||||
Vector3d largestMtv = new Vector3d();
|
||||
|
||||
for (int iteration = 0; iteration < MAX_RESOLUTION_ITERATIONS; iteration++) {
|
||||
largestMtv.zero();
|
||||
double[] largestLengthSqr = {0.0D};
|
||||
|
||||
AABB envelope = envelope(center, player.getBbWidth(), player.getBbHeight(), orientation)
|
||||
.inflate(EPSILON);
|
||||
forEachCollisionBox(player, envelope, box -> {
|
||||
setBlockObb(block, box);
|
||||
mtv.set(Double.MAX_VALUE);
|
||||
OrientedBoundingBox3d.sat(entity, block, mtv);
|
||||
double lengthSqr = validMtv(mtv) ? mtv.lengthSquared() : 0.0D;
|
||||
if (lengthSqr > largestLengthSqr[0]) {
|
||||
largestLengthSqr[0] = lengthSqr;
|
||||
largestMtv.set(mtv);
|
||||
}
|
||||
});
|
||||
|
||||
if (largestLengthSqr[0] <= SAT_EPSILON_SQR) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3d normal = new Vector3d(largestMtv).normalize();
|
||||
double upDot = normal.dot(up);
|
||||
boolean vertical = Math.abs(upDot) > 0.6D;
|
||||
|
||||
if (!vertical
|
||||
&& player.onGround()
|
||||
&& requestedStep.dot(normal) < 0.0D
|
||||
&& tryStepUp(player, center, up, orientation)) {
|
||||
entity.setPosition(center);
|
||||
continue;
|
||||
}
|
||||
|
||||
center.add(largestMtv);
|
||||
entity.setPosition(center);
|
||||
flags.contacts++;
|
||||
flags.maxCorrection = Math.max(
|
||||
flags.maxCorrection,
|
||||
Math.sqrt(largestLengthSqr[0])
|
||||
);
|
||||
if (vertical) {
|
||||
flags.vertical = true;
|
||||
if (upDot > 0.0D) {
|
||||
flags.below = true;
|
||||
}
|
||||
} else {
|
||||
flags.horizontal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean tryStepUp(
|
||||
Player player,
|
||||
Vector3d center,
|
||||
Vector3d up,
|
||||
Quaterniondc orientation
|
||||
) {
|
||||
double maxStep = player.maxUpStep();
|
||||
if (maxStep <= EPSILON) {
|
||||
return false;
|
||||
}
|
||||
Vector3d candidate = new Vector3d();
|
||||
for (double amount = 0.0625D; amount <= maxStep + EPSILON; amount += 0.0625D) {
|
||||
candidate.set(center).fma(amount, up);
|
||||
if (!hasCollision(
|
||||
player,
|
||||
candidate,
|
||||
player.getBbWidth(),
|
||||
player.getBbHeight(),
|
||||
orientation
|
||||
)) {
|
||||
center.set(candidate);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean hasCollision(
|
||||
Player player,
|
||||
Vector3d center,
|
||||
double width,
|
||||
double height,
|
||||
Quaterniondc orientation
|
||||
) {
|
||||
LevelReusedVectors sink = ((LevelExtension) player.level()).sable$getJOMLSink();
|
||||
OrientedBoundingBox3d entity = new OrientedBoundingBox3d(
|
||||
center,
|
||||
new Vector3d(width, height, width),
|
||||
orientation,
|
||||
sink
|
||||
);
|
||||
OrientedBoundingBox3d block = new OrientedBoundingBox3d(sink);
|
||||
block.getOrientation().identity();
|
||||
Vector3d mtv = new Vector3d();
|
||||
boolean[] collision = {false};
|
||||
AABB envelope = envelope(center, width, height, orientation).inflate(EPSILON);
|
||||
forEachCollisionBox(player, envelope, box -> {
|
||||
if (collision[0]) {
|
||||
return;
|
||||
}
|
||||
setBlockObb(block, box);
|
||||
mtv.set(Double.MAX_VALUE);
|
||||
OrientedBoundingBox3d.sat(entity, block, mtv);
|
||||
collision[0] = validMtv(mtv);
|
||||
});
|
||||
return collision[0];
|
||||
}
|
||||
|
||||
private static void forEachCollisionBox(
|
||||
Player player,
|
||||
AABB envelope,
|
||||
java.util.function.Consumer<AABB> consumer
|
||||
) {
|
||||
Level level = player.level();
|
||||
CollisionContext context = CollisionContext.of(player);
|
||||
int minX = Mth.floor(envelope.minX);
|
||||
int minY = Math.max(level.getMinBuildHeight(), Mth.floor(envelope.minY));
|
||||
int minZ = Mth.floor(envelope.minZ);
|
||||
int maxX = Mth.floor(envelope.maxX - EPSILON);
|
||||
int maxY = Math.min(level.getMaxBuildHeight() - 1, Mth.floor(envelope.maxY - EPSILON));
|
||||
int maxZ = Mth.floor(envelope.maxZ - EPSILON);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
pos.set(x, minY, z);
|
||||
if (!level.hasChunkAt(pos)) {
|
||||
continue;
|
||||
}
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
pos.set(x, y, z);
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (state.isAir()) {
|
||||
continue;
|
||||
}
|
||||
VoxelShape shape = state.getCollisionShape(level, pos, context);
|
||||
for (AABB localBox : shape.toAabbs()) {
|
||||
consumer.accept(localBox.move(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void setBlockObb(OrientedBoundingBox3d block, AABB box) {
|
||||
block.getPosition().set(
|
||||
(box.minX + box.maxX) * 0.5D,
|
||||
(box.minY + box.maxY) * 0.5D,
|
||||
(box.minZ + box.maxZ) * 0.5D
|
||||
);
|
||||
block.getDimensions().set(box.getXsize(), box.getYsize(), box.getZsize());
|
||||
block.getOrientation().identity();
|
||||
}
|
||||
|
||||
private static boolean validMtv(Vector3d mtv) {
|
||||
return Double.isFinite(mtv.x)
|
||||
&& Double.isFinite(mtv.y)
|
||||
&& Double.isFinite(mtv.z)
|
||||
&& mtv.x != Double.MAX_VALUE
|
||||
&& mtv.y != Double.MAX_VALUE
|
||||
&& mtv.z != Double.MAX_VALUE
|
||||
&& mtv.lengthSquared() > SAT_EPSILON_SQR;
|
||||
}
|
||||
|
||||
private static AABB envelope(
|
||||
Vector3d center,
|
||||
double width,
|
||||
double height,
|
||||
Quaterniondc orientation
|
||||
) {
|
||||
double halfWidth = width * 0.5D;
|
||||
double halfHeight = height * 0.5D;
|
||||
Vector3d right = orientation.transform(new Vector3d(halfWidth, 0.0D, 0.0D));
|
||||
Vector3d up = orientation.transform(new Vector3d(0.0D, halfHeight, 0.0D));
|
||||
Vector3d forward = orientation.transform(new Vector3d(0.0D, 0.0D, halfWidth));
|
||||
double extentX = Math.abs(right.x) + Math.abs(up.x) + Math.abs(forward.x);
|
||||
double extentY = Math.abs(right.y) + Math.abs(up.y) + Math.abs(forward.y);
|
||||
double extentZ = Math.abs(right.z) + Math.abs(up.z) + Math.abs(forward.z);
|
||||
return new AABB(
|
||||
center.x - extentX,
|
||||
center.y - extentY,
|
||||
center.z - extentZ,
|
||||
center.x + extentX,
|
||||
center.y + extentY,
|
||||
center.z + extentZ
|
||||
);
|
||||
}
|
||||
|
||||
private static Vector3d toJoml(Vec3 value) {
|
||||
return new Vector3d(value.x, value.y, value.z);
|
||||
}
|
||||
|
||||
private static boolean isFinite(Vec3 value) {
|
||||
return Double.isFinite(value.x)
|
||||
&& Double.isFinite(value.y)
|
||||
&& Double.isFinite(value.z);
|
||||
}
|
||||
|
||||
private static final class MutableCollision {
|
||||
private boolean horizontal;
|
||||
private boolean vertical;
|
||||
private boolean below;
|
||||
private int contacts;
|
||||
private double maxCorrection;
|
||||
}
|
||||
|
||||
private record CollisionPass(
|
||||
boolean subLevelHorizontalCollision,
|
||||
boolean horizontalCollision,
|
||||
boolean verticalCollision,
|
||||
boolean verticalCollisionBelow,
|
||||
boolean minorHorizontalCollision,
|
||||
SubLevel trackingSubLevel,
|
||||
CollisionResult parentResult
|
||||
) {
|
||||
private static CollisionPass capture(SubLevelEntityCollision.CollisionInfo info) {
|
||||
if (info == null) {
|
||||
return new CollisionPass(false, false, false, false, false, null, null);
|
||||
}
|
||||
return new CollisionPass(
|
||||
info.subLevelHorizontalCollision,
|
||||
info.horizontalCollision,
|
||||
info.verticalCollision,
|
||||
info.verticalCollisionBelow,
|
||||
info.minorHorizontalCollision,
|
||||
info.trackingSubLevel,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private CollisionPass withResult(CollisionResult result) {
|
||||
return new CollisionPass(
|
||||
subLevelHorizontalCollision,
|
||||
horizontalCollision,
|
||||
verticalCollision,
|
||||
verticalCollisionBelow,
|
||||
minorHorizontalCollision,
|
||||
trackingSubLevel,
|
||||
result
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public record CollisionResult(
|
||||
Vec3 requestedMotion,
|
||||
Vec3 motion,
|
||||
boolean horizontalCollision,
|
||||
boolean verticalCollision,
|
||||
boolean verticalCollisionBelow,
|
||||
int contacts,
|
||||
double maxCorrection
|
||||
) {
|
||||
private static CollisionResult clear(Vec3 motion) {
|
||||
return new CollisionResult(motion, motion, false, false, false, 0, 0.0D);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package dev.nightly.sablegravity.physics;
|
||||
|
||||
import dev.nightly.sablegravity.api.GravityFrameId;
|
||||
import dev.nightly.sablegravity.api.GravityFrameProvider;
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/** Built-in provider for physical Sable ship/contraption frames. */
|
||||
final class SableSubLevelFrameProvider implements GravityFrameProvider {
|
||||
/** Captures entities emitted just outside a hull before world gravity can steal them. */
|
||||
private static final double CAPTURE_MARGIN = 8.0D;
|
||||
/** Wider release radius prevents one-tick frame flapping at a moving/rotating hull boundary. */
|
||||
private static final double RELEASE_MARGIN = 12.0D;
|
||||
|
||||
@Override
|
||||
public Optional<SableSubLevelGravityFrame> resolve(
|
||||
Entity entity,
|
||||
GravityFrameId previousFrame
|
||||
) {
|
||||
Level level = entity.level();
|
||||
|
||||
// Sable's collision result is authoritative when it has just selected another ship.
|
||||
SubLevel tracked = valid(level, Sable.HELPER.getTrackingSubLevel(entity));
|
||||
if (tracked != null && withinLocalBounds(tracked, entity.position(), RELEASE_MARGIN)) {
|
||||
return Optional.of(new SableSubLevelGravityFrame(tracked));
|
||||
}
|
||||
|
||||
SubLevel previous = fromId(level, previousFrame);
|
||||
if (previous != null && withinLocalBounds(previous, entity.position(), RELEASE_MARGIN)) {
|
||||
return Optional.of(new SableSubLevelGravityFrame(previous));
|
||||
}
|
||||
|
||||
SubLevel lastTracked = valid(level, Sable.HELPER.getLastTrackingSubLevel(entity));
|
||||
if (lastTracked != null
|
||||
&& withinLocalBounds(lastTracked, entity.position(), RELEASE_MARGIN)) {
|
||||
return Optional.of(new SableSubLevelGravityFrame(lastTracked));
|
||||
}
|
||||
|
||||
return findAt(level, entity.position(), entity.getBoundingBox());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<SableSubLevelGravityFrame> resolveAt(
|
||||
Level level,
|
||||
Vec3 worldPosition,
|
||||
GravityFrameId previousFrame
|
||||
) {
|
||||
SubLevel previous = fromId(level, previousFrame);
|
||||
if (previous != null && withinLocalBounds(previous, worldPosition, RELEASE_MARGIN)) {
|
||||
return Optional.of(new SableSubLevelGravityFrame(previous));
|
||||
}
|
||||
AABB pointBounds = new AABB(worldPosition, worldPosition).inflate(CAPTURE_MARGIN);
|
||||
return findAt(level, worldPosition, pointBounds);
|
||||
}
|
||||
|
||||
private static Optional<SableSubLevelGravityFrame> findAt(
|
||||
Level level,
|
||||
Vec3 worldPosition,
|
||||
AABB searchBounds
|
||||
) {
|
||||
SubLevel best = null;
|
||||
double bestDistance = Double.POSITIVE_INFINITY;
|
||||
for (SubLevel candidate : Sable.HELPER.getAllIntersecting(
|
||||
level,
|
||||
new BoundingBox3d(searchBounds.inflate(CAPTURE_MARGIN))
|
||||
)) {
|
||||
candidate = valid(level, candidate);
|
||||
if (candidate == null) {
|
||||
continue;
|
||||
}
|
||||
double distance = distanceToLocalBounds(candidate, worldPosition);
|
||||
if (distance <= CAPTURE_MARGIN * CAPTURE_MARGIN && distance < bestDistance) {
|
||||
best = candidate;
|
||||
bestDistance = distance;
|
||||
}
|
||||
}
|
||||
return best == null
|
||||
? Optional.empty()
|
||||
: Optional.of(new SableSubLevelGravityFrame(best));
|
||||
}
|
||||
|
||||
static SubLevel fromId(Level level, GravityFrameId id) {
|
||||
if (id == null
|
||||
|| !SableSubLevelGravityFrame.PROVIDER_ID.equals(id.provider())
|
||||
|| !id.dimension().equals(level.dimension().location())) {
|
||||
return null;
|
||||
}
|
||||
SubLevelContainer container = SubLevelContainer.getContainer(level);
|
||||
return container == null ? null : valid(level, container.getSubLevel(id.localId()));
|
||||
}
|
||||
|
||||
private static SubLevel valid(Level level, SubLevel frame) {
|
||||
return frame != null && !frame.isRemoved() && frame.getLevel() == level ? frame : null;
|
||||
}
|
||||
|
||||
private static boolean withinLocalBounds(SubLevel frame, Vec3 worldPosition, double margin) {
|
||||
return distanceToLocalBounds(frame, worldPosition) <= margin * margin;
|
||||
}
|
||||
|
||||
private static double distanceToLocalBounds(SubLevel frame, Vec3 worldPosition) {
|
||||
Vec3 local = frame.logicalPose().transformPositionInverse(worldPosition);
|
||||
var bounds = frame.getPlot().getBoundingBox();
|
||||
double dx = axisDistance(local.x, bounds.minX(), bounds.maxX() + 1.0D);
|
||||
double dy = axisDistance(local.y, bounds.minY(), bounds.maxY() + 1.0D);
|
||||
double dz = axisDistance(local.z, bounds.minZ(), bounds.maxZ() + 1.0D);
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private static double axisDistance(double value, double min, double max) {
|
||||
if (value < min) {
|
||||
return min - value;
|
||||
}
|
||||
if (value > max) {
|
||||
return value - max;
|
||||
}
|
||||
return 0.0D;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package dev.nightly.sablegravity.physics;
|
||||
|
||||
import dev.nightly.sablegravity.SableGravityMod;
|
||||
import dev.nightly.sablegravity.api.GravityFrame;
|
||||
import dev.nightly.sablegravity.api.GravityFrameId;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Quaterniondc;
|
||||
|
||||
/** Gravity frame backed by a live Sable SubLevel pose. */
|
||||
public final class SableSubLevelGravityFrame implements GravityFrame {
|
||||
public static final net.minecraft.resources.ResourceLocation PROVIDER_ID =
|
||||
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath(
|
||||
SableGravityMod.MOD_ID,
|
||||
"sable_sublevel"
|
||||
);
|
||||
|
||||
private final SubLevel subLevel;
|
||||
private final GravityFrameId id;
|
||||
|
||||
public SableSubLevelGravityFrame(SubLevel subLevel) {
|
||||
this.subLevel = subLevel;
|
||||
this.id = new GravityFrameId(
|
||||
PROVIDER_ID,
|
||||
subLevel.getLevel().dimension().location(),
|
||||
subLevel.getUniqueId()
|
||||
);
|
||||
}
|
||||
|
||||
public SubLevel subLevel() {
|
||||
return subLevel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GravityFrameId id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Level level() {
|
||||
return subLevel.getLevel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return !subLevel.isRemoved();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 toLocalPosition(Vec3 worldPosition) {
|
||||
return subLevel.logicalPose().transformPositionInverse(worldPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 toWorldPosition(Vec3 localPosition) {
|
||||
return subLevel.logicalPose().transformPosition(localPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 toLocalVelocity(Vec3 worldVelocity) {
|
||||
return subLevel.logicalPose().transformNormalInverse(worldVelocity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 toWorldVelocity(Vec3 localVelocity) {
|
||||
return subLevel.logicalPose().transformNormal(localVelocity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Quaterniondc orientation(float partialTick) {
|
||||
return SableFrameOrientation.getFrameOrientation(subLevel, partialTick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 frameVelocityAt(Vec3 worldPosition) {
|
||||
Vec3 previousLocal = subLevel.lastPose().transformPositionInverse(worldPosition);
|
||||
return subLevel.logicalPose().transformPosition(previousLocal).subtract(worldPosition);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ license = "All Rights Reserved"
|
||||
|
||||
[[mods]]
|
||||
modId = "sablegravity"
|
||||
version = "0.6.0-test16-ground-flight-sneak"
|
||||
version = "0.7.4-player-recovery"
|
||||
displayName = "Sable Gravity Local Physics"
|
||||
authors = "Nightly"
|
||||
description = '''Adds SubLevel-local gravity with Sable OBB collision, stable local grounding, creative flight, local-space animation and ship-local edge sneaking.'''
|
||||
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.'''
|
||||
|
||||
[[mixins]]
|
||||
config = "sablegravity.mixins.json"
|
||||
|
||||
@@ -4,14 +4,23 @@
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"minVersion": "0.8",
|
||||
"mixins": [
|
||||
"EntityGravityStateMixin",
|
||||
"PlayerGravityDataMixin",
|
||||
"EntityDirectionalGravityMixin",
|
||||
"LivingEntityDirectionalGravityMixin",
|
||||
"PlayerLocalPhysicsMixin",
|
||||
"EntityCustomOrientationMixin",
|
||||
"LivingEntityInputAccessor",
|
||||
"LivingEntityJumpMixin",
|
||||
"PlayerEdgeSneakMixin"
|
||||
"PlayerPoseFitMixin",
|
||||
"EntityCustomOrientationMixin",
|
||||
"EntityAabbIsolationMixin",
|
||||
"SableCollisionYawMixin",
|
||||
"PlayerEdgeSneakMixin",
|
||||
"PlayerFlightCrouchPoseMixin"
|
||||
],
|
||||
"client": [
|
||||
"LocalPlayerLifecycleMixin"
|
||||
"LocalPlayerLifecycleMixin",
|
||||
"LocalPlayerFlightCrouchMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package dev.devce.rocketnautics.shaded.orekit.compiler.plugin;
|
||||
|
||||
import com.sun.source.util.JavacTask;
|
||||
import com.sun.source.util.Plugin;
|
||||
|
||||
/** Dev-runtime service shim for the provider omitted from Cosmonautics 26.07.270. */
|
||||
public final class DefaultDataContextPlugin implements Plugin {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DefaultDataContextPlugin";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(JavacTask task, String... arguments) {
|
||||
// Java-eval snippets do not compile Orekit @DefaultDataContext annotated sources.
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user