test16 version as base for OOB gravity
This commit is contained in:
commit
55213cb5aa
28 files changed
+1716
No files matched your search
+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.
|
||||||
Binary file not shown.
@@ -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