test16 version as base for OOB gravity
This commit is contained in:
+45
@@ -0,0 +1,45 @@
|
||||
### Gradle ###
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/**/build/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/**/out/
|
||||
|
||||
.run/
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.eclipse/
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/**/bin/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### OpenCode ###
|
||||
.opencode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
|
||||
### Minecraft Modding ###
|
||||
run/
|
||||
!**/src/**/run/
|
||||
**/src/generated/**/.cache/
|
||||
repo/
|
||||
!**/src/**/repo/
|
||||
|
||||
### Local reference checkouts ###
|
||||
@@ -0,0 +1,12 @@
|
||||
# Sable Gravity 0.6.0 test16
|
||||
|
||||
Built on the working test13 Sable OBB collision path.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,70 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'net.neoforged.moddev' version '2.0.107'
|
||||
}
|
||||
|
||||
group = 'dev.nightly.sablegravity'
|
||||
version = project.mod_version
|
||||
|
||||
base {
|
||||
archivesName = "sablegravity"
|
||||
}
|
||||
|
||||
java.toolchain.languageVersion = JavaLanguageVersion.of(21)
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
name = 'Modrinth'
|
||||
url = 'https://api.modrinth.com/maven'
|
||||
content {
|
||||
includeGroup 'maven.modrinth'
|
||||
}
|
||||
}
|
||||
maven {
|
||||
name = 'RyanHCode'
|
||||
url = 'https://maven.ryanhcode.dev/releases'
|
||||
content {
|
||||
includeGroup 'dev.ryanhcode.sable-companion'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
neoForge {
|
||||
version = project.neo_version
|
||||
|
||||
runs {
|
||||
client {
|
||||
client()
|
||||
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
|
||||
}
|
||||
server {
|
||||
server()
|
||||
programArgument '--nogui'
|
||||
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
|
||||
}
|
||||
}
|
||||
|
||||
mods {
|
||||
sablegravity {
|
||||
sourceSet sourceSets.main
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Sable is part of the compile API used by this mod and is also loaded in dev runs.
|
||||
implementation "maven.modrinth:sable:${project.sable_version}"
|
||||
// Sable embeds Companion at runtime, but its public API is a separate compile artifact.
|
||||
compileOnly "dev.ryanhcode.sable-companion:sable-companion-common-1.21.1:${project.sable_companion_version}"
|
||||
|
||||
// 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:the-one-probe:${project.the_one_probe_version}"
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = 'UTF-8'
|
||||
options.release = 21
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
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
|
||||
the_one_probe_version=1.21_neo-12.0.8
|
||||
Vendored
BIN
Binary file not shown.
+8
@@ -0,0 +1,8 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
# Architectury Loom needs Gradle 8.x (not 9.x). 8.10.2 is known-good with Loom 1.7.
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,251 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,9 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
maven { url = 'https://maven.neoforged.net/releases' }
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = 'sablegravity'
|
||||
@@ -0,0 +1,125 @@
|
||||
package dev.nightly.sablegravity;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
||||
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.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 java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Mod(SableGravityMod.MOD_ID)
|
||||
public final class SableGravityMod {
|
||||
public static final String MOD_ID = "sablegravity";
|
||||
public static final float VANILLA_STRENGTH = 9.8F;
|
||||
private static volatile float globalStrength = VANILLA_STRENGTH;
|
||||
|
||||
public SableGravityMod(ModContainer container, IEventBus modBus) {
|
||||
NeoForge.EVENT_BUS.addListener(this::registerCommands);
|
||||
}
|
||||
|
||||
private void registerCommands(RegisterCommandsEvent event) {
|
||||
register(event.getDispatcher());
|
||||
}
|
||||
|
||||
private static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
|
||||
dispatcher.register(
|
||||
Commands.literal("sablegravity")
|
||||
.requires(source -> source.hasPermission(2))
|
||||
.then(Commands.literal("strength")
|
||||
.executes(context -> showStrength(context.getSource()))
|
||||
.then(Commands.argument("value", DoubleArgumentType.doubleArg(0.0D, 100.0D))
|
||||
.executes(context -> setStrength(
|
||||
context.getSource(),
|
||||
(float) DoubleArgumentType.getDouble(context, "value")
|
||||
))
|
||||
)
|
||||
)
|
||||
.then(Commands.literal("enable")
|
||||
.executes(context -> setEnabled(
|
||||
context.getSource(),
|
||||
List.of(context.getSource().getPlayerOrException()),
|
||||
true
|
||||
))
|
||||
.then(Commands.argument("targets", EntityArgument.players())
|
||||
.executes(context -> setEnabled(
|
||||
context.getSource(),
|
||||
EntityArgument.getPlayers(context, "targets"),
|
||||
true
|
||||
))
|
||||
)
|
||||
)
|
||||
.then(Commands.literal("disable")
|
||||
.executes(context -> setEnabled(
|
||||
context.getSource(),
|
||||
List.of(context.getSource().getPlayerOrException()),
|
||||
false
|
||||
))
|
||||
.then(Commands.argument("targets", EntityArgument.players())
|
||||
.executes(context -> setEnabled(
|
||||
context.getSource(),
|
||||
EntityArgument.getPlayers(context, "targets"),
|
||||
false
|
||||
))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static int showStrength(CommandSourceStack source) {
|
||||
source.sendSuccess(
|
||||
() -> Component.literal("Sable gravity strength: " + format(globalStrength) + " m/s²"),
|
||||
false
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int setStrength(CommandSourceStack source, float strength) {
|
||||
globalStrength = strength;
|
||||
for (ServerPlayer player : source.getServer().getPlayerList().getPlayers()) {
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
state.sablegravity$setStrength(strength);
|
||||
}
|
||||
source.sendSuccess(
|
||||
() -> Component.literal(
|
||||
"Sable gravity strength set to " + format(strength) + " m/s²"
|
||||
+ (strength == VANILLA_STRENGTH ? " (vanilla)" : "")
|
||||
),
|
||||
true
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int setEnabled(CommandSourceStack source, Collection<ServerPlayer> targets, boolean enabled) {
|
||||
for (ServerPlayer player : targets) {
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
state.sablegravity$setStrength(globalStrength);
|
||||
state.sablegravity$setEnabled(enabled);
|
||||
state.sablegravity$setLocalStateValid(false);
|
||||
}
|
||||
int count = targets.size();
|
||||
source.sendSuccess(
|
||||
() -> Component.literal(
|
||||
"Sable gravity " + (enabled ? "enabled" : "disabled")
|
||||
+ " for " + count + " player" + (count == 1 ? "" : "s")
|
||||
),
|
||||
true
|
||||
);
|
||||
return count;
|
||||
}
|
||||
|
||||
private static String format(float value) {
|
||||
if (value == (long) value) {
|
||||
return Long.toString((long) value);
|
||||
}
|
||||
return Float.toString(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package dev.nightly.sablegravity;
|
||||
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface SableGravityState {
|
||||
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,79 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
import dev.nightly.sablegravity.physics.SableLocalPlayerPhysics;
|
||||
import dev.ryanhcode.sable.api.entity.EntitySubLevelUtil;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Mixin(value = EntitySubLevelUtil.class, priority = 3000, remap = false)
|
||||
public abstract class EntityCustomOrientationMixin {
|
||||
@Inject(
|
||||
method = "getCustomEntityOrientation",
|
||||
at = @At("HEAD"),
|
||||
cancellable = true,
|
||||
remap = false
|
||||
)
|
||||
private static void sablegravity$getCustomOrientation(
|
||||
Entity entity,
|
||||
float partialTicks,
|
||||
CallbackInfoReturnable<Quaterniondc> cir
|
||||
) {
|
||||
if (!(entity instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
if (!state.sablegravity$isEnabled() || state.sablegravity$isInLocalSimulation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrame(player);
|
||||
if (frame == null || frame.isRemoved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
cir.setReturnValue(frame.logicalPose().orientation());
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "hasCustomEntityOrientation",
|
||||
at = @At("HEAD"),
|
||||
cancellable = true,
|
||||
remap = false
|
||||
)
|
||||
private static void sablegravity$hasCustomOrientation(
|
||||
Entity entity,
|
||||
CallbackInfoReturnable<Boolean> cir
|
||||
) {
|
||||
if (!(entity instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
if (!state.sablegravity$isEnabled() || state.sablegravity$isInLocalSimulation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrame(player);
|
||||
if (frame == null || frame.isRemoved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
cir.setReturnValue(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
import dev.nightly.sablegravity.physics.SableLocalPlayerPhysics;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3dc;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
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;
|
||||
|
||||
/** Computes limb animation from displacement in the ship-local frame, not world X/Z. */
|
||||
@Mixin(LivingEntity.class)
|
||||
public abstract class LivingEntityAnimationMixin {
|
||||
@Inject(method = "calculateEntityAnimation", at = @At("HEAD"), cancellable = true)
|
||||
private void sablegravity$calculateLocalAnimation(boolean flutter, CallbackInfo ci) {
|
||||
LivingEntity self = (LivingEntity) (Object) this;
|
||||
if (!(self instanceof Player player)
|
||||
|| !player.level().isClientSide()
|
||||
|| !player.isLocalPlayer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
if (!state.sablegravity$isEnabled() || !state.sablegravity$isLocalStateValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrame(player);
|
||||
if (frame == null || frame.isRemoved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Pose3dc pose = frame.logicalPose();
|
||||
Vec3 currentLocal = pose.transformPositionInverse(player.position());
|
||||
Vec3 previousLocal = state.sablegravity$getLocalPosition();
|
||||
Vec3 localDelta = currentLocal.subtract(previousLocal);
|
||||
state.sablegravity$setLocalPosition(currentLocal);
|
||||
|
||||
double dy = flutter ? localDelta.y : 0.0D;
|
||||
float distance = (float) Math.sqrt(
|
||||
localDelta.x * localDelta.x + dy * dy + localDelta.z * localDelta.z
|
||||
);
|
||||
float animationSpeed = Math.min(distance * 4.0F, 1.0F);
|
||||
self.walkAnimation.update(animationSpeed, 0.4F);
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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.LivingEntity;
|
||||
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;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Runs vanilla jump construction in ship-local velocity coordinates. */
|
||||
@Mixin(LivingEntity.class)
|
||||
public abstract class LivingEntityJumpMixin {
|
||||
@Unique private SubLevel sablegravity$jumpFrame;
|
||||
@Unique private boolean sablegravity$localizedJump;
|
||||
|
||||
@Inject(method = "jumpFromGround", at = @At("HEAD"))
|
||||
private void sablegravity$beforeJump(CallbackInfo ci) {
|
||||
if (!((Object) this instanceof Player player) || !player.isLocalPlayer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
if (!state.sablegravity$isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrame(player);
|
||||
if (frame == null || frame.isRemoved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// jumpFromGround assumes +Y and yaw are ordinary world axes. Temporarily make velocity
|
||||
// local, let vanilla build jump/sprint impulse unchanged, then rotate the result back.
|
||||
Vec3 localVelocity;
|
||||
if (state.sablegravity$isLocalStateValid()
|
||||
&& state.sablegravity$getFrameId() != null
|
||||
&& state.sablegravity$getFrameId().equals(frame.getUniqueId())) {
|
||||
localVelocity = state.sablegravity$getLocalVelocity();
|
||||
} else {
|
||||
localVelocity = frame.logicalPose().transformNormalInverse(player.getDeltaMovement());
|
||||
}
|
||||
player.setDeltaMovement(localVelocity);
|
||||
state.sablegravity$setInLocalSimulation(true);
|
||||
sablegravity$jumpFrame = frame;
|
||||
sablegravity$localizedJump = true;
|
||||
}
|
||||
|
||||
@Inject(method = "jumpFromGround", at = @At("TAIL"))
|
||||
private void sablegravity$afterJump(CallbackInfo ci) {
|
||||
if (!sablegravity$localizedJump
|
||||
|| !((Object) this instanceof Player player)
|
||||
|| sablegravity$jumpFrame == null) {
|
||||
sablegravity$clearJumpState();
|
||||
return;
|
||||
}
|
||||
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
Vec3 localVelocity = player.getDeltaMovement();
|
||||
state.sablegravity$setLocalVelocity(localVelocity);
|
||||
state.sablegravity$setLocalStateValid(true);
|
||||
state.sablegravity$setFrameId(sablegravity$jumpFrame.getUniqueId());
|
||||
player.setDeltaMovement(sablegravity$jumpFrame.logicalPose().transformNormal(localVelocity));
|
||||
sablegravity$clearJumpState();
|
||||
}
|
||||
|
||||
@Unique
|
||||
private void sablegravity$clearJumpState() {
|
||||
if ((Object) this instanceof SableGravityState state) {
|
||||
state.sablegravity$setInLocalSimulation(false);
|
||||
}
|
||||
sablegravity$jumpFrame = null;
|
||||
sablegravity$localizedJump = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import dev.nightly.sablegravity.physics.SableLocalPlayerPhysics;
|
||||
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.CallbackInfo;
|
||||
|
||||
/** Keeps creative flight independent from vanilla's world-axis ground flag. */
|
||||
@Mixin(targets = "net.minecraft.client.player.LocalPlayer", priority = 4000)
|
||||
public abstract class LocalPlayerLifecycleMixin {
|
||||
@Inject(method = "aiStep", at = @At("HEAD"))
|
||||
private void sablegravity$beforeLocalAiStep(CallbackInfo ci) {
|
||||
SableLocalPlayerPhysics.beforeLocalPlayerTick((Player) (Object) this);
|
||||
}
|
||||
|
||||
@Inject(method = "aiStep", at = @At("TAIL"))
|
||||
private void sablegravity$afterLocalAiStep(CallbackInfo ci) {
|
||||
SableLocalPlayerPhysics.afterLocalPlayerTick((Player) (Object) this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
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;
|
||||
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.
|
||||
*/
|
||||
@Mixin(value = Player.class, priority = 4000)
|
||||
public abstract class PlayerEdgeSneakMixin {
|
||||
private static final double STEP = 0.05D;
|
||||
private static final double FLOOR_EPSILON = 1.0E-5D;
|
||||
|
||||
@Inject(method = "maybeBackOffFromEdge", at = @At("HEAD"), cancellable = true)
|
||||
private void sablegravity$localEdgeBackoff(
|
||||
Vec3 movement,
|
||||
MoverType moverType,
|
||||
CallbackInfoReturnable<Vec3> cir
|
||||
) {
|
||||
Player player = (Player) (Object) this;
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
|
||||
if (!player.isLocalPlayer()
|
||||
|| !state.sablegravity$isEnabled()
|
||||
|| player.getAbilities().flying
|
||||
|| !player.isShiftKeyDown()
|
||||
|| !SableLocalPlayerPhysics.isLocalGrounded(player)
|
||||
|| (moverType != MoverType.SELF && moverType != MoverType.PLAYER)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrame(player);
|
||||
if (frame == null || frame.isRemoved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Pose3dc pose = frame.logicalPose();
|
||||
Vec3 localMovement = pose.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
|
||||
);
|
||||
|
||||
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)) {
|
||||
x = reduceTowardsZero(x);
|
||||
}
|
||||
while (z != 0.0D && wouldSlideOff(level, player, localBounds, 0.0D, z, fallDistance)) {
|
||||
z = reduceTowardsZero(z);
|
||||
}
|
||||
while (x != 0.0D
|
||||
&& z != 0.0D
|
||||
&& wouldSlideOff(level, player, localBounds, x, z, fallDistance)) {
|
||||
x = reduceTowardsZero(x);
|
||||
z = reduceTowardsZero(z);
|
||||
}
|
||||
|
||||
cir.setReturnValue(pose.transformNormal(new Vec3(x, localMovement.y, z)));
|
||||
}
|
||||
|
||||
private static boolean wouldSlideOff(
|
||||
Level level,
|
||||
Player player,
|
||||
AABB localBounds,
|
||||
double localX,
|
||||
double localZ,
|
||||
double fallDistance
|
||||
) {
|
||||
AABB future = localBounds.move(localX, 0.0D, localZ);
|
||||
AABB floorProbe = new AABB(
|
||||
future.minX,
|
||||
future.minY - fallDistance - FLOOR_EPSILON,
|
||||
future.minZ,
|
||||
future.maxX,
|
||||
future.minY,
|
||||
future.maxZ
|
||||
);
|
||||
return level.noCollision(player, floorProbe);
|
||||
}
|
||||
|
||||
private static double reduceTowardsZero(double value) {
|
||||
if (Math.abs(value) <= STEP) {
|
||||
return 0.0D;
|
||||
}
|
||||
return value - Math.copySign(STEP, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package dev.nightly.sablegravity.mixin;
|
||||
|
||||
import dev.nightly.sablegravity.SableGravityMod;
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
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;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(Player.class)
|
||||
public abstract class PlayerGravityDataMixin implements SableGravityState {
|
||||
@Unique
|
||||
private static final EntityDataAccessor<Boolean> SABLEGRAVITY_ENABLED =
|
||||
SynchedEntityData.defineId(Player.class, EntityDataSerializers.BOOLEAN);
|
||||
|
||||
@Unique
|
||||
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);
|
||||
builder.define(SABLEGRAVITY_STRENGTH, SableGravityMod.VANILLA_STRENGTH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sablegravity$isEnabled() {
|
||||
return ((Player) (Object) this).getEntityData().get(SABLEGRAVITY_ENABLED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sablegravity$setEnabled(boolean enabled) {
|
||||
((Player) (Object) this).getEntityData().set(SABLEGRAVITY_ENABLED, enabled);
|
||||
sablegravity$localStateValid = false;
|
||||
sablegravity$frameId = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float sablegravity$getStrength() {
|
||||
return ((Player) (Object) this).getEntityData().get(SABLEGRAVITY_STRENGTH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sablegravity$setStrength(float strength) {
|
||||
((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,55 @@
|
||||
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 net.minecraft.world.phys.Vec3;
|
||||
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;
|
||||
|
||||
/** Replaces movement only for the authoritative local client player. */
|
||||
@Mixin(value = Player.class, priority = 3000)
|
||||
public abstract class PlayerLocalPhysicsMixin {
|
||||
@Inject(method = "travel", at = @At("HEAD"), cancellable = true)
|
||||
private void sablegravity$replacePlayerTravel(Vec3 input, CallbackInfo ci) {
|
||||
Player player = (Player) (Object) this;
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
|
||||
// Sable intentionally does not run SubLevel OBB collision for ServerPlayer. The local
|
||||
// client owns this kernel and Sable transforms the resulting movement packets.
|
||||
if (!player.isLocalPlayer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.sablegravity$isEnabled()) {
|
||||
SableLocalPlayerPhysics.detach(player);
|
||||
return;
|
||||
}
|
||||
if (state.sablegravity$isInLocalSimulation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Riding, elytra and fluids still use vanilla/Sable paths. Creative flight is handled by
|
||||
// the local kernel so vertical controls, inertia and collision stay in the ship basis.
|
||||
if (player.isSpectator()
|
||||
|| player.isPassenger()
|
||||
|| player.isFallFlying()
|
||||
|| player.isInWaterOrBubble()
|
||||
|| player.isInLava()) {
|
||||
SableLocalPlayerPhysics.detach(player);
|
||||
return;
|
||||
}
|
||||
|
||||
SubLevel frame = SableLocalPlayerPhysics.resolveFrame(player);
|
||||
if (frame == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SableLocalPlayerPhysics.simulate(player, input, frame)) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
package dev.nightly.sablegravity.physics;
|
||||
|
||||
import dev.nightly.sablegravity.SableGravityMod;
|
||||
import dev.nightly.sablegravity.SableGravityState;
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3dc;
|
||||
import dev.ryanhcode.sable.mixinterface.entity.entity_sublevel_collision.EntityMovementExtension;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.entity_collision.SubLevelEntityCollision;
|
||||
import net.minecraft.world.entity.MoverType;
|
||||
import net.minecraft.world.entity.player.Abilities;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.Collections;
|
||||
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.
|
||||
*/
|
||||
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 VANILLA_GRAVITY_PER_TICK = 0.08D;
|
||||
private static final double AIR_DRAG = 0.98D;
|
||||
private static final double AIR_HORIZONTAL_FRICTION = 0.91D;
|
||||
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<>());
|
||||
private static final Map<Player, Boolean> LOCAL_GROUNDED =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private static final Map<Player, Vec3> LAST_POST_TICK_VELOCITY =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private SableLocalPlayerPhysics() {}
|
||||
|
||||
/** Latches the first tracked frame and keeps it while the player is airborne or flying. */
|
||||
public static SubLevel resolveFrame(Player player) {
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
if (!state.sablegravity$isEnabled()) {
|
||||
detach(player);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
SubLevel tracked = Sable.HELPER.getTrackingSubLevel(player);
|
||||
if (tracked == null || tracked.isRemoved() || tracked.getLevel() != player.level()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public static SubLevel getAttachedFrame(Player player) {
|
||||
return resolveFrame(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 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);
|
||||
}
|
||||
}
|
||||
|
||||
/** Captures the final velocity after vanilla/Sable post-travel hooks for the next flight tick. */
|
||||
public static void afterLocalPlayerTick(Player player) {
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
if (!state.sablegravity$isEnabled() || !state.sablegravity$isLocalStateValid()) {
|
||||
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) {
|
||||
ATTACHED_FRAMES.remove(player);
|
||||
LOCAL_GROUNDED.remove(player);
|
||||
LAST_POST_TICK_VELOCITY.remove(player);
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
state.sablegravity$setLocalStateValid(false);
|
||||
state.sablegravity$setLocalPosition(Vec3.ZERO);
|
||||
state.sablegravity$setFrameId(null);
|
||||
}
|
||||
|
||||
public static boolean simulate(Player player, Vec3 input, SubLevel frame) {
|
||||
SableGravityState state = (SableGravityState) player;
|
||||
Pose3dc pose = frame.logicalPose();
|
||||
EntityMovementExtension movement = (EntityMovementExtension) player;
|
||||
Abilities abilities = player.getAbilities();
|
||||
boolean flying = abilities.flying;
|
||||
boolean groundedBeforeMove = isLocalGrounded(player);
|
||||
|
||||
Vec3 localVelocity;
|
||||
if (!state.sablegravity$isLocalStateValid()
|
||||
|| state.sablegravity$getFrameId() == null
|
||||
|| !state.sablegravity$getFrameId().equals(frame.getUniqueId())) {
|
||||
localVelocity = pose.transformNormalInverse(player.getDeltaMovement());
|
||||
state.sablegravity$setLocalVelocity(localVelocity);
|
||||
state.sablegravity$setLocalStateValid(true);
|
||||
state.sablegravity$setFrameId(frame.getUniqueId());
|
||||
state.sablegravity$setLocalPosition(pose.transformPositionInverse(player.position()));
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float acceleration;
|
||||
if (flying) {
|
||||
acceleration = abilities.getFlyingSpeed() * (player.isSprinting() ? 2.0F : 1.0F);
|
||||
} else {
|
||||
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.
|
||||
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);
|
||||
Vec3 requestedWorldMotion = pose.transformNormal(requestedLocalVelocity);
|
||||
Vec3 positionBeforeMove = player.position();
|
||||
player.setDeltaMovement(requestedWorldMotion);
|
||||
player.move(MoverType.SELF, requestedWorldMotion);
|
||||
|
||||
SubLevelEntityCollision.CollisionInfo collision = movement.sable$getCollisionInfo();
|
||||
Vec3 actualWorldMotion = player.position().subtract(positionBeforeMove);
|
||||
Vec3 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);
|
||||
|
||||
boolean localGrounded = collision != null && collision.verticalCollisionBelow;
|
||||
LOCAL_GROUNDED.put(player, !flying && localGrounded);
|
||||
|
||||
// inheritedMotion/inheritedVelocity are intentionally left to Sable's travel RETURN mixin.
|
||||
if (flying) {
|
||||
player.fallDistance = 0.0F;
|
||||
player.setOnGround(false);
|
||||
localVelocity = new Vec3(
|
||||
localVelocity.x * AIR_HORIZONTAL_FRICTION,
|
||||
localVelocity.y * FLIGHT_VERTICAL_DRAG,
|
||||
localVelocity.z * AIR_HORIZONTAL_FRICTION
|
||||
);
|
||||
} else {
|
||||
player.setOnGround(localGrounded);
|
||||
if (!player.isNoGravity()) {
|
||||
double gravity = VANILLA_GRAVITY_PER_TICK
|
||||
* (Math.max(0.0F, state.sablegravity$getStrength()) / SableGravityMod.VANILLA_STRENGTH);
|
||||
localVelocity = localVelocity.add(0.0D, -gravity, 0.0D);
|
||||
}
|
||||
|
||||
double horizontalFriction = localGrounded
|
||||
? DEFAULT_GROUND_FRICTION
|
||||
: AIR_HORIZONTAL_FRICTION;
|
||||
|
||||
localVelocity = new Vec3(
|
||||
localVelocity.x * horizontalFriction,
|
||||
localVelocity.y * AIR_DRAG,
|
||||
localVelocity.z * horizontalFriction
|
||||
);
|
||||
|
||||
// Static friction for numerical OBB residue. Real motion/knockback above this small
|
||||
// threshold remains untouched, while a motionless player no longer creeps downhill.
|
||||
double horizontalInputSqr = input.x * input.x + input.z * input.z;
|
||||
if (localGrounded
|
||||
&& horizontalInputSqr < EPSILON
|
||||
&& Math.hypot(localVelocity.x, localVelocity.z) < GROUND_STATIC_FRICTION_LIMIT) {
|
||||
localVelocity = new Vec3(0.0D, localVelocity.y, 0.0D);
|
||||
}
|
||||
}
|
||||
|
||||
updateLocalWalkAnimation(player, actualLocalMotion);
|
||||
state.sablegravity$setLocalPosition(pose.transformPositionInverse(player.position()));
|
||||
state.sablegravity$setLocalVelocity(localVelocity);
|
||||
player.setDeltaMovement(pose.transformNormal(localVelocity));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Vec3 resolveBlockedVelocity(
|
||||
Vec3 requested,
|
||||
Vec3 actualMotion,
|
||||
SubLevelEntityCollision.CollisionInfo collision
|
||||
) {
|
||||
double x = requested.x;
|
||||
double y = requested.y;
|
||||
double z = requested.z;
|
||||
|
||||
if (collision != null) {
|
||||
if (collision.verticalCollisionBelow && y < 0.0D) {
|
||||
y = 0.0D;
|
||||
} else if (collision.verticalCollision && y > 0.0D) {
|
||||
y = 0.0D;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
if (Math.signum(requested) != Math.signum(actual) && Math.abs(actual) > COLLISION_EPSILON) {
|
||||
return true;
|
||||
}
|
||||
return Math.abs(actual) + COLLISION_EPSILON < Math.abs(requested);
|
||||
}
|
||||
|
||||
private static void updateLocalWalkAnimation(Player player, Vec3 actualLocalMotion) {
|
||||
if (!player.level().isClientSide()) {
|
||||
return;
|
||||
}
|
||||
float distance = (float) Math.sqrt(
|
||||
actualLocalMotion.x * actualLocalMotion.x
|
||||
+ actualLocalMotion.z * actualLocalMotion.z
|
||||
);
|
||||
float animationSpeed = Math.min(distance * 4.0F, 1.0F);
|
||||
player.walkAnimation.update(animationSpeed, 0.4F);
|
||||
}
|
||||
|
||||
private static Vec3 getInputVector(Vec3 input, float speed, float yawDegrees) {
|
||||
double lengthSqr = input.lengthSqr();
|
||||
if (lengthSqr < EPSILON) {
|
||||
return Vec3.ZERO;
|
||||
}
|
||||
|
||||
Vec3 normalized = lengthSqr > 1.0D ? input.scale(1.0D / Math.sqrt(lengthSqr)) : input;
|
||||
normalized = normalized.scale(speed);
|
||||
double radians = yawDegrees * (Math.PI / 180.0D);
|
||||
double sin = Math.sin(radians);
|
||||
double cos = Math.cos(radians);
|
||||
|
||||
return new Vec3(
|
||||
normalized.x * cos - normalized.z * sin,
|
||||
normalized.y,
|
||||
normalized.z * cos + normalized.x * sin
|
||||
);
|
||||
}
|
||||
|
||||
private static double clamp(double value, double min, double max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
modLoader = "javafml"
|
||||
loaderVersion = "[4,)"
|
||||
license = "All Rights Reserved"
|
||||
|
||||
[[mods]]
|
||||
modId = "sablegravity"
|
||||
version = "0.6.0-test16-ground-flight-sneak"
|
||||
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.'''
|
||||
|
||||
[[mixins]]
|
||||
config = "sablegravity.mixins.json"
|
||||
|
||||
[[dependencies.sablegravity]]
|
||||
modId = "neoforge"
|
||||
type = "required"
|
||||
versionRange = "[21.1.228,)"
|
||||
ordering = "NONE"
|
||||
side = "BOTH"
|
||||
|
||||
[[dependencies.sablegravity]]
|
||||
modId = "minecraft"
|
||||
type = "required"
|
||||
versionRange = "[1.21.1]"
|
||||
ordering = "NONE"
|
||||
side = "BOTH"
|
||||
|
||||
[[dependencies.sablegravity]]
|
||||
modId = "sable"
|
||||
type = "required"
|
||||
versionRange = "[2.0.3,2.1.0)"
|
||||
ordering = "AFTER"
|
||||
side = "BOTH"
|
||||
@@ -0,0 +1 @@
|
||||
{"pack":{"description":"Sable Gravity resources","pack_format":48}}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "dev.nightly.sablegravity.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"minVersion": "0.8",
|
||||
"mixins": [
|
||||
"PlayerGravityDataMixin",
|
||||
"PlayerLocalPhysicsMixin",
|
||||
"EntityCustomOrientationMixin",
|
||||
"LivingEntityJumpMixin",
|
||||
"PlayerEdgeSneakMixin"
|
||||
],
|
||||
"client": [
|
||||
"LocalPlayerLifecycleMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Checks the relative frame used by Sable's OBB collision.
|
||||
|
||||
Sable gives a ship block OBB the SubLevel pose q_ship and gives the custom player
|
||||
OBB q_entity. For an upright player relative to the logical deck, q_entity must be
|
||||
q_ship, so inverse(q_ship) * q_entity is identity.
|
||||
"""
|
||||
import math
|
||||
|
||||
|
||||
def qx(deg):
|
||||
a = math.radians(deg) / 2.0
|
||||
return (math.cos(a), math.sin(a), 0.0, 0.0) # w,x,y,z
|
||||
|
||||
|
||||
def conj(q):
|
||||
w,x,y,z=q
|
||||
return (w,-x,-y,-z)
|
||||
|
||||
|
||||
def mul(a,b):
|
||||
aw,ax,ay,az=a; bw,bx,by,bz=b
|
||||
return (
|
||||
aw*bw-ax*bx-ay*by-az*bz,
|
||||
aw*bx+ax*bw+ay*bz-az*by,
|
||||
aw*by-ax*bz+ay*bw+az*bx,
|
||||
aw*bz+ax*by-ay*bx+az*bw,
|
||||
)
|
||||
|
||||
|
||||
def rotate(q,v):
|
||||
p=(0.0,*v)
|
||||
r=mul(mul(q,p),conj(q))
|
||||
return r[1:]
|
||||
|
||||
|
||||
def close(a,b,eps=1e-9):
|
||||
return all(abs(x-y)<eps for x,y in zip(a,b))
|
||||
|
||||
for deg in (0,20,90,180):
|
||||
ship=qx(deg)
|
||||
entity=ship
|
||||
relative=mul(conj(ship),entity)
|
||||
world_up=rotate(entity,(0.0,1.0,0.0))
|
||||
world_gravity=rotate(ship,(0.0,-1.0,0.0))
|
||||
assert close(relative,(1.0,0.0,0.0,0.0)) or close(relative,(-1.0,0.0,0.0,0.0))
|
||||
assert abs(sum(a*b for a,b in zip(world_up,world_gravity))+1.0)<1e-9
|
||||
print(f"X {deg:3d}: relative={relative}, up={world_up}, gravity={world_gravity}")
|
||||
|
||||
print("OK: player OBB stays upright relative to the logical deck at 0/20/90/180 degrees")
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression checks for local creative-flight input and changing ship orientation."""
|
||||
import math
|
||||
|
||||
|
||||
def qx(deg):
|
||||
a = math.radians(deg) / 2.0
|
||||
return (math.cos(a), math.sin(a), 0.0, 0.0) # w,x,y,z
|
||||
|
||||
|
||||
def conj(q):
|
||||
w,x,y,z=q
|
||||
return (w,-x,-y,-z)
|
||||
|
||||
|
||||
def mul(a,b):
|
||||
aw,ax,ay,az=a; bw,bx,by,bz=b
|
||||
return (
|
||||
aw*bw-ax*bx-ay*by-az*bz,
|
||||
aw*bx+ax*bw+ay*bz-az*by,
|
||||
aw*by-ax*bz+ay*bw+az*bx,
|
||||
aw*bz+ax*by-ay*bx+az*bw,
|
||||
)
|
||||
|
||||
|
||||
def rotate(q,v):
|
||||
p=(0.0,*v)
|
||||
r=mul(mul(q,p),conj(q))
|
||||
return r[1:]
|
||||
|
||||
|
||||
def add(a,b): return tuple(x+y for x,y in zip(a,b))
|
||||
def sub(a,b): return tuple(x-y for x,y in zip(a,b))
|
||||
def close(a,b,e=1e-9): return all(abs(x-y)<e for x,y in zip(a,b))
|
||||
|
||||
# Stored local velocity is authoritative. LocalPlayer.aiStep adds a local-Y flight impulse,
|
||||
# already rotated into world space by Sable. Reconciliation must recover exactly that impulse,
|
||||
# even when the frame orientation changes between ticks.
|
||||
stored_local=(0.13,-0.04,0.27)
|
||||
ascend_local=(0.0,0.15,0.0)
|
||||
for previous,current in ((0,20),(20,90),(90,180),(180,10)):
|
||||
q=qx(current)
|
||||
expected_world=rotate(q,stored_local)
|
||||
actual_world=add(expected_world,rotate(q,ascend_local))
|
||||
external_world=sub(actual_world,expected_world)
|
||||
recovered_external=rotate(conj(q),external_world)
|
||||
reconciled=add(stored_local,recovered_external)
|
||||
assert close(recovered_external,ascend_local)
|
||||
assert close(reconciled,add(stored_local,ascend_local))
|
||||
print(f"{previous:3d}->{current:3d}: recovered flight impulse {recovered_external}")
|
||||
|
||||
# A frame rotation by itself must not manufacture a flight impulse.
|
||||
for deg in (0,20,90,180):
|
||||
q=qx(deg)
|
||||
expected=rotate(q,stored_local)
|
||||
recovered=rotate(conj(q),sub(expected,expected))
|
||||
assert close(recovered,(0.0,0.0,0.0))
|
||||
|
||||
print('OK: flight input is recovered in local Y and frame rotation adds no fake acceleration')
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic basis checks for the local/world gravity convention used by test11."""
|
||||
import math
|
||||
|
||||
|
||||
def q_axis(axis, degrees):
|
||||
a = math.radians(degrees) * 0.5
|
||||
s = math.sin(a)
|
||||
return (axis[0]*s, axis[1]*s, axis[2]*s, math.cos(a))
|
||||
|
||||
|
||||
def q_conj(q):
|
||||
return (-q[0], -q[1], -q[2], q[3])
|
||||
|
||||
|
||||
def q_mul(a, b):
|
||||
ax, ay, az, aw = a; bx, by, bz, bw = b
|
||||
return (
|
||||
aw*bx + ax*bw + ay*bz - az*by,
|
||||
aw*by - ax*bz + ay*bw + az*bx,
|
||||
aw*bz + ax*by - ay*bx + az*bw,
|
||||
aw*bw - ax*bx - ay*by - az*bz,
|
||||
)
|
||||
|
||||
|
||||
def rotate(q, v):
|
||||
r = q_mul(q_mul(q, (v[0], v[1], v[2], 0.0)), q_conj(q))
|
||||
return r[:3]
|
||||
|
||||
|
||||
def close(a, b, eps=1e-9):
|
||||
return all(abs(x-y) <= eps for x, y in zip(a, b))
|
||||
|
||||
|
||||
UP = (0.0, 1.0, 0.0)
|
||||
DOWN = (0.0, -1.0, 0.0)
|
||||
for angle in (0, 20, 90, 180):
|
||||
q = q_axis((1.0, 0.0, 0.0), angle)
|
||||
world_up = rotate(q, UP)
|
||||
world_gravity = rotate(q, DOWN)
|
||||
assert close(rotate(q_conj(q), world_up), UP)
|
||||
assert close(rotate(q_conj(q), world_gravity), DOWN)
|
||||
assert close(tuple(-x for x in world_up), world_gravity)
|
||||
print(f"X {angle:3d}: up={world_up!r}, gravity={world_gravity!r}")
|
||||
|
||||
q180 = q_axis((1.0, 0.0, 0.0), 180)
|
||||
assert close(rotate(q180, UP), (0.0, -1.0, 0.0))
|
||||
assert close(rotate(q180, DOWN), (0.0, 1.0, 0.0))
|
||||
print("OK: at 180 degrees jump points world-down and gravity points world-up")
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Checks that limb animation uses ship-local displacement, not world X/Z."""
|
||||
import math
|
||||
|
||||
|
||||
def qx(deg):
|
||||
a=math.radians(deg)/2
|
||||
return (math.cos(a),math.sin(a),0.0,0.0)
|
||||
|
||||
def conj(q):
|
||||
w,x,y,z=q; return (w,-x,-y,-z)
|
||||
|
||||
def mul(a,b):
|
||||
aw,ax,ay,az=a; bw,bx,by,bz=b
|
||||
return (aw*bw-ax*bx-ay*by-az*bz,
|
||||
aw*bx+ax*bw+ay*bz-az*by,
|
||||
aw*by-ax*bz+ay*bw+az*bx,
|
||||
aw*bz+ax*by-ay*bx+az*bw)
|
||||
|
||||
def rotate(q,v):
|
||||
r=mul(mul(q,(0.0,*v)),conj(q)); return r[1:]
|
||||
|
||||
def length_xz(v): return math.sqrt(v[0]*v[0]+v[2]*v[2])
|
||||
def length_local(v): return math.sqrt(v[0]*v[0]+v[2]*v[2])
|
||||
|
||||
walk=(0.0,0.0,0.25)
|
||||
for deg in (0,20,90,180):
|
||||
world=rotate(qx(deg),walk)
|
||||
recovered=rotate(conj(qx(deg)),world)
|
||||
assert abs(length_local(recovered)-0.25)<1e-9
|
||||
print(f"X {deg:3d}: world delta={world}, local walk distance={length_local(recovered):.3f}, vanilla world-XZ={length_xz(world):.3f}")
|
||||
|
||||
# At 90 degrees around X, forward walking is world-vertical: vanilla X/Z animation would be 0.
|
||||
assert length_xz(rotate(qx(90),walk)) < 1e-9
|
||||
print('OK: local animation still walks at 90 degrees while vanilla world-XZ would freeze')
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for the test9/test10 fake-impulse bug."""
|
||||
import math
|
||||
|
||||
|
||||
def qx(deg):
|
||||
a=math.radians(deg)/2
|
||||
return (math.cos(a),math.sin(a),0.0,0.0)
|
||||
|
||||
def conj(q):
|
||||
w,x,y,z=q; return (w,-x,-y,-z)
|
||||
|
||||
def mul(a,b):
|
||||
aw,ax,ay,az=a; bw,bx,by,bz=b
|
||||
return (aw*bw-ax*bx-ay*by-az*bz,
|
||||
aw*bx+ax*bw+ay*bz-az*by,
|
||||
aw*by-ax*bz+ay*bw+az*bx,
|
||||
aw*bz+ax*by-ay*bx+az*bw)
|
||||
|
||||
def rotate(q,v):
|
||||
r=mul(mul(q,(0.0,*v)),conj(q)); return r[1:]
|
||||
|
||||
def invrotate(q,v): return rotate(conj(q),v)
|
||||
def close(a,b,e=1e-9): return all(abs(x-y)<e for x,y in zip(a,b))
|
||||
|
||||
local=(0.17,0.42,-0.08)
|
||||
for deg in (0,20,45,90,180):
|
||||
world=rotate(qx(deg),local)
|
||||
recovered=invrotate(qx(deg),world)
|
||||
assert close(recovered,local)
|
||||
print(f"X {deg:3d}: local={local} -> world={world} -> local={recovered}")
|
||||
print('OK: changing ship orientation changes world representation without changing local velocity')
|
||||
Reference in New Issue
Block a user