From a78e657e703281d367bc8f4a69a8076a41335b3f Mon Sep 17 00:00:00 2001 From: Marcel Brannahl Date: Sat, 10 Sep 2022 06:02:42 +0200 Subject: [PATCH 01/12] Smooth Zoom implemented, game crashes --- gradle.properties | 2 +- gradlew | 0 .../logical_zoom/LogicalZoom.java | 198 +++++++++++------- .../logical_zoom/mixin/LogicalZoomMixin.java | 19 +- 4 files changed, 134 insertions(+), 85 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradle.properties b/gradle.properties index 7440bf3..90ce681 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ yarn_mappings=1.19.2+build.1 loader_version=0.14.9 # Mod Properties -mod_version = 0.0.17 +mod_version = 0.1.0 maven_group = com.logicalgeekboy.logical_zoom archives_base_name = logical_zoom diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java index bc00cf4..e876547 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java @@ -1,82 +1,136 @@ package com.logicalgeekboy.logical_zoom; +import org.lwjgl.glfw.GLFW; + import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; -import net.minecraft.client.util.InputUtil; import net.minecraft.client.MinecraftClient; import net.minecraft.client.option.KeyBinding; - -import org.lwjgl.glfw.GLFW; +import net.minecraft.client.util.InputUtil; public class LogicalZoom implements ClientModInitializer { - private static boolean currentlyZoomed; - private static KeyBinding keyBinding; - private static boolean originalSmoothCameraEnabled; - private static final MinecraftClient mc = MinecraftClient.getInstance(); - - public static final double zoomLevel = 0.23; - - @Override - public void onInitializeClient() { - keyBinding = new KeyBinding("key.logical_zoom.zoom", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_C, "category.logical_zoom.zoom"); - - currentlyZoomed = false; - originalSmoothCameraEnabled = false; - - KeyBindingHelper.registerKeyBinding(keyBinding); - } - - public static boolean isZooming() { - return keyBinding.isPressed(); - } - - public static void manageSmoothCamera() { - if (zoomStarting()) { - zoomStarted(); - enableSmoothCamera(); - } - - if (zoomStopping()) { - zoomStopped(); - resetSmoothCamera(); - } - } - - private static boolean isSmoothCamera() { - return mc.options.smoothCameraEnabled; - } - - private static void enableSmoothCamera() { - mc.options.smoothCameraEnabled = true; - } - - private static void disableSmoothCamera() { - mc.options.smoothCameraEnabled = false; - } - - private static boolean zoomStarting() { - return isZooming() && !currentlyZoomed; - } - - private static boolean zoomStopping() { - return !isZooming() && currentlyZoomed; - } - - private static void zoomStarted() { - originalSmoothCameraEnabled = isSmoothCamera(); - currentlyZoomed = true; - } - - private static void zoomStopped() { - currentlyZoomed = false; - } - - private static void resetSmoothCamera() { - if (originalSmoothCameraEnabled) { - enableSmoothCamera(); - } else { - disableSmoothCamera(); - } - } + private static long lastZoomKeyActionTimestamp; + private static KeyBinding keyBinding; + private static boolean originalSmoothCameraEnabled; + private static ZoomState currentState; + + private static final MinecraftClient MC = MinecraftClient.getInstance(); + // The zoom level is a multiplier of the FOV value (in degrees) which means + // that values < 1 decrease the FOV and thus increase the zoom! + // TODO think about making configurable (#2) + private static final double ZOOM_LEVEL = 0.23; + // TODO make configurable (#2) + // better to make it a long since it's compared with System.currentTimeMillis() + private static final long SMOOTH_ZOOM_DURATION_MILLIS = 500; + + @Override + public void onInitializeClient() { + // TODO add Mod Menu config for smooth zoom on/off + duration (#2) + keyBinding = new KeyBinding("key.logical_zoom.zoom", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_C, + "category.logical_zoom.zoom"); + + lastZoomKeyActionTimestamp = 0L; + originalSmoothCameraEnabled = false; + currentState = ZoomState.NO_ZOOM; + + KeyBindingHelper.registerKeyBinding(keyBinding); + } + + private static boolean isZoomKeyPressed() { + return keyBinding.isPressed(); + } + + private static boolean hasMaxDurationPassed() { + return System.currentTimeMillis() - lastZoomKeyActionTimestamp >= SMOOTH_ZOOM_DURATION_MILLIS; + } + + public static double getCurrentZoomLevel() { + updateZoomStateAndSmoothCamera(); + double currentDurationMillis = getCurrentDuration(); + return currentState.getZoomLevelFunction().apply(ZOOM_LEVEL, SMOOTH_ZOOM_DURATION_MILLIS, + currentDurationMillis); + } + + private static void updateZoomStateAndSmoothCamera() { + if (isZoomKeyPressed()) { + switch (currentState) { + case NO_ZOOM: + originalSmoothCameraEnabled = isSmoothCameraEnabled(); + enableSmoothCamera(); + case ZOOM_OUT: + currentState = ZoomState.ZOOM_IN; + markKeyAction(); + break; + case ZOOM_IN: + if (hasMaxDurationPassed()) { + currentState = ZoomState.FULL_ZOOM; + } + break; + case FULL_ZOOM: + // do nothing + } + } else { + switch (currentState) { + case ZOOM_IN: + case FULL_ZOOM: + currentState = ZoomState.ZOOM_OUT; + markKeyAction(); + case ZOOM_OUT: + if (hasMaxDurationPassed()) { + currentState = ZoomState.NO_ZOOM; + resetSmoothCamera(); + } + break; + case NO_ZOOM: + // do nothing + } + } + } + + private static void markKeyAction() { + lastZoomKeyActionTimestamp = System.currentTimeMillis(); + } + + private static boolean isSmoothCameraEnabled() { + return MC.options.smoothCameraEnabled; + } + + private static void enableSmoothCamera() { + MC.options.smoothCameraEnabled = true; + } + + private static void resetSmoothCamera() { + MC.options.smoothCameraEnabled = originalSmoothCameraEnabled; + } + + private static double getCurrentDuration() { + return System.currentTimeMillis() - lastZoomKeyActionTimestamp; + } + + private static enum ZoomState { + + NO_ZOOM((zl, d, x) -> 1.0), ZOOM_IN((zl, d, x) -> 1 - Math.log(toEFraction(d, x)) * (1.0 - zl)), + FULL_ZOOM((zl, d, x) -> zl), ZOOM_OUT((zl, d, x) -> zl + Math.log(toEFraction(d, x)) * (1.0 - zl)); + + private final ZoomLevelFunction zoomLevelFunction; + + private ZoomState(ZoomLevelFunction zoomLevelFunction) { + this.zoomLevelFunction = zoomLevelFunction; + } + + public ZoomLevelFunction getZoomLevelFunction() { + return zoomLevelFunction; + } + + private static double toEFraction(double maxDuration, double currentDuration) { + return Math.E / maxDuration * currentDuration; + } + } + + @FunctionalInterface + private static interface ZoomLevelFunction { + + double apply(double zoomLevel, double duration, double currentDuration); + } } diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/mixin/LogicalZoomMixin.java b/src/main/java/com/logicalgeekboy/logical_zoom/mixin/LogicalZoomMixin.java index f1293b9..9734e70 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/mixin/LogicalZoomMixin.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/mixin/LogicalZoomMixin.java @@ -1,28 +1,23 @@ package com.logicalgeekboy.logical_zoom.mixin; -import com.logicalgeekboy.logical_zoom.LogicalZoom; - 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; +import com.logicalgeekboy.logical_zoom.LogicalZoom; + import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; - import net.minecraft.client.render.GameRenderer; @Environment(EnvType.CLIENT) @Mixin(GameRenderer.class) public class LogicalZoomMixin { - @Inject(method = "getFov(Lnet/minecraft/client/render/Camera;FZ)D", at = @At("RETURN"), cancellable = true) - public void getZoomLevel(CallbackInfoReturnable callbackInfo) { - if(LogicalZoom.isZooming()) { - double fov = callbackInfo.getReturnValue(); - callbackInfo.setReturnValue(fov * LogicalZoom.zoomLevel); - } - - LogicalZoom.manageSmoothCamera(); - } + @Inject(method = "getFov(Lnet/minecraft/client/render/Camera;FZ)D", at = @At("RETURN"), cancellable = true) + public void getZoomLevel(CallbackInfoReturnable callbackInfo) { + double fov = callbackInfo.getReturnValue(); + callbackInfo.setReturnValue(fov * LogicalZoom.getCurrentZoomLevel()); + } } From bb379bfda6aae3baaab580af004e87cad00964b2 Mon Sep 17 00:00:00 2001 From: Marcel Brannahl Date: Sat, 10 Sep 2022 08:09:08 +0200 Subject: [PATCH 02/12] Update All The Things, still crashing at startup --- .gitignore | 6 +- build.gradle | 2 +- gradle.properties | 4 +- gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 268 ++++++++++++++--------- gradlew.bat | 191 ++++++++-------- logical_zoom_client.launch | 20 ++ logical_zoom_server.launch | 20 ++ settings.gradle | 2 +- 10 files changed, 301 insertions(+), 214 deletions(-) create mode 100644 logical_zoom_client.launch create mode 100644 logical_zoom_server.launch diff --git a/.gitignore b/.gitignore index 9563893..8da6f39 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,8 @@ bin/ # fabric -run/ \ No newline at end of file +run/ + +# old files + +*.old diff --git a/build.gradle b/build.gradle index 68661f8..a470381 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'fabric-loom' version '0.12-SNAPSHOT' + id 'fabric-loom' version '1.0-SNAPSHOT' id 'maven-publish' } diff --git a/gradle.properties b/gradle.properties index 90ce681..7e8d9e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx4G # Fabric Properties from the Fabric insaller minecraft_version=1.19.2 -yarn_mappings=1.19.2+build.1 +yarn_mappings=1.19.2+build.9 loader_version=0.14.9 # Mod Properties @@ -12,4 +12,4 @@ maven_group = com.logicalgeekboy.logical_zoom archives_base_name = logical_zoom # Fabric API version -fabric_version=0.58.6+1.19.2 +fabric_version=0.60.0+1.19.2 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 27732 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 G>;C}_dM#G~ delta 22714 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 \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +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 -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # 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"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +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 - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # 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" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || 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 @@ -105,84 +140,101 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -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" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + 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 - i=$((i+1)) + # 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 - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# 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. +# -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 9618d8d..53a6b23 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,100 +1,91 @@ -@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 - -@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=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@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%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="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! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@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 + +@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=. +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. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +: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 diff --git a/logical_zoom_client.launch b/logical_zoom_client.launch new file mode 100644 index 0000000..8728f89 --- /dev/null +++ b/logical_zoom_client.launch @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/logical_zoom_server.launch b/logical_zoom_server.launch new file mode 100644 index 0000000..a768090 --- /dev/null +++ b/logical_zoom_server.launch @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index 5b60df3..27e3d10 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ pluginManagement { repositories { - jcenter() + mavenCentral() maven { name = 'Fabric' url = 'https://maven.fabricmc.net/' From 231044346feafd65fe4b8608cc64d93d32454c16 Mon Sep 17 00:00:00 2001 From: Marcel Brannahl Date: Sat, 10 Sep 2022 10:09:18 +0200 Subject: [PATCH 03/12] Fine Tuning of the log function #1 --- .../logical_zoom/LogicalZoom.java | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java index e876547..ae0d8e9 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java @@ -22,7 +22,7 @@ public class LogicalZoom implements ClientModInitializer { private static final double ZOOM_LEVEL = 0.23; // TODO make configurable (#2) // better to make it a long since it's compared with System.currentTimeMillis() - private static final long SMOOTH_ZOOM_DURATION_MILLIS = 500; + private static final long SMOOTH_ZOOM_DURATION_MILLIS = 170; @Override public void onInitializeClient() { @@ -110,8 +110,15 @@ private static double getCurrentDuration() { private static enum ZoomState { - NO_ZOOM((zl, d, x) -> 1.0), ZOOM_IN((zl, d, x) -> 1 - Math.log(toEFraction(d, x)) * (1.0 - zl)), - FULL_ZOOM((zl, d, x) -> zl), ZOOM_OUT((zl, d, x) -> zl + Math.log(toEFraction(d, x)) * (1.0 - zl)); + NO_ZOOM((zl, md, x) -> 1.0), ZOOM_IN((zl, md, x) -> 1 - logAdjusted(zl, md, x)), FULL_ZOOM((zl, md, x) -> zl), + ZOOM_OUT((zl, md, x) -> zl + logAdjusted(zl, md, x)); + + private static final double Y_MIN = -3.0; + private static final double Y_MAX = 3.0; + private static final double Y_RANGE = Y_MAX - Y_MIN; + private static final double X_MIN = Math.pow(Math.E, Y_MIN); + private static final double X_MAX = Math.pow(Math.E, Y_MAX); + private static final double X_RANGE = X_MAX - X_MIN; private final ZoomLevelFunction zoomLevelFunction; @@ -123,14 +130,18 @@ public ZoomLevelFunction getZoomLevelFunction() { return zoomLevelFunction; } - private static double toEFraction(double maxDuration, double currentDuration) { - return Math.E / maxDuration * currentDuration; + private static double logAdjusted(double zoomLevel, double maxDuration, double currentDuration) { + return (Math.log(toDomain(maxDuration, currentDuration)) - Y_MIN) / (Y_RANGE) * (1.0 - zoomLevel); + } + + private static double toDomain(double maxDuration, double currentDuration) { + return X_RANGE / maxDuration * currentDuration + X_MIN; } } @FunctionalInterface private static interface ZoomLevelFunction { - double apply(double zoomLevel, double duration, double currentDuration); + double apply(double zoomLevel, double maxDuration, double currentDuration); } } From e56ae20a646959a48cf16529e25984fab0f720f4 Mon Sep 17 00:00:00 2001 From: Marcel Brannahl Date: Sat, 10 Sep 2022 10:32:02 +0200 Subject: [PATCH 04/12] Reset smooth camera as soon as key is released #1 --- .../java/com/logicalgeekboy/logical_zoom/LogicalZoom.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java index ae0d8e9..a623107 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java @@ -22,7 +22,7 @@ public class LogicalZoom implements ClientModInitializer { private static final double ZOOM_LEVEL = 0.23; // TODO make configurable (#2) // better to make it a long since it's compared with System.currentTimeMillis() - private static final long SMOOTH_ZOOM_DURATION_MILLIS = 170; + private static final long SMOOTH_ZOOM_DURATION_MILLIS = 3000; // 170; @Override public void onInitializeClient() { @@ -76,10 +76,11 @@ private static void updateZoomStateAndSmoothCamera() { case FULL_ZOOM: currentState = ZoomState.ZOOM_OUT; markKeyAction(); + resetSmoothCamera(); + break; case ZOOM_OUT: if (hasMaxDurationPassed()) { currentState = ZoomState.NO_ZOOM; - resetSmoothCamera(); } break; case NO_ZOOM: From 21a926444ac2cf8a5aa69436222f2f16b0196888 Mon Sep 17 00:00:00 2001 From: Marcel Brannahl Date: Sat, 10 Sep 2022 11:16:56 +0200 Subject: [PATCH 05/12] Fixed jumpy behaviour for partial zooms, closes #1 --- .../logical_zoom/LogicalZoom.java | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java index a623107..9d3e4da 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java @@ -22,7 +22,7 @@ public class LogicalZoom implements ClientModInitializer { private static final double ZOOM_LEVEL = 0.23; // TODO make configurable (#2) // better to make it a long since it's compared with System.currentTimeMillis() - private static final long SMOOTH_ZOOM_DURATION_MILLIS = 3000; // 170; + private static final long SMOOTH_ZOOM_DURATION_MILLIS = 170; @Override public void onInitializeClient() { @@ -42,7 +42,7 @@ private static boolean isZoomKeyPressed() { } private static boolean hasMaxDurationPassed() { - return System.currentTimeMillis() - lastZoomKeyActionTimestamp >= SMOOTH_ZOOM_DURATION_MILLIS; + return getCurrentRemainingDuration() <= 0.0; } public static double getCurrentZoomLevel() { @@ -56,11 +56,10 @@ private static void updateZoomStateAndSmoothCamera() { if (isZoomKeyPressed()) { switch (currentState) { case NO_ZOOM: - originalSmoothCameraEnabled = isSmoothCameraEnabled(); - enableSmoothCamera(); + initZoomIn(0L); + break; case ZOOM_OUT: - currentState = ZoomState.ZOOM_IN; - markKeyAction(); + initZoomIn(getCurrentRemainingDuration()); break; case ZOOM_IN: if (hasMaxDurationPassed()) { @@ -72,11 +71,11 @@ private static void updateZoomStateAndSmoothCamera() { } } else { switch (currentState) { - case ZOOM_IN: case FULL_ZOOM: - currentState = ZoomState.ZOOM_OUT; - markKeyAction(); - resetSmoothCamera(); + initZoomOut(0L); + break; + case ZOOM_IN: + initZoomOut(getCurrentRemainingDuration()); break; case ZOOM_OUT: if (hasMaxDurationPassed()) { @@ -89,8 +88,21 @@ private static void updateZoomStateAndSmoothCamera() { } } - private static void markKeyAction() { - lastZoomKeyActionTimestamp = System.currentTimeMillis(); + private static void initZoomIn(long offset) { + markKeyEvent(offset); + originalSmoothCameraEnabled = isSmoothCameraEnabled(); + enableSmoothCamera(); + currentState = ZoomState.ZOOM_IN; + } + + private static void initZoomOut(long offset) { + markKeyEvent(offset); + resetSmoothCamera(); + currentState = ZoomState.ZOOM_OUT; + } + + private static void markKeyEvent(long offset) { + lastZoomKeyActionTimestamp = System.currentTimeMillis() - offset; } private static boolean isSmoothCameraEnabled() { @@ -105,10 +117,14 @@ private static void resetSmoothCamera() { MC.options.smoothCameraEnabled = originalSmoothCameraEnabled; } - private static double getCurrentDuration() { + private static long getCurrentDuration() { return System.currentTimeMillis() - lastZoomKeyActionTimestamp; } + private static long getCurrentRemainingDuration() { + return SMOOTH_ZOOM_DURATION_MILLIS - getCurrentDuration(); + } + private static enum ZoomState { NO_ZOOM((zl, md, x) -> 1.0), ZOOM_IN((zl, md, x) -> 1 - logAdjusted(zl, md, x)), FULL_ZOOM((zl, md, x) -> zl), From d1108d27432d6f3a4546258b8fddaf6999417687 Mon Sep 17 00:00:00 2001 From: Marcel Brannahl Date: Wed, 14 Sep 2022 14:12:06 +0200 Subject: [PATCH 06/12] Include mc version in mod version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7e8d9e3..78791bc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ yarn_mappings=1.19.2+build.9 loader_version=0.14.9 # Mod Properties -mod_version = 0.1.0 +mod_version = 0.1.0+1.19.2 maven_group = com.logicalgeekboy.logical_zoom archives_base_name = logical_zoom From 5508c1baaca6626d8c28167c05d407c0bacaecd3 Mon Sep 17 00:00:00 2001 From: Marcel Brannahl Date: Wed, 14 Sep 2022 22:02:13 +0200 Subject: [PATCH 07/12] Update build files, add Mod Menu integration #2 --- build.gradle | 15 ++- gradle.properties | 3 + logical_zoom_client.launch | 7 +- logical_zoom_server.launch | 7 +- .../logical_zoom/config/ConfigHandler.java | 110 ++++++++++++++++++ .../logical_zoom/config/ConfigScreen.java | 48 ++++++++ .../logical_zoom/config/ConfigUtil.java | 58 +++++++++ .../assets/logical_zoom/lang/en_us.json | 5 +- src/main/resources/fabric.mod.json | 3 + 9 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java create mode 100644 src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java create mode 100644 src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java diff --git a/build.gradle b/build.gradle index a470381..5e8fbbf 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,11 @@ repositories { // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. // See https://docs.gradle.org/current/userguide/declaring_repositories.html // for more information about repositories. + + // Cloth Config + maven { url "https://maven.shedaniel.me/" } + // Mod Menu + maven { url "https://maven.terraformersmc.com/releases/" } } dependencies { @@ -26,6 +31,14 @@ dependencies { // Fabric API. This is technically optional, but you probably want it anyway. modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + + // Cloth Config API. Needed for Mod Menu compatibility + modApi ("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { + exclude(group: "net.fabricmc.fabric-api") + } + modApi ("com.terraformersmc:modmenu:${project.mod_menu_version}") { + exclude(group: "net.fabricmc.fabric-api") + } } processResources { @@ -45,7 +58,7 @@ java { // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task // if it is present. // If you remove this line, sources will not be generated. - withSourcesJar() + // withSourcesJar() } jar { diff --git a/gradle.properties b/gradle.properties index 78791bc..fcb1ad0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,3 +13,6 @@ archives_base_name = logical_zoom # Fabric API version fabric_version=0.60.0+1.19.2 + +cloth_config_version=8.2.88 +mod_menu_version=4.0.6 \ No newline at end of file diff --git a/logical_zoom_client.launch b/logical_zoom_client.launch index 8728f89..469c7d7 100644 --- a/logical_zoom_client.launch +++ b/logical_zoom_client.launch @@ -6,15 +6,12 @@ - - - + + - - diff --git a/logical_zoom_server.launch b/logical_zoom_server.launch index a768090..717f759 100644 --- a/logical_zoom_server.launch +++ b/logical_zoom_server.launch @@ -6,15 +6,12 @@ - - - + + - - diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java new file mode 100644 index 0000000..f59a03f --- /dev/null +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java @@ -0,0 +1,110 @@ +package com.logicalgeekboy.logical_zoom.config; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Optional; +import java.util.Properties; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.util.InputUtil.Key; +import net.minecraft.text.Text; + +public class ConfigHandler { + + private static final File CONFIG_FILE = new File(ConfigUtil.CONFIG_FILE_NAME); + private static final ConfigHandler INSTANCE = new ConfigHandler(); + + private final ClientPlayerEntity player; + + private Properties properties; + + @SuppressWarnings("resource") // Minecraft client is auto-closable but must not be closed by us. + private ConfigHandler() { + this.player = MinecraftClient.getInstance().player; + this.properties = new Properties(); + loadProperties(); + } + + static ConfigHandler getInstance() { + return INSTANCE; + } + + double getZoomFactor() { + String property = (String) this.properties.getOrDefault(ConfigUtil.OPTION_ZOOM_FACTOR, + ConfigUtil.DEFAULT_ENABLE_SMOOTH_ZOOM); + return Double.parseDouble(property); + } + + Key getZoomKey() { + return ConfigUtil.getKeyFromCode( + (String) this.properties.getOrDefault(ConfigUtil.OPTION_ZOOM_KEY, ConfigUtil.DEFAULT_ZOOM_KEY)); + } + + boolean isSmoothZoomEnabled() { + return "true".equals(this.properties.getProperty(ConfigUtil.OPTION_ENABLE_SMOOTH_ZOOM)); + } + + void setZoomFactor(double zoomFactor) { + this.properties.put(ConfigUtil.OPTION_ZOOM_FACTOR, Double.toString(zoomFactor)); + } + + void setZoomKey(Key zoomKey) { + this.properties.put(ConfigUtil.OPTION_ZOOM_KEY, Integer.toString(zoomKey.getCode())); + } + + void setSmoothZoomEnabled(boolean isSmoothZoomEnabled) { + this.properties.put(ConfigUtil.OPTION_ENABLE_SMOOTH_ZOOM, Boolean.toString(isSmoothZoomEnabled)); + } + + Optional getZoomFactorError(double zoomFactor) { + if (zoomFactor < ConfigUtil.MIN_ZOOM_FACTOR) { + return Optional.of(Text.translatable(ConfigUtil.ERROR_ZOOM_FACTOR_TOO_SMALL)); + } else if (zoomFactor > ConfigUtil.MAX_ZOOM_FACTOR) { + return Optional.of(Text.translatable(ConfigUtil.ERROR_ZOOM_FACTOR_TOO_LARGE)); + } + + return Optional.empty(); + } + + @SuppressWarnings("resource") // Minecraft client is auto-closable but must not be closed by us. + private void loadProperties() { + if (!CONFIG_FILE.exists()) { + loadDefaultProperties(); + return; + } + + try (InputStream is = new FileInputStream(CONFIG_FILE)) { + this.properties.load(is); + } catch (IOException e) { + this.player.sendMessage(Text.translatable(ConfigUtil.ERROR_CONFIG_FILE_READ)); + loadDefaultProperties(); + } + + } + + private void loadDefaultProperties() { + properties.put(ConfigUtil.OPTION_ENABLE_SMOOTH_ZOOM, ConfigUtil.DEFAULT_ENABLE_SMOOTH_ZOOM); + properties.put(ConfigUtil.OPTION_ZOOM_FACTOR, ConfigUtil.DEFAULT_ZOOM_FACTOR); + properties.put(ConfigUtil.OPTION_ZOOM_KEY, ConfigUtil.DEFAULT_ZOOM_KEY); + + // since the default properties are only loaded if the properties file does not + // exist or cannot be accessed for whatever reason, let's create/overwrite the + // file. + saveProperties(); + } + + @SuppressWarnings("resource") // Minecraft client is auto-closable but must not be closed by us. + void saveProperties() { + try (OutputStream out = new FileOutputStream(CONFIG_FILE)) { + // TODO check if the file is overwritten or the contents appended + this.properties.store(out, null); + } catch (IOException e) { + this.player.sendMessage(Text.translatable(ConfigUtil.ERROR_CONFIG_FILE_WRITE)); + } + } +} diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java new file mode 100644 index 0000000..372ce0b --- /dev/null +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java @@ -0,0 +1,48 @@ +package com.logicalgeekboy.logical_zoom.config; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; + +import me.shedaniel.clothconfig2.api.ConfigBuilder; +import me.shedaniel.clothconfig2.api.ConfigCategory; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; + +public class ConfigScreen implements ModMenuApi { + + private static final ConfigHandler HANDLER = ConfigHandler.getInstance(); + + public static Screen createConfigScreen(Screen parent) { + ConfigBuilder builder = ConfigBuilder.create().setParentScreen(parent) + .setTitle(Text.translatable(ConfigUtil.MENU_TITLE)).setSavingRunnable(HANDLER::saveProperties); + + ConfigCategory general = builder.getOrCreateCategory(Text.translatable(ConfigUtil.CATEGORY_GENERAL)); + + // add zoom key field + general.addEntry(builder.entryBuilder() + .startKeyCodeField(Text.translatable(ConfigUtil.OPTION_ZOOM_KEY), HANDLER.getZoomKey()) + .setDefaultValue(ConfigUtil.getDefaultZoomKey()).setKeySaveConsumer(HANDLER::setZoomKey) + .setTooltip(Text.translatable(ConfigUtil.OPTION_ZOOM_KEY)).build()); + + // add zoom factor field (double value) + general.addEntry(builder.entryBuilder() + .startDoubleField(Text.translatable(ConfigUtil.OPTION_ZOOM_FACTOR), HANDLER.getZoomFactor()) + .setDefaultValue(Double.parseDouble(ConfigUtil.DEFAULT_ZOOM_FACTOR)) + .setSaveConsumer(HANDLER::setZoomFactor).setMin(ConfigUtil.MIN_ZOOM_FACTOR) + .setMax(ConfigUtil.MAX_ZOOM_FACTOR).setTooltip(Text.translatable(ConfigUtil.TOOLTIP_ZOOM_FACTOR)) + .setErrorSupplier(HANDLER::getZoomFactorError).build()); + + general.addEntry(builder.entryBuilder() + .startBooleanToggle(Text.translatable(ConfigUtil.OPTION_ENABLE_SMOOTH_ZOOM), + HANDLER.isSmoothZoomEnabled()) + .setDefaultValue(ConfigUtil.getDefaultEnableSmoothZoom()).setSaveConsumer(HANDLER::setSmoothZoomEnabled) + .build()); + + return builder.build(); + } + + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return ConfigScreen::createConfigScreen; + } +} diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java new file mode 100644 index 0000000..133c691 --- /dev/null +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java @@ -0,0 +1,58 @@ +package com.logicalgeekboy.logical_zoom.config; + +import org.lwjgl.glfw.GLFW; + +import net.minecraft.client.util.InputUtil; + +public class ConfigUtil { + + static final String NAMESPACE = "logical_zoom"; + + static final String OPTION_PREFIX = NAMESPACE + ".option"; + static final String TOOLTIP_PREFIX = NAMESPACE + ".tooltip"; + static final String WARN_PREFIX = NAMESPACE + ".warn"; + static final String ERROR_PREFIX = NAMESPACE + ".error"; + + static final String CATEGORY_GENERAL = NAMESPACE + ".category.general"; + static final String MENU_TITLE = NAMESPACE + ".menu.title"; + + static final String OPTION_ZOOM_FACTOR = OPTION_PREFIX + ".zoom_factor"; + static final String OPTION_ZOOM_KEY = OPTION_PREFIX + ".zoom_key"; + static final String OPTION_ENABLE_SMOOTH_ZOOM = OPTION_PREFIX + ".toggle_smooth_zoom"; + + static final String DEFAULT_ZOOM_FACTOR = "5.0"; + static final double MIN_ZOOM_FACTOR = 1.0; + static final double MAX_ZOOM_FACTOR = 5.0; + static final String DEFAULT_ZOOM_KEY = Integer.toString(GLFW.GLFW_KEY_C); + static final String DEFAULT_ENABLE_SMOOTH_ZOOM = "true"; + + static final String TOOLTIP_ZOOM_FACTOR = TOOLTIP_PREFIX + ".zoom_factor"; + static final String TOOLTIP_ZOOM_KEY = TOOLTIP_PREFIX + ".zoom_key"; + static final String TOOLTIP_ENABLE_SMOOTH_ZOOM = TOOLTIP_PREFIX + ".toggle_smooth_zoom"; + + static final String CONFIG_FILE_NAME = "config/logical_zoom.properties"; + static final String ERROR_CONFIG_FILE_READ = ERROR_PREFIX + ".config_file_read"; + static final String ERROR_CONFIG_FILE_WRITE = ERROR_PREFIX + ".config_file_write"; + static final String ERROR_ZOOM_FACTOR_TOO_SMALL = ERROR_PREFIX + ".zoom_factor_too_small"; + static final String ERROR_ZOOM_FACTOR_TOO_LARGE = ERROR_PREFIX + ".zoom_factor_too_large"; + + static InputUtil.Key getDefaultZoomKey() { + return getKeyFromCode(DEFAULT_ZOOM_KEY); + } + + static InputUtil.Key getKeyFromCode(String property) { + return getKeyFromCode(Integer.parseInt(property)); + } + + static InputUtil.Key getKeyFromCode(int keyCode) { + return InputUtil.Type.KEYSYM.createFromCode(keyCode); + } + + static double getDefaultZoomFactor() { + return Double.parseDouble(DEFAULT_ZOOM_FACTOR); + } + + static boolean getDefaultEnableSmoothZoom() { + return Boolean.parseBoolean(DEFAULT_ENABLE_SMOOTH_ZOOM); + } +} diff --git a/src/main/resources/assets/logical_zoom/lang/en_us.json b/src/main/resources/assets/logical_zoom/lang/en_us.json index e288eb9..a109f3c 100644 --- a/src/main/resources/assets/logical_zoom/lang/en_us.json +++ b/src/main/resources/assets/logical_zoom/lang/en_us.json @@ -1,4 +1,7 @@ { "key.logical_zoom.zoom": "Toggle Zoom", - "category.logical_zoom.zoom": "Logical Zoom" + "category.logical_zoom.zoom": "Logical Zoom", + "title.logical_zoom.menu": "Logical Zoom Settings", + "option.logical_zoom.toggle_smooth_zoom": "", + "tooltip.logical_zoom.toggle_smooth_zoom": "" } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 084e444..9cc973f 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -20,6 +20,9 @@ "entrypoints": { "client": [ "com.logicalgeekboy.logical_zoom.LogicalZoom" + ], + "modmenu": [ + "com.logicalgeekboy.logical_zoom.config.ConfigScreen" ] }, "mixins": [ From 1787fd093d8fe385de8f583c7ea605f957ffeebc Mon Sep 17 00:00:00 2001 From: Marcel Brannahl Date: Fri, 16 Sep 2022 01:53:45 +0200 Subject: [PATCH 08/12] Config functionality fully implemented #2 --- .../logical_zoom/LogicalZoom.java | 83 +++++++++++-------- .../logical_zoom/config/ConfigHandler.java | 45 ++++++++-- .../logical_zoom/config/ConfigScreen.java | 21 +++-- .../logical_zoom/config/ConfigUtil.java | 73 +++++++++------- .../assets/logical_zoom/lang/en_us.json | 20 +++-- 5 files changed, 161 insertions(+), 81 deletions(-) diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java index 9d3e4da..92aabaf 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java @@ -1,6 +1,7 @@ package com.logicalgeekboy.logical_zoom; -import org.lwjgl.glfw.GLFW; +import com.logicalgeekboy.logical_zoom.config.ConfigHandler; +import com.logicalgeekboy.logical_zoom.config.ConfigUtil; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; @@ -11,47 +12,48 @@ public class LogicalZoom implements ClientModInitializer { private static long lastZoomKeyActionTimestamp; - private static KeyBinding keyBinding; + private static KeyBinding zoomKeyBinding; private static boolean originalSmoothCameraEnabled; private static ZoomState currentState; private static final MinecraftClient MC = MinecraftClient.getInstance(); - // The zoom level is a multiplier of the FOV value (in degrees) which means - // that values < 1 decrease the FOV and thus increase the zoom! - // TODO think about making configurable (#2) - private static final double ZOOM_LEVEL = 0.23; - // TODO make configurable (#2) - // better to make it a long since it's compared with System.currentTimeMillis() - private static final long SMOOTH_ZOOM_DURATION_MILLIS = 170; + private static final ConfigHandler HANDLER = ConfigHandler.getInstance(); @Override public void onInitializeClient() { // TODO add Mod Menu config for smooth zoom on/off + duration (#2) - keyBinding = new KeyBinding("key.logical_zoom.zoom", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_C, - "category.logical_zoom.zoom"); + zoomKeyBinding = new KeyBinding(ConfigUtil.OPTION_ZOOM_KEY, InputUtil.Type.KEYSYM, HANDLER.getZoomKeyCode(), + ConfigUtil.CATEGORY_ZOOM_KEY); lastZoomKeyActionTimestamp = 0L; originalSmoothCameraEnabled = false; currentState = ZoomState.NO_ZOOM; - KeyBindingHelper.registerKeyBinding(keyBinding); + KeyBindingHelper.registerKeyBinding(zoomKeyBinding); + } + + public static double getCurrentZoomLevel() { + updateZoomStateAndSmoothCamera(); + double currentDurationMillis = getCurrentDuration(); + return currentState.getZoomFactorFunction().apply(1 / HANDLER.getZoomFactor(), + HANDLER.getSmoothZoomDurationMillis(), currentDurationMillis); + } + + public static void updateZoomKeyBinding(InputUtil.Key zoomKey) { + // TODO can we make this a thing solely managed by Mod Menu somehow? This seems + // a bit hacky. + zoomKeyBinding.setBoundKey(zoomKey); + KeyBinding.updateKeysByCode(); } private static boolean isZoomKeyPressed() { - return keyBinding.isPressed(); + return zoomKeyBinding.isPressed(); } private static boolean hasMaxDurationPassed() { return getCurrentRemainingDuration() <= 0.0; } - public static double getCurrentZoomLevel() { - updateZoomStateAndSmoothCamera(); - double currentDurationMillis = getCurrentDuration(); - return currentState.getZoomLevelFunction().apply(ZOOM_LEVEL, SMOOTH_ZOOM_DURATION_MILLIS, - currentDurationMillis); - } - private static void updateZoomStateAndSmoothCamera() { if (isZoomKeyPressed()) { switch (currentState) { @@ -92,13 +94,13 @@ private static void initZoomIn(long offset) { markKeyEvent(offset); originalSmoothCameraEnabled = isSmoothCameraEnabled(); enableSmoothCamera(); - currentState = ZoomState.ZOOM_IN; + currentState = HANDLER.isSmoothZoomEnabled() ? ZoomState.ZOOM_IN : ZoomState.FULL_ZOOM; } private static void initZoomOut(long offset) { markKeyEvent(offset); resetSmoothCamera(); - currentState = ZoomState.ZOOM_OUT; + currentState = HANDLER.isSmoothZoomEnabled() ? ZoomState.ZOOM_OUT : ZoomState.NO_ZOOM; } private static void markKeyEvent(long offset) { @@ -122,33 +124,46 @@ private static long getCurrentDuration() { } private static long getCurrentRemainingDuration() { - return SMOOTH_ZOOM_DURATION_MILLIS - getCurrentDuration(); + return HANDLER.getSmoothZoomDurationMillis() - getCurrentDuration(); } private static enum ZoomState { - NO_ZOOM((zl, md, x) -> 1.0), ZOOM_IN((zl, md, x) -> 1 - logAdjusted(zl, md, x)), FULL_ZOOM((zl, md, x) -> zl), - ZOOM_OUT((zl, md, x) -> zl + logAdjusted(zl, md, x)); - + /** + * No zoom. The zoom factor function always returns 1. + */ + NO_ZOOM((zf, md, x) -> 1.0), + /** + * Zooming in. The zoom factor function returns a value + */ + ZOOM_IN((zf, md, x) -> 1 - logAdjusted(zf, md, x)), FULL_ZOOM((zf, md, x) -> zf), + ZOOM_OUT((zf, md, x) -> zf + logAdjusted(zf, md, x)); + + // The y range influences the slope of the zoom factor function especially near + // x_min and x_max. + // The lower y_min is, the steeper the slope is near x_min. + // The higher y_max is, the more shallow the slope is near x_max. private static final double Y_MIN = -3.0; private static final double Y_MAX = 3.0; private static final double Y_RANGE = Y_MAX - Y_MIN; + // the min and max x values equal e^y_min and e^y_max respectively because we + // want the logarithmic function to produce output values between 0 and 1. private static final double X_MIN = Math.pow(Math.E, Y_MIN); private static final double X_MAX = Math.pow(Math.E, Y_MAX); private static final double X_RANGE = X_MAX - X_MIN; - private final ZoomLevelFunction zoomLevelFunction; + private final ZoomFactorFunction zoomFactorFunction; - private ZoomState(ZoomLevelFunction zoomLevelFunction) { - this.zoomLevelFunction = zoomLevelFunction; + private ZoomState(ZoomFactorFunction zoomFactorFunction) { + this.zoomFactorFunction = zoomFactorFunction; } - public ZoomLevelFunction getZoomLevelFunction() { - return zoomLevelFunction; + public ZoomFactorFunction getZoomFactorFunction() { + return zoomFactorFunction; } - private static double logAdjusted(double zoomLevel, double maxDuration, double currentDuration) { - return (Math.log(toDomain(maxDuration, currentDuration)) - Y_MIN) / (Y_RANGE) * (1.0 - zoomLevel); + private static double logAdjusted(double zoomFactor, double maxDuration, double currentDuration) { + return (Math.log(toDomain(maxDuration, currentDuration)) - Y_MIN) / (Y_RANGE) * (1.0 - zoomFactor); } private static double toDomain(double maxDuration, double currentDuration) { @@ -157,7 +172,7 @@ private static double toDomain(double maxDuration, double currentDuration) { } @FunctionalInterface - private static interface ZoomLevelFunction { + private static interface ZoomFactorFunction { double apply(double zoomLevel, double maxDuration, double currentDuration); } diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java index f59a03f..c08af65 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java @@ -9,8 +9,11 @@ import java.util.Optional; import java.util.Properties; +import com.logicalgeekboy.logical_zoom.LogicalZoom; + import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.util.InputUtil; import net.minecraft.client.util.InputUtil.Key; import net.minecraft.text.Text; @@ -30,38 +33,52 @@ private ConfigHandler() { loadProperties(); } - static ConfigHandler getInstance() { + public static ConfigHandler getInstance() { return INSTANCE; } - double getZoomFactor() { + public double getZoomFactor() { String property = (String) this.properties.getOrDefault(ConfigUtil.OPTION_ZOOM_FACTOR, ConfigUtil.DEFAULT_ENABLE_SMOOTH_ZOOM); return Double.parseDouble(property); } - Key getZoomKey() { - return ConfigUtil.getKeyFromCode( + public InputUtil.Key getZoomKey() { + return ConfigUtil.getKeyFromCode(Integer.toString(getZoomKeyCode())); + } + + public int getZoomKeyCode() { + return Integer.parseInt( (String) this.properties.getOrDefault(ConfigUtil.OPTION_ZOOM_KEY, ConfigUtil.DEFAULT_ZOOM_KEY)); } - boolean isSmoothZoomEnabled() { + public boolean isSmoothZoomEnabled() { return "true".equals(this.properties.getProperty(ConfigUtil.OPTION_ENABLE_SMOOTH_ZOOM)); } - void setZoomFactor(double zoomFactor) { + public long getSmoothZoomDurationMillis() { + return Long.parseLong((String) this.properties.getOrDefault(ConfigUtil.OPTION_SMOOTH_ZOOM_DURATION_MILLIS, + ConfigUtil.DEFAULT_SMOOTH_ZOOM_DURATION_MILLIS)); + } + + public void setZoomFactor(double zoomFactor) { this.properties.put(ConfigUtil.OPTION_ZOOM_FACTOR, Double.toString(zoomFactor)); } - void setZoomKey(Key zoomKey) { + public void setZoomKey(Key zoomKey) { this.properties.put(ConfigUtil.OPTION_ZOOM_KEY, Integer.toString(zoomKey.getCode())); + LogicalZoom.updateZoomKeyBinding(zoomKey); } - void setSmoothZoomEnabled(boolean isSmoothZoomEnabled) { + public void setSmoothZoomEnabled(boolean isSmoothZoomEnabled) { this.properties.put(ConfigUtil.OPTION_ENABLE_SMOOTH_ZOOM, Boolean.toString(isSmoothZoomEnabled)); } - Optional getZoomFactorError(double zoomFactor) { + public void setSmoothZoomDurationMillis(long millis) { + this.properties.put(ConfigUtil.OPTION_SMOOTH_ZOOM_DURATION_MILLIS, Long.toString(millis)); + } + + public Optional getZoomFactorError(double zoomFactor) { if (zoomFactor < ConfigUtil.MIN_ZOOM_FACTOR) { return Optional.of(Text.translatable(ConfigUtil.ERROR_ZOOM_FACTOR_TOO_SMALL)); } else if (zoomFactor > ConfigUtil.MAX_ZOOM_FACTOR) { @@ -71,6 +88,16 @@ Optional getZoomFactorError(double zoomFactor) { return Optional.empty(); } + public Optional getSmoothZoomDurationMillisError(long millis) { + if (millis < ConfigUtil.MIN_SMOOTH_ZOOM_DURATION_MILLIS) { + return Optional.of(Text.translatable(ConfigUtil.ERROR_SMOOTH_ZOOM_DURATION_MILLIS_TOO_SMALL)); + } else if (millis > ConfigUtil.MAX_SMOOTH_ZOOM_DURATION_MILLIS) { + return Optional.of(Text.translatable(ConfigUtil.ERROR_SMOOTH_ZOOM_DURATION_MILLIS_TOO_LARGE)); + } + + return Optional.empty(); + } + @SuppressWarnings("resource") // Minecraft client is auto-closable but must not be closed by us. private void loadProperties() { if (!CONFIG_FILE.exists()) { diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java index 372ce0b..df0a2f0 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java @@ -22,21 +22,32 @@ public static Screen createConfigScreen(Screen parent) { general.addEntry(builder.entryBuilder() .startKeyCodeField(Text.translatable(ConfigUtil.OPTION_ZOOM_KEY), HANDLER.getZoomKey()) .setDefaultValue(ConfigUtil.getDefaultZoomKey()).setKeySaveConsumer(HANDLER::setZoomKey) - .setTooltip(Text.translatable(ConfigUtil.OPTION_ZOOM_KEY)).build()); + .setTooltip(Text.translatable(ConfigUtil.TOOLTIP_ZOOM_KEY)).build()); // add zoom factor field (double value) general.addEntry(builder.entryBuilder() .startDoubleField(Text.translatable(ConfigUtil.OPTION_ZOOM_FACTOR), HANDLER.getZoomFactor()) - .setDefaultValue(Double.parseDouble(ConfigUtil.DEFAULT_ZOOM_FACTOR)) - .setSaveConsumer(HANDLER::setZoomFactor).setMin(ConfigUtil.MIN_ZOOM_FACTOR) - .setMax(ConfigUtil.MAX_ZOOM_FACTOR).setTooltip(Text.translatable(ConfigUtil.TOOLTIP_ZOOM_FACTOR)) + .setDefaultValue(ConfigUtil.getDefaultZoomFactor()).setSaveConsumer(HANDLER::setZoomFactor) + .setMin(ConfigUtil.MIN_ZOOM_FACTOR).setMax(ConfigUtil.MAX_ZOOM_FACTOR) + .setTooltip(Text.translatable(ConfigUtil.TOOLTIP_ZOOM_FACTOR)) .setErrorSupplier(HANDLER::getZoomFactorError).build()); + // add enable smooth zoom button general.addEntry(builder.entryBuilder() .startBooleanToggle(Text.translatable(ConfigUtil.OPTION_ENABLE_SMOOTH_ZOOM), HANDLER.isSmoothZoomEnabled()) .setDefaultValue(ConfigUtil.getDefaultEnableSmoothZoom()).setSaveConsumer(HANDLER::setSmoothZoomEnabled) - .build()); + .setTooltip(Text.translatable(ConfigUtil.TOOLTIP_ENABLE_SMOOTH_ZOOM)).build()); + + // add smooth zoom duration field (double value) + general.addEntry(builder.entryBuilder() + .startLongField(Text.translatable(ConfigUtil.OPTION_SMOOTH_ZOOM_DURATION_MILLIS), + HANDLER.getSmoothZoomDurationMillis()) + .setDefaultValue(ConfigUtil.getDefaultSmoothZoomDurationMillis()) + .setSaveConsumer(HANDLER::setSmoothZoomDurationMillis) + .setMin(ConfigUtil.MIN_SMOOTH_ZOOM_DURATION_MILLIS).setMax(ConfigUtil.MAX_SMOOTH_ZOOM_DURATION_MILLIS) + .setTooltip(Text.translatable(ConfigUtil.TOOLTIP_SMOOTH_ZOOM_DURATION_MILLIS)) + .setErrorSupplier(HANDLER::getSmoothZoomDurationMillisError).build()); return builder.build(); } diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java index 133c691..51c1a69 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java @@ -6,53 +6,70 @@ public class ConfigUtil { - static final String NAMESPACE = "logical_zoom"; + public static final String NAMESPACE = "logical_zoom"; - static final String OPTION_PREFIX = NAMESPACE + ".option"; - static final String TOOLTIP_PREFIX = NAMESPACE + ".tooltip"; - static final String WARN_PREFIX = NAMESPACE + ".warn"; - static final String ERROR_PREFIX = NAMESPACE + ".error"; + public static final String CATEGORY_PREFIX = NAMESPACE + ".category"; + public static final String OPTION_PREFIX = NAMESPACE + ".option"; + public static final String TOOLTIP_PREFIX = NAMESPACE + ".tooltip"; + public static final String WARN_PREFIX = NAMESPACE + ".warn"; + public static final String ERROR_PREFIX = NAMESPACE + ".error"; - static final String CATEGORY_GENERAL = NAMESPACE + ".category.general"; - static final String MENU_TITLE = NAMESPACE + ".menu.title"; + public static final String MENU_TITLE = NAMESPACE + ".menu_title"; - static final String OPTION_ZOOM_FACTOR = OPTION_PREFIX + ".zoom_factor"; - static final String OPTION_ZOOM_KEY = OPTION_PREFIX + ".zoom_key"; - static final String OPTION_ENABLE_SMOOTH_ZOOM = OPTION_PREFIX + ".toggle_smooth_zoom"; + public static final String CATEGORY_GENERAL = CATEGORY_PREFIX + ".general"; + public static final String CATEGORY_ZOOM_KEY = CATEGORY_PREFIX + ".zoom_key"; - static final String DEFAULT_ZOOM_FACTOR = "5.0"; - static final double MIN_ZOOM_FACTOR = 1.0; - static final double MAX_ZOOM_FACTOR = 5.0; - static final String DEFAULT_ZOOM_KEY = Integer.toString(GLFW.GLFW_KEY_C); - static final String DEFAULT_ENABLE_SMOOTH_ZOOM = "true"; + public static final String OPTION_ZOOM_KEY = OPTION_PREFIX + ".zoom_key"; + public static final String OPTION_ZOOM_FACTOR = OPTION_PREFIX + ".zoom_factor"; + public static final String OPTION_ENABLE_SMOOTH_ZOOM = OPTION_PREFIX + ".enable_smooth_zoom"; + public static final String OPTION_SMOOTH_ZOOM_DURATION_MILLIS = OPTION_PREFIX + ".smooth_zoom_duration_millis"; - static final String TOOLTIP_ZOOM_FACTOR = TOOLTIP_PREFIX + ".zoom_factor"; - static final String TOOLTIP_ZOOM_KEY = TOOLTIP_PREFIX + ".zoom_key"; - static final String TOOLTIP_ENABLE_SMOOTH_ZOOM = TOOLTIP_PREFIX + ".toggle_smooth_zoom"; + public static final String DEFAULT_ZOOM_KEY = Integer.toString(GLFW.GLFW_KEY_C); + public static final String DEFAULT_ZOOM_FACTOR = "3.0"; + public static final double MIN_ZOOM_FACTOR = 1.0; + public static final double MAX_ZOOM_FACTOR = 5.0; + public static final String DEFAULT_ENABLE_SMOOTH_ZOOM = "true"; + public static final String DEFAULT_SMOOTH_ZOOM_DURATION_MILLIS = "150"; + // The duration value must not be zero or it will lead to a division by zero! + public static final long MIN_SMOOTH_ZOOM_DURATION_MILLIS = 1L; + public static final long MAX_SMOOTH_ZOOM_DURATION_MILLIS = 10000L; - static final String CONFIG_FILE_NAME = "config/logical_zoom.properties"; - static final String ERROR_CONFIG_FILE_READ = ERROR_PREFIX + ".config_file_read"; - static final String ERROR_CONFIG_FILE_WRITE = ERROR_PREFIX + ".config_file_write"; - static final String ERROR_ZOOM_FACTOR_TOO_SMALL = ERROR_PREFIX + ".zoom_factor_too_small"; - static final String ERROR_ZOOM_FACTOR_TOO_LARGE = ERROR_PREFIX + ".zoom_factor_too_large"; + public static final String TOOLTIP_ZOOM_FACTOR = TOOLTIP_PREFIX + ".zoom_factor"; + public static final String TOOLTIP_ZOOM_KEY = TOOLTIP_PREFIX + ".zoom_key"; + public static final String TOOLTIP_ENABLE_SMOOTH_ZOOM = TOOLTIP_PREFIX + ".enable_smooth_zoom"; + public static final String TOOLTIP_SMOOTH_ZOOM_DURATION_MILLIS = TOOLTIP_PREFIX + ".smooth_zoom_duration_millis"; - static InputUtil.Key getDefaultZoomKey() { + public static final String CONFIG_FILE_NAME = "config/logical_zoom.properties"; + public static final String ERROR_CONFIG_FILE_READ = ERROR_PREFIX + ".config_file_read"; + public static final String ERROR_CONFIG_FILE_WRITE = ERROR_PREFIX + ".config_file_write"; + public static final String ERROR_ZOOM_FACTOR_TOO_SMALL = ERROR_PREFIX + ".zoom_factor_too_small"; + public static final String ERROR_ZOOM_FACTOR_TOO_LARGE = ERROR_PREFIX + ".zoom_factor_too_large"; + public static final String ERROR_SMOOTH_ZOOM_DURATION_MILLIS_TOO_SMALL = ERROR_PREFIX + + ".smooth_zoom_duration_millis_too_small"; + public static final String ERROR_SMOOTH_ZOOM_DURATION_MILLIS_TOO_LARGE = ERROR_PREFIX + + ".smooth_zoom_duration_millis_too_large"; + + public static InputUtil.Key getDefaultZoomKey() { return getKeyFromCode(DEFAULT_ZOOM_KEY); } - static InputUtil.Key getKeyFromCode(String property) { + public static InputUtil.Key getKeyFromCode(String property) { return getKeyFromCode(Integer.parseInt(property)); } - static InputUtil.Key getKeyFromCode(int keyCode) { + public static InputUtil.Key getKeyFromCode(int keyCode) { return InputUtil.Type.KEYSYM.createFromCode(keyCode); } - static double getDefaultZoomFactor() { + public static double getDefaultZoomFactor() { return Double.parseDouble(DEFAULT_ZOOM_FACTOR); } - static boolean getDefaultEnableSmoothZoom() { + public static boolean getDefaultEnableSmoothZoom() { return Boolean.parseBoolean(DEFAULT_ENABLE_SMOOTH_ZOOM); } + + public static long getDefaultSmoothZoomDurationMillis() { + return Long.parseLong(DEFAULT_SMOOTH_ZOOM_DURATION_MILLIS); + } } diff --git a/src/main/resources/assets/logical_zoom/lang/en_us.json b/src/main/resources/assets/logical_zoom/lang/en_us.json index a109f3c..3e2fcaa 100644 --- a/src/main/resources/assets/logical_zoom/lang/en_us.json +++ b/src/main/resources/assets/logical_zoom/lang/en_us.json @@ -1,7 +1,17 @@ { - "key.logical_zoom.zoom": "Toggle Zoom", - "category.logical_zoom.zoom": "Logical Zoom", - "title.logical_zoom.menu": "Logical Zoom Settings", - "option.logical_zoom.toggle_smooth_zoom": "", - "tooltip.logical_zoom.toggle_smooth_zoom": "" + "logical_zoom.menu_title": "Logical Zoom Settings", + "logical_zoom.category.general": "General", + "logical_zoom.category.zoom_key": "Logical Zoom", + + "logical_zoom.option.zoom_key": "Zoom", + "logical_zoom.tooltip.zoom_key": "The key used to zoom in.", + + "logical_zoom.option.zoom_factor": "Zoom Factor", + "logical_zoom.tooltip.zoom_factor": "Defines how much the picture is zoomed when pressing the zoom key.", + + "logical_zoom.option.enable_smooth_zoom": "Enable Smooth Zoom", + "logical_zoom.tooltip.enable_smooth_zoom": "Enables/disables the zoom-in and zoom-out transition animations.", + + "logical_zoom.option.smooth_zoom_duration_millis": "Smooth Zoom Duration (in ms)", + "logical_zoom.tooltip.smooth_zoom_duration_millis": "Defines how long the zoom-in and zoom-out transitions take." } From 8d002ef9bc7d783d6a47ba16260cc42928111077 Mon Sep 17 00:00:00 2001 From: Marcel Brannahl Date: Fri, 16 Sep 2022 03:27:26 +0200 Subject: [PATCH 09/12] Add missing lang entries #2 --- .../com/logicalgeekboy/logical_zoom/LogicalZoom.java | 1 - .../logical_zoom/config/ConfigHandler.java | 9 ++++++--- .../logicalgeekboy/logical_zoom/config/ConfigUtil.java | 2 -- src/main/resources/assets/logical_zoom/lang/en_us.json | 9 +++++++-- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java index 92aabaf..1bc0814 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java @@ -21,7 +21,6 @@ public class LogicalZoom implements ClientModInitializer { @Override public void onInitializeClient() { - // TODO add Mod Menu config for smooth zoom on/off + duration (#2) zoomKeyBinding = new KeyBinding(ConfigUtil.OPTION_ZOOM_KEY, InputUtil.Type.KEYSYM, HANDLER.getZoomKeyCode(), ConfigUtil.CATEGORY_ZOOM_KEY); diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java index c08af65..d6f3fd8 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java @@ -9,7 +9,10 @@ import java.util.Optional; import java.util.Properties; +import org.slf4j.Logger; + import com.logicalgeekboy.logical_zoom.LogicalZoom; +import com.mojang.logging.LogUtils; import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.ClientPlayerEntity; @@ -21,6 +24,7 @@ public class ConfigHandler { private static final File CONFIG_FILE = new File(ConfigUtil.CONFIG_FILE_NAME); private static final ConfigHandler INSTANCE = new ConfigHandler(); + private static final Logger LOG = LogUtils.getLogger(); private final ClientPlayerEntity player; @@ -108,7 +112,7 @@ private void loadProperties() { try (InputStream is = new FileInputStream(CONFIG_FILE)) { this.properties.load(is); } catch (IOException e) { - this.player.sendMessage(Text.translatable(ConfigUtil.ERROR_CONFIG_FILE_READ)); + LOG.error("Could not read from config file!", e); loadDefaultProperties(); } @@ -128,10 +132,9 @@ private void loadDefaultProperties() { @SuppressWarnings("resource") // Minecraft client is auto-closable but must not be closed by us. void saveProperties() { try (OutputStream out = new FileOutputStream(CONFIG_FILE)) { - // TODO check if the file is overwritten or the contents appended this.properties.store(out, null); } catch (IOException e) { - this.player.sendMessage(Text.translatable(ConfigUtil.ERROR_CONFIG_FILE_WRITE)); + LOG.error("Could not write to config file!", e); } } } diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java index 51c1a69..11b8af1 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java @@ -40,8 +40,6 @@ public class ConfigUtil { public static final String TOOLTIP_SMOOTH_ZOOM_DURATION_MILLIS = TOOLTIP_PREFIX + ".smooth_zoom_duration_millis"; public static final String CONFIG_FILE_NAME = "config/logical_zoom.properties"; - public static final String ERROR_CONFIG_FILE_READ = ERROR_PREFIX + ".config_file_read"; - public static final String ERROR_CONFIG_FILE_WRITE = ERROR_PREFIX + ".config_file_write"; public static final String ERROR_ZOOM_FACTOR_TOO_SMALL = ERROR_PREFIX + ".zoom_factor_too_small"; public static final String ERROR_ZOOM_FACTOR_TOO_LARGE = ERROR_PREFIX + ".zoom_factor_too_large"; public static final String ERROR_SMOOTH_ZOOM_DURATION_MILLIS_TOO_SMALL = ERROR_PREFIX diff --git a/src/main/resources/assets/logical_zoom/lang/en_us.json b/src/main/resources/assets/logical_zoom/lang/en_us.json index 3e2fcaa..74b89f8 100644 --- a/src/main/resources/assets/logical_zoom/lang/en_us.json +++ b/src/main/resources/assets/logical_zoom/lang/en_us.json @@ -10,8 +10,13 @@ "logical_zoom.tooltip.zoom_factor": "Defines how much the picture is zoomed when pressing the zoom key.", "logical_zoom.option.enable_smooth_zoom": "Enable Smooth Zoom", - "logical_zoom.tooltip.enable_smooth_zoom": "Enables/disables the zoom-in and zoom-out transition animations.", + "logical_zoom.tooltip.enable_smooth_zoom": "Enables/disables zoom-in/-out animations.", "logical_zoom.option.smooth_zoom_duration_millis": "Smooth Zoom Duration (in ms)", - "logical_zoom.tooltip.smooth_zoom_duration_millis": "Defines how long the zoom-in and zoom-out transitions take." + "logical_zoom.tooltip.smooth_zoom_duration_millis": "Defines how long the animations take (higher value = slower).", + + "logical_zoom.error.zoom_factor_too_small": "Zoom factor too small (min. 1.0)!", + "logical_zoom.error.zoom_factor_too_large": "Zoom factor too large (max. 5.0)!", + "logical_zoom.error.smooth_zoom_duration_millis_too_small": "Duration must be positive!", + "logical_zoom.error.smooth_zoom_duration_millis_too_large": "Duration too large (max. 10000)!" } From fe04795e13c0a9886f6e4e23650fcce4a074bfc4 Mon Sep 17 00:00:00 2001 From: marcelbpunkt Date: Sat, 17 Sep 2022 16:52:35 +0200 Subject: [PATCH 10/12] Handle keybind via native MC options only #2 --- .gitignore | 4 +++ build.gradle | 12 ++++++--- gradle.properties | 4 +-- logical_zoom_client.launch | 17 ------------ logical_zoom_server.launch | 17 ------------ .../logical_zoom/LogicalZoom.java | 19 +++++++------- .../logical_zoom/config/ConfigHandler.java | 26 ------------------- .../logical_zoom/config/ConfigScreen.java | 6 ----- .../logical_zoom/config/ConfigUtil.java | 23 +--------------- .../assets/logical_zoom/lang/en_us.json | 6 ++--- src/main/resources/fabric.mod.json | 23 ++++++++++++---- 11 files changed, 46 insertions(+), 111 deletions(-) delete mode 100644 logical_zoom_client.launch delete mode 100644 logical_zoom_server.launch diff --git a/.gitignore b/.gitignore index 8da6f39..ad3d73b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,10 @@ classes/ *.ipr *.iws +# eclipse + +*.launch + # vscode .settings/ diff --git a/build.gradle b/build.gradle index 5e8fbbf..8eb47bf 100644 --- a/build.gradle +++ b/build.gradle @@ -36,16 +36,22 @@ dependencies { modApi ("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { exclude(group: "net.fabricmc.fabric-api") } - modApi ("com.terraformersmc:modmenu:${project.mod_menu_version}") { + modApi ("com.terraformersmc:modmenu:${project.modmenu_version}") { exclude(group: "net.fabricmc.fabric-api") } } processResources { - inputs.property "version", project.version + // inputs.property "version", project.version + // inputs.property "loader_version", project.loader_version + // inputs.property "cloth_config_version", project.cloth_config_version + // inputs.property "modmenu_version", project.modmenu_version filesMatching("fabric.mod.json") { - expand "version": project.version + expand (project: project) // "version": project.version + // expand "loader_version": project.loader_version + // expand "cloth_config_version": project.cloth_config_version + // expand "modmenu_version": project.modmenu_version } } diff --git a/gradle.properties b/gradle.properties index fcb1ad0..38a7c87 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ # Done to increase the memory available to gradle. org.gradle.jvmargs=-Xmx4G -# Fabric Properties from the Fabric insaller +# Fabric Properties from the Fabric installer minecraft_version=1.19.2 yarn_mappings=1.19.2+build.9 loader_version=0.14.9 @@ -15,4 +15,4 @@ archives_base_name = logical_zoom fabric_version=0.60.0+1.19.2 cloth_config_version=8.2.88 -mod_menu_version=4.0.6 \ No newline at end of file +modmenu_version=4.0.6 \ No newline at end of file diff --git a/logical_zoom_client.launch b/logical_zoom_client.launch deleted file mode 100644 index 469c7d7..0000000 --- a/logical_zoom_client.launch +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/logical_zoom_server.launch b/logical_zoom_server.launch deleted file mode 100644 index 717f759..0000000 --- a/logical_zoom_server.launch +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java index 1bc0814..47adc4e 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java @@ -21,8 +21,14 @@ public class LogicalZoom implements ClientModInitializer { @Override public void onInitializeClient() { - zoomKeyBinding = new KeyBinding(ConfigUtil.OPTION_ZOOM_KEY, InputUtil.Type.KEYSYM, HANDLER.getZoomKeyCode(), - ConfigUtil.CATEGORY_ZOOM_KEY); + /* + * The order of "key"/"category", the namespace and "zoom" is slightly different + * than in ConfigUtil in order to be consistent with the rest of the keybinds. + * It's also not included in the config classes because the zoom key is only + * configurable via Options -> Controls -> Key Binds. + */ + zoomKeyBinding = new KeyBinding("key." + ConfigUtil.NAMESPACE + ".zoom", InputUtil.Type.KEYSYM, + InputUtil.GLFW_KEY_C, "category." + ConfigUtil.NAMESPACE + ".zoom"); lastZoomKeyActionTimestamp = 0L; originalSmoothCameraEnabled = false; @@ -38,13 +44,6 @@ public static double getCurrentZoomLevel() { HANDLER.getSmoothZoomDurationMillis(), currentDurationMillis); } - public static void updateZoomKeyBinding(InputUtil.Key zoomKey) { - // TODO can we make this a thing solely managed by Mod Menu somehow? This seems - // a bit hacky. - zoomKeyBinding.setBoundKey(zoomKey); - KeyBinding.updateKeysByCode(); - } - private static boolean isZoomKeyPressed() { return zoomKeyBinding.isPressed(); } @@ -142,7 +141,7 @@ private static enum ZoomState { // x_min and x_max. // The lower y_min is, the steeper the slope is near x_min. // The higher y_max is, the more shallow the slope is near x_max. - private static final double Y_MIN = -3.0; + private static final double Y_MIN = -2.5; private static final double Y_MAX = 3.0; private static final double Y_RANGE = Y_MAX - Y_MIN; // the min and max x values equal e^y_min and e^y_max respectively because we diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java index d6f3fd8..ca84da6 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java @@ -11,13 +11,8 @@ import org.slf4j.Logger; -import com.logicalgeekboy.logical_zoom.LogicalZoom; import com.mojang.logging.LogUtils; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.client.util.InputUtil; -import net.minecraft.client.util.InputUtil.Key; import net.minecraft.text.Text; public class ConfigHandler { @@ -26,13 +21,9 @@ public class ConfigHandler { private static final ConfigHandler INSTANCE = new ConfigHandler(); private static final Logger LOG = LogUtils.getLogger(); - private final ClientPlayerEntity player; - private Properties properties; - @SuppressWarnings("resource") // Minecraft client is auto-closable but must not be closed by us. private ConfigHandler() { - this.player = MinecraftClient.getInstance().player; this.properties = new Properties(); loadProperties(); } @@ -47,15 +38,6 @@ public double getZoomFactor() { return Double.parseDouble(property); } - public InputUtil.Key getZoomKey() { - return ConfigUtil.getKeyFromCode(Integer.toString(getZoomKeyCode())); - } - - public int getZoomKeyCode() { - return Integer.parseInt( - (String) this.properties.getOrDefault(ConfigUtil.OPTION_ZOOM_KEY, ConfigUtil.DEFAULT_ZOOM_KEY)); - } - public boolean isSmoothZoomEnabled() { return "true".equals(this.properties.getProperty(ConfigUtil.OPTION_ENABLE_SMOOTH_ZOOM)); } @@ -69,11 +51,6 @@ public void setZoomFactor(double zoomFactor) { this.properties.put(ConfigUtil.OPTION_ZOOM_FACTOR, Double.toString(zoomFactor)); } - public void setZoomKey(Key zoomKey) { - this.properties.put(ConfigUtil.OPTION_ZOOM_KEY, Integer.toString(zoomKey.getCode())); - LogicalZoom.updateZoomKeyBinding(zoomKey); - } - public void setSmoothZoomEnabled(boolean isSmoothZoomEnabled) { this.properties.put(ConfigUtil.OPTION_ENABLE_SMOOTH_ZOOM, Boolean.toString(isSmoothZoomEnabled)); } @@ -102,7 +79,6 @@ public Optional getSmoothZoomDurationMillisError(long millis) { return Optional.empty(); } - @SuppressWarnings("resource") // Minecraft client is auto-closable but must not be closed by us. private void loadProperties() { if (!CONFIG_FILE.exists()) { loadDefaultProperties(); @@ -121,7 +97,6 @@ private void loadProperties() { private void loadDefaultProperties() { properties.put(ConfigUtil.OPTION_ENABLE_SMOOTH_ZOOM, ConfigUtil.DEFAULT_ENABLE_SMOOTH_ZOOM); properties.put(ConfigUtil.OPTION_ZOOM_FACTOR, ConfigUtil.DEFAULT_ZOOM_FACTOR); - properties.put(ConfigUtil.OPTION_ZOOM_KEY, ConfigUtil.DEFAULT_ZOOM_KEY); // since the default properties are only loaded if the properties file does not // exist or cannot be accessed for whatever reason, let's create/overwrite the @@ -129,7 +104,6 @@ private void loadDefaultProperties() { saveProperties(); } - @SuppressWarnings("resource") // Minecraft client is auto-closable but must not be closed by us. void saveProperties() { try (OutputStream out = new FileOutputStream(CONFIG_FILE)) { this.properties.store(out, null); diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java index df0a2f0..4fb80a2 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java @@ -18,12 +18,6 @@ public static Screen createConfigScreen(Screen parent) { ConfigCategory general = builder.getOrCreateCategory(Text.translatable(ConfigUtil.CATEGORY_GENERAL)); - // add zoom key field - general.addEntry(builder.entryBuilder() - .startKeyCodeField(Text.translatable(ConfigUtil.OPTION_ZOOM_KEY), HANDLER.getZoomKey()) - .setDefaultValue(ConfigUtil.getDefaultZoomKey()).setKeySaveConsumer(HANDLER::setZoomKey) - .setTooltip(Text.translatable(ConfigUtil.TOOLTIP_ZOOM_KEY)).build()); - // add zoom factor field (double value) general.addEntry(builder.entryBuilder() .startDoubleField(Text.translatable(ConfigUtil.OPTION_ZOOM_FACTOR), HANDLER.getZoomFactor()) diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java index 11b8af1..b644956 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java @@ -1,9 +1,5 @@ package com.logicalgeekboy.logical_zoom.config; -import org.lwjgl.glfw.GLFW; - -import net.minecraft.client.util.InputUtil; - public class ConfigUtil { public static final String NAMESPACE = "logical_zoom"; @@ -11,31 +7,26 @@ public class ConfigUtil { public static final String CATEGORY_PREFIX = NAMESPACE + ".category"; public static final String OPTION_PREFIX = NAMESPACE + ".option"; public static final String TOOLTIP_PREFIX = NAMESPACE + ".tooltip"; - public static final String WARN_PREFIX = NAMESPACE + ".warn"; public static final String ERROR_PREFIX = NAMESPACE + ".error"; public static final String MENU_TITLE = NAMESPACE + ".menu_title"; public static final String CATEGORY_GENERAL = CATEGORY_PREFIX + ".general"; - public static final String CATEGORY_ZOOM_KEY = CATEGORY_PREFIX + ".zoom_key"; - public static final String OPTION_ZOOM_KEY = OPTION_PREFIX + ".zoom_key"; public static final String OPTION_ZOOM_FACTOR = OPTION_PREFIX + ".zoom_factor"; public static final String OPTION_ENABLE_SMOOTH_ZOOM = OPTION_PREFIX + ".enable_smooth_zoom"; public static final String OPTION_SMOOTH_ZOOM_DURATION_MILLIS = OPTION_PREFIX + ".smooth_zoom_duration_millis"; - public static final String DEFAULT_ZOOM_KEY = Integer.toString(GLFW.GLFW_KEY_C); public static final String DEFAULT_ZOOM_FACTOR = "3.0"; public static final double MIN_ZOOM_FACTOR = 1.0; public static final double MAX_ZOOM_FACTOR = 5.0; public static final String DEFAULT_ENABLE_SMOOTH_ZOOM = "true"; - public static final String DEFAULT_SMOOTH_ZOOM_DURATION_MILLIS = "150"; + public static final String DEFAULT_SMOOTH_ZOOM_DURATION_MILLIS = "120"; // The duration value must not be zero or it will lead to a division by zero! public static final long MIN_SMOOTH_ZOOM_DURATION_MILLIS = 1L; public static final long MAX_SMOOTH_ZOOM_DURATION_MILLIS = 10000L; public static final String TOOLTIP_ZOOM_FACTOR = TOOLTIP_PREFIX + ".zoom_factor"; - public static final String TOOLTIP_ZOOM_KEY = TOOLTIP_PREFIX + ".zoom_key"; public static final String TOOLTIP_ENABLE_SMOOTH_ZOOM = TOOLTIP_PREFIX + ".enable_smooth_zoom"; public static final String TOOLTIP_SMOOTH_ZOOM_DURATION_MILLIS = TOOLTIP_PREFIX + ".smooth_zoom_duration_millis"; @@ -47,18 +38,6 @@ public class ConfigUtil { public static final String ERROR_SMOOTH_ZOOM_DURATION_MILLIS_TOO_LARGE = ERROR_PREFIX + ".smooth_zoom_duration_millis_too_large"; - public static InputUtil.Key getDefaultZoomKey() { - return getKeyFromCode(DEFAULT_ZOOM_KEY); - } - - public static InputUtil.Key getKeyFromCode(String property) { - return getKeyFromCode(Integer.parseInt(property)); - } - - public static InputUtil.Key getKeyFromCode(int keyCode) { - return InputUtil.Type.KEYSYM.createFromCode(keyCode); - } - public static double getDefaultZoomFactor() { return Double.parseDouble(DEFAULT_ZOOM_FACTOR); } diff --git a/src/main/resources/assets/logical_zoom/lang/en_us.json b/src/main/resources/assets/logical_zoom/lang/en_us.json index 74b89f8..7b29558 100644 --- a/src/main/resources/assets/logical_zoom/lang/en_us.json +++ b/src/main/resources/assets/logical_zoom/lang/en_us.json @@ -1,10 +1,10 @@ { + "key.logical_zoom.zoom": "Zoom", + "category.logical_zoom.zoom": "Logical Zoom", + "logical_zoom.menu_title": "Logical Zoom Settings", "logical_zoom.category.general": "General", - "logical_zoom.category.zoom_key": "Logical Zoom", - "logical_zoom.option.zoom_key": "Zoom", - "logical_zoom.tooltip.zoom_key": "The key used to zoom in.", "logical_zoom.option.zoom_factor": "Zoom Factor", "logical_zoom.tooltip.zoom_factor": "Defines how much the picture is zoomed when pressing the zoom key.", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 9cc973f..77f1d34 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "id": "logical_zoom", - "version": "${version}", + "version": "${project.version}", "name": "Logical Zoom", "description": "A simple zoom", @@ -12,6 +12,17 @@ "homepage": "https://logicalgeekboy.com", "sources": "https://github.com/logicalgeekboy/logical_zoom" }, + "contributors": [ + { + "name": "marcelbpunkt", + "contact": { + "homepage": "https://github.com/marcelbpunkt", + "sources": "https://github.com/marcelbpunkt/logical_zoom", + "issues": "https://github.com/marcelbpunkt/logical_zoom/issues", + "discord": "https://discordapp.com/users/marcelbpunkt#8899" + } + } + ], "license": "MIT", "icon": "assets/logical_zoom/icon.png", @@ -33,11 +44,13 @@ ], "depends": { - "fabricloader": ">=0.14.9", - "fabric": "*", - "minecraft": "1.19.2" + "fabricloader": ">=${project.loader_version}", + "fabric-api": "*", + "minecraft": ">=1.19 <1.20", + "cloth-config": ">=${project.cloth_config_version}" }, "suggests": { - "flamingo": "*" + "flamingo": "*", + "modmenu": ">=${project.modmenu_version}" } } From b10eca07264fc85bea20c3d3177b7667832ff72d Mon Sep 17 00:00:00 2001 From: marcelbpunkt Date: Sat, 17 Sep 2022 19:21:53 +0200 Subject: [PATCH 11/12] Add Javadoc, code cleanup #2 --- build.gradle | 10 +- .../logical_zoom/LogicalZoom.java | 147 +++++++++++++++--- .../logical_zoom/config/ConfigHandler.java | 74 ++++++++- .../logical_zoom/config/ConfigScreen.java | 15 +- .../logical_zoom/config/ConfigUtil.java | 104 ++++++++++++- .../logical_zoom/mixin/LogicalZoomMixin.java | 2 +- 6 files changed, 317 insertions(+), 35 deletions(-) diff --git a/build.gradle b/build.gradle index 8eb47bf..ccd2cc9 100644 --- a/build.gradle +++ b/build.gradle @@ -42,16 +42,8 @@ dependencies { } processResources { - // inputs.property "version", project.version - // inputs.property "loader_version", project.loader_version - // inputs.property "cloth_config_version", project.cloth_config_version - // inputs.property "modmenu_version", project.modmenu_version - filesMatching("fabric.mod.json") { - expand (project: project) // "version": project.version - // expand "loader_version": project.loader_version - // expand "cloth_config_version": project.cloth_config_version - // expand "modmenu_version": project.modmenu_version + expand (project: project) } } diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java index 47adc4e..b6d0598 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/LogicalZoom.java @@ -9,6 +9,15 @@ import net.minecraft.client.option.KeyBinding; import net.minecraft.client.util.InputUtil; +/** + * This class puts the "Logic" in "Logical Zoom" (i.e. it contains most part of + * the mod logic). It controls the zoom activation and animation, smooth camera + * management, and also registers the key bind such that it is configurable via + * Minecraft's native option menu. + * + * @author LogicalGeekBoy, marcelbpunkt + * + */ public class LogicalZoom implements ClientModInitializer { private static long lastZoomKeyActionTimestamp; @@ -37,7 +46,17 @@ public void onInitializeClient() { KeyBindingHelper.registerKeyBinding(zoomKeyBinding); } - public static double getCurrentZoomLevel() { + /** + * Returns the current FOV multiplier, i.e. the inverse value of the current + * zoom factor. + * + * @return the inverse value of the current zoom factor; if smooth zoom is + * disabled, it returns either 1 or the inverse of the full zoom factor + * depending on whether the zoom key is currently pressed. If smooth + * zoom is enabled, it can also return values in between for when the + * camera is currently zooming in or out. + */ + public static double getCurrentFOVMultiplier() { updateZoomStateAndSmoothCamera(); double currentDurationMillis = getCurrentDuration(); return currentState.getZoomFactorFunction().apply(1 / HANDLER.getZoomFactor(), @@ -56,38 +75,61 @@ private static void updateZoomStateAndSmoothCamera() { if (isZoomKeyPressed()) { switch (currentState) { case NO_ZOOM: + // zoom key is pressed while not currently zooming at all + // => begin zoom-in if smooth zoom is enabled initZoomIn(0L); break; case ZOOM_OUT: + // zoom key is pressed while zooming out + // -> zoom back in from current camera position + // (i.e. not from "fully zoomed out" position) initZoomIn(getCurrentRemainingDuration()); break; case ZOOM_IN: + // zoom key is still pressed while currently zooming in + // -> continue zooming in until full zoom is reached if (hasMaxDurationPassed()) { currentState = ZoomState.FULL_ZOOM; } break; case FULL_ZOOM: - // do nothing + // do nothing, i.e. keep up full zoom and full zoom state } } else { switch (currentState) { case FULL_ZOOM: + // zoom key is released while fully zoomed in + // -> begin zoom-out if smooth zoom is enabled initZoomOut(0L); break; case ZOOM_IN: + // zoom key is released while zooming in + // -> zoom back out from current camera position + // (i.e. not from "fully zoomed in" position) initZoomOut(getCurrentRemainingDuration()); break; case ZOOM_OUT: + // zoom key is still released while currently zooming out + // -> continue zooming back out to no zoom if (hasMaxDurationPassed()) { currentState = ZoomState.NO_ZOOM; } break; case NO_ZOOM: - // do nothing + // do nothing, i.e. keep up 1.0x zoom and no zoom state } } } + /** + * Changes the current state to {@link ZoomState#ZOOM_IN} or + * {@link ZoomState#FULL_ZOOM} depending on whether smooth zoom is enabled, + * determines the current camera position, remembers the player's smooth camera + * setting, and enables smooth camera while the zoom key is pressed. + * + * @param offset zero if the camera is currently fully zoomed out,
+ * a value between 0 and the smooth zoom duration otherwise + */ private static void initZoomIn(long offset) { markKeyEvent(offset); originalSmoothCameraEnabled = isSmoothCameraEnabled(); @@ -95,6 +137,15 @@ private static void initZoomIn(long offset) { currentState = HANDLER.isSmoothZoomEnabled() ? ZoomState.ZOOM_IN : ZoomState.FULL_ZOOM; } + /** + * Changes the current state to {@link ZoomState#ZOOM_OUT} or + * {@link ZoomState#NO_ZOOM} depending on whether smooth zoom is enabled, + * determines the current camera position, and resets the smooth camera state to + * the player's original setting. + * + * @param offset zero if the camera is currently fully zoomed in,
+ * a value between 0 and the smooth zoom duration otherwise + */ private static void initZoomOut(long offset) { markKeyEvent(offset); resetSmoothCamera(); @@ -125,6 +176,12 @@ private static long getCurrentRemainingDuration() { return HANDLER.getSmoothZoomDurationMillis() - getCurrentDuration(); } + /** + * Contains all zoom states and their respective {@link FOVMultiplier}. + * + * @author marcelbpunkt + * + */ private static enum ZoomState { /** @@ -132,46 +189,100 @@ private static enum ZoomState { */ NO_ZOOM((zf, md, x) -> 1.0), /** - * Zooming in. The zoom factor function returns a value + * Zooming in. The zoom factor function returns a logarithmically increasing + * value between 1.0 and the full zoom factor. + */ + ZOOM_IN((zf, md, x) -> 1 - logAdjusted(zf, md, x)), + /** + * Full zoom. The zoom factor function always returns the full zoom factor. + */ + FULL_ZOOM((zf, md, x) -> zf), + /** + * Zooming out. The zoom factor function returns a logarithmically decreasing + * value between the full zoom factor and 1.0. */ - ZOOM_IN((zf, md, x) -> 1 - logAdjusted(zf, md, x)), FULL_ZOOM((zf, md, x) -> zf), ZOOM_OUT((zf, md, x) -> zf + logAdjusted(zf, md, x)); - // The y range influences the slope of the zoom factor function especially near - // x_min and x_max. - // The lower y_min is, the steeper the slope is near x_min. - // The higher y_max is, the more shallow the slope is near x_max. + //////////////////////////////// + // Function domain definition // + //////////////////////////////// + + /* + * The function domain influences the slope of the zoom factor function, + * especially near x_min and x_max. The lower y_min is, the steeper the slope is + * near x_min, zooming in/out faster at the beginning of the animation. The + * higher y_max is, the more shallow the slope is near x_max, zooming in/out + * more slowly towards the end of the animation. + */ private static final double Y_MIN = -2.5; private static final double Y_MAX = 3.0; private static final double Y_RANGE = Y_MAX - Y_MIN; - // the min and max x values equal e^y_min and e^y_max respectively because we - // want the logarithmic function to produce output values between 0 and 1. + + /* + * The min and max x values equal e^y_min and e^y_max respectively such that the + * x range matches the y range exactly (since e.g. log(e^y_min) == y_min) + */ private static final double X_MIN = Math.pow(Math.E, Y_MIN); private static final double X_MAX = Math.pow(Math.E, Y_MAX); private static final double X_RANGE = X_MAX - X_MIN; - private final ZoomFactorFunction zoomFactorFunction; + private final FOVMultiplier zoomFactorFunction; - private ZoomState(ZoomFactorFunction zoomFactorFunction) { + private ZoomState(FOVMultiplier zoomFactorFunction) { this.zoomFactorFunction = zoomFactorFunction; } - public ZoomFactorFunction getZoomFactorFunction() { + /** + * Returns the zoom factor function of the current zoom state. + * + * @return the zoom factor function of the current zoom state + */ + public FOVMultiplier getZoomFactorFunction() { return zoomFactorFunction; } - private static double logAdjusted(double zoomFactor, double maxDuration, double currentDuration) { - return (Math.log(toDomain(maxDuration, currentDuration)) - Y_MIN) / (Y_RANGE) * (1.0 - zoomFactor); + /** + * A logarithmic function that calculates and returns a value between + * {@code 0.0} and {@code 1.0 - zoomFactorInverse}. + * + * @param zoomFactorInverse the inverse value of the zoom factor setting + * @param maxDuration the smooth zoom duration setting + * @param currentDuration the current duration counting from {@code 0.0} to + * {@code maxDuration} + * @return a logarithmic value between {@code 0} and + * {@code 1.0 - zoomFactorInverse} + */ + private static double logAdjusted(double zoomFactorInverse, double maxDuration, double currentDuration) { + return (Math.log(toDomain(maxDuration, currentDuration)) - Y_MIN) / (Y_RANGE) * (1.0 - zoomFactorInverse); } + /** + * Converts a number in the range between {@code 0} and {@code maxDuration} to a + * number in the range between {@link #X_MIN} and {@link #X_MAX} and returns it. + * + * @param maxDuration the smooth zoom duration setting + * @param currentDuration the current duration counting from {@code 0.0} to + * {@code maxDuration} + * @return the converted number (between {@code X_MIN} and {@code X_MAX} + */ private static double toDomain(double maxDuration, double currentDuration) { return X_RANGE / maxDuration * currentDuration + X_MIN; } } + /** + * A function that calculates the current Field of View (FOV) multiplier which + * represents the inverse value of the current zoom factor. The current FOV + * multiplier function value depends on how much time has passed since the last + * zoom key press (zoom-in) or release (zoom-out) and must produce values + * between the inverse value of the zoom factor setting and 1.0. + * + * @author marcelbpunkt + * + */ @FunctionalInterface - private static interface ZoomFactorFunction { + private static interface FOVMultiplier { - double apply(double zoomLevel, double maxDuration, double currentDuration); + double apply(double zoomFactor, double maxDuration, double currentDuration); } } diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java index ca84da6..180867a 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigHandler.java @@ -15,6 +15,14 @@ import net.minecraft.text.Text; +/** + * Handles reading from and writing to the config file + * ({@link ConfigUtil#CONFIG_FILE_NAME}), as well as getting and setting the + * properties in-game. + * + * @author marcelbpunkt + * + */ public class ConfigHandler { private static final File CONFIG_FILE = new File(ConfigUtil.CONFIG_FILE_NAME); @@ -28,37 +36,86 @@ private ConfigHandler() { loadProperties(); } + /** + * Returns the only instance of this class. + * + * @return the only instance of this class + */ public static ConfigHandler getInstance() { return INSTANCE; } + /** + * Returns the zoom factor setting. + * + * @return the zoom factor setting + */ public double getZoomFactor() { String property = (String) this.properties.getOrDefault(ConfigUtil.OPTION_ZOOM_FACTOR, ConfigUtil.DEFAULT_ENABLE_SMOOTH_ZOOM); return Double.parseDouble(property); } + /** + * Returns the "Enable Smooth Zoom" setting. + * + * @return the "Enable Smooth Zoom" setting + */ public boolean isSmoothZoomEnabled() { return "true".equals(this.properties.getProperty(ConfigUtil.OPTION_ENABLE_SMOOTH_ZOOM)); } + /** + * Returns the smooth zoom duration setting. + * + * @return the smooth zoom duration setting in milliseconds + */ public long getSmoothZoomDurationMillis() { return Long.parseLong((String) this.properties.getOrDefault(ConfigUtil.OPTION_SMOOTH_ZOOM_DURATION_MILLIS, ConfigUtil.DEFAULT_SMOOTH_ZOOM_DURATION_MILLIS)); } + /** + * Returns the zoom factor setting. + * + * @param zoomFactor the zoom factor setting + */ public void setZoomFactor(double zoomFactor) { this.properties.put(ConfigUtil.OPTION_ZOOM_FACTOR, Double.toString(zoomFactor)); } + /** + * Sets the "Enable Smooth Zoom" setting. + * + * @param isSmoothZoomEnabled the new "Enable Smooth Zoom" setting + */ public void setSmoothZoomEnabled(boolean isSmoothZoomEnabled) { this.properties.put(ConfigUtil.OPTION_ENABLE_SMOOTH_ZOOM, Boolean.toString(isSmoothZoomEnabled)); } + /** + * Sets the smooth zoom duration setting. + * + * @param millis the new smooth zoom duration in milliseconds + */ public void setSmoothZoomDurationMillis(long millis) { this.properties.put(ConfigUtil.OPTION_SMOOTH_ZOOM_DURATION_MILLIS, Long.toString(millis)); } + /** + * Checks a specified zoom factor setting and returns an error message if it is + * too small or too large. + * + * @param zoomFactor the zoom factor setting that is to be checked + * @return + *

    + *
  • a "zoom factor too small" error message, if + * {@code zoomFactor < ConfigUtil.MIN_ZOOM_FACTOR},
  • + *
  • a "zoom factor too large" error message, if + * {@code zoomFactor > ConfigUtil.MAX_ZOOM_FACTOR},
  • + *
  • an empty Optional otherwise
  • + *
+ */ public Optional getZoomFactorError(double zoomFactor) { if (zoomFactor < ConfigUtil.MIN_ZOOM_FACTOR) { return Optional.of(Text.translatable(ConfigUtil.ERROR_ZOOM_FACTOR_TOO_SMALL)); @@ -69,6 +126,20 @@ public Optional getZoomFactorError(double zoomFactor) { return Optional.empty(); } + /** + * Checks a specified smooth zoom duration setting and returns an error message + * if it is too small or too large. + * + * @param millis the smooth zoom duration setting that is to be checked + * @return + *
    + *
  • a "duration too small" error message, if + * {@code millis < ConfigUtil.MIN_SMOOTH_ZOOM_DURATION_MILLIS},
  • + *
  • a "duration too large" error message, if + * {@code millis > ConfigUtil.MAX_SMOOTH_ZOOM_DURATION_MILLIS},
  • + *
  • an empty Optional otherwise
  • + *
+ */ public Optional getSmoothZoomDurationMillisError(long millis) { if (millis < ConfigUtil.MIN_SMOOTH_ZOOM_DURATION_MILLIS) { return Optional.of(Text.translatable(ConfigUtil.ERROR_SMOOTH_ZOOM_DURATION_MILLIS_TOO_SMALL)); @@ -91,10 +162,11 @@ private void loadProperties() { LOG.error("Could not read from config file!", e); loadDefaultProperties(); } - } private void loadDefaultProperties() { + // don't synchronize since this method is called only by loadProperties() which + // already has a lock on this.properties at this point! properties.put(ConfigUtil.OPTION_ENABLE_SMOOTH_ZOOM, ConfigUtil.DEFAULT_ENABLE_SMOOTH_ZOOM); properties.put(ConfigUtil.OPTION_ZOOM_FACTOR, ConfigUtil.DEFAULT_ZOOM_FACTOR); diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java index 4fb80a2..ade4608 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigScreen.java @@ -8,10 +8,23 @@ import net.minecraft.client.gui.screen.Screen; import net.minecraft.text.Text; +/** + * Builds and registers the Logical Zoom configuration screen and registers it + * with Mod Menu (if installed). + * + * @author marcelbpunkt + * + */ public class ConfigScreen implements ModMenuApi { private static final ConfigHandler HANDLER = ConfigHandler.getInstance(); + /** + * Builds and returns the Logical Zoom configuration screen. + * + * @param parent the screen from which this screen is opened + * @return the Logical Zoom configuration screen + */ public static Screen createConfigScreen(Screen parent) { ConfigBuilder builder = ConfigBuilder.create().setParentScreen(parent) .setTitle(Text.translatable(ConfigUtil.MENU_TITLE)).setSavingRunnable(HANDLER::saveProperties); @@ -33,7 +46,7 @@ public static Screen createConfigScreen(Screen parent) { .setDefaultValue(ConfigUtil.getDefaultEnableSmoothZoom()).setSaveConsumer(HANDLER::setSmoothZoomEnabled) .setTooltip(Text.translatable(ConfigUtil.TOOLTIP_ENABLE_SMOOTH_ZOOM)).build()); - // add smooth zoom duration field (double value) + // add smooth zoom duration field (long value) general.addEntry(builder.entryBuilder() .startLongField(Text.translatable(ConfigUtil.OPTION_SMOOTH_ZOOM_DURATION_MILLIS), HANDLER.getSmoothZoomDurationMillis()) diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java index b644956..b655489 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/config/ConfigUtil.java @@ -1,51 +1,145 @@ package com.logicalgeekboy.logical_zoom.config; +import com.logicalgeekboy.logical_zoom.LogicalZoom; + +/** + * Contains constants for all translation keys and default values except for the + * zoom key bind which is defined in {@link LogicalZoom}. All properties stored + * in the config file are String values in order to make use of + * {@code Properties.load} and {@code Properties.store}. + *

+ * The default values can also be retrieved in their correct data type via the + * respective getter methods. + * + * @author marcelbpunkt + * + */ public class ConfigUtil { + /** + * The namespace of this mod + */ public static final String NAMESPACE = "logical_zoom"; - public static final String CATEGORY_PREFIX = NAMESPACE + ".category"; - public static final String OPTION_PREFIX = NAMESPACE + ".option"; - public static final String TOOLTIP_PREFIX = NAMESPACE + ".tooltip"; - public static final String ERROR_PREFIX = NAMESPACE + ".error"; + private static final String CATEGORY_PREFIX = NAMESPACE + ".category"; + private static final String OPTION_PREFIX = NAMESPACE + ".option"; + private static final String TOOLTIP_PREFIX = NAMESPACE + ".tooltip"; + private static final String ERROR_PREFIX = NAMESPACE + ".error"; + /** + * The title displayed when this mod is configured via the Mod Menu screen + */ public static final String MENU_TITLE = NAMESPACE + ".menu_title"; + /** + * The "General" settings category + */ public static final String CATEGORY_GENERAL = CATEGORY_PREFIX + ".general"; + /** + * The translation key for the zoom factor setting + */ public static final String OPTION_ZOOM_FACTOR = OPTION_PREFIX + ".zoom_factor"; + /** + * The translation key for the "Enable Smooth Zoom" setting + */ public static final String OPTION_ENABLE_SMOOTH_ZOOM = OPTION_PREFIX + ".enable_smooth_zoom"; + /** + * The translation key for the smooth zoom duration setting + */ public static final String OPTION_SMOOTH_ZOOM_DURATION_MILLIS = OPTION_PREFIX + ".smooth_zoom_duration_millis"; + /** + * The default value for the zoom factor setting + */ public static final String DEFAULT_ZOOM_FACTOR = "3.0"; + /** + * The smallest possible zoom factor setting + */ public static final double MIN_ZOOM_FACTOR = 1.0; + /** + * The greatest possible zoom factor setting + */ public static final double MAX_ZOOM_FACTOR = 5.0; + /** + * The default "Enable Smooth Zoom" setting + */ public static final String DEFAULT_ENABLE_SMOOTH_ZOOM = "true"; + /** + * The default smooth zoom duration setting in milliseconds + */ public static final String DEFAULT_SMOOTH_ZOOM_DURATION_MILLIS = "120"; - // The duration value must not be zero or it will lead to a division by zero! + /** + * The smallest possible smooth zoom duration setting. + *

+ * The smooth zoom duration value must not be zero or it will lead to a division + * by zero! + */ public static final long MIN_SMOOTH_ZOOM_DURATION_MILLIS = 1L; + /** + * The greatest possible smooth zoom duration setting + */ public static final long MAX_SMOOTH_ZOOM_DURATION_MILLIS = 10000L; + /** + * The translation key for the zoom factor tooltip + */ public static final String TOOLTIP_ZOOM_FACTOR = TOOLTIP_PREFIX + ".zoom_factor"; + /** + * The translation key for the "Enable Smooth Zoom" tooltip + */ public static final String TOOLTIP_ENABLE_SMOOTH_ZOOM = TOOLTIP_PREFIX + ".enable_smooth_zoom"; + /** + * The translation key for the smooth zoom duration tooltip + */ public static final String TOOLTIP_SMOOTH_ZOOM_DURATION_MILLIS = TOOLTIP_PREFIX + ".smooth_zoom_duration_millis"; + /** + * The file name of the properties file to which all settings are written + */ public static final String CONFIG_FILE_NAME = "config/logical_zoom.properties"; + /** + * The translation key for the "zoom factor too small" error message + */ public static final String ERROR_ZOOM_FACTOR_TOO_SMALL = ERROR_PREFIX + ".zoom_factor_too_small"; + /** + * The translation key for the "zoom factor too large" error message + */ public static final String ERROR_ZOOM_FACTOR_TOO_LARGE = ERROR_PREFIX + ".zoom_factor_too_large"; + /** + * The translation key for the "smooth zoom duration too small" error message + */ public static final String ERROR_SMOOTH_ZOOM_DURATION_MILLIS_TOO_SMALL = ERROR_PREFIX + ".smooth_zoom_duration_millis_too_small"; + /** + * The translation key for the "smooth zoom duration too large" error message + */ public static final String ERROR_SMOOTH_ZOOM_DURATION_MILLIS_TOO_LARGE = ERROR_PREFIX + ".smooth_zoom_duration_millis_too_large"; + /** + * Returns the default zoom factor setting. + * + * @return the default zoom factor setting + */ public static double getDefaultZoomFactor() { return Double.parseDouble(DEFAULT_ZOOM_FACTOR); } + /** + * Returns the default "Enable Smooth Zoom" setting. + * + * @return the default "Enable Smooth Zoom" setting + */ public static boolean getDefaultEnableSmoothZoom() { return Boolean.parseBoolean(DEFAULT_ENABLE_SMOOTH_ZOOM); } + /** + * Returns the default smooth zoom duration setting in milliseconds. + * + * @return the default smooth zoom duration setting in milliseconds + */ public static long getDefaultSmoothZoomDurationMillis() { return Long.parseLong(DEFAULT_SMOOTH_ZOOM_DURATION_MILLIS); } diff --git a/src/main/java/com/logicalgeekboy/logical_zoom/mixin/LogicalZoomMixin.java b/src/main/java/com/logicalgeekboy/logical_zoom/mixin/LogicalZoomMixin.java index 9734e70..9a621a3 100644 --- a/src/main/java/com/logicalgeekboy/logical_zoom/mixin/LogicalZoomMixin.java +++ b/src/main/java/com/logicalgeekboy/logical_zoom/mixin/LogicalZoomMixin.java @@ -18,6 +18,6 @@ public class LogicalZoomMixin { @Inject(method = "getFov(Lnet/minecraft/client/render/Camera;FZ)D", at = @At("RETURN"), cancellable = true) public void getZoomLevel(CallbackInfoReturnable callbackInfo) { double fov = callbackInfo.getReturnValue(); - callbackInfo.setReturnValue(fov * LogicalZoom.getCurrentZoomLevel()); + callbackInfo.setReturnValue(fov * LogicalZoom.getCurrentFOVMultiplier()); } } From 6a4af45237f266e9406d1f7356e8a99a9a4134b4 Mon Sep 17 00:00:00 2001 From: marcelbpunkt Date: Sat, 17 Sep 2022 19:22:21 +0200 Subject: [PATCH 12/12] Change version to 0.2.0+1.19.2, closes #2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 38a7c87..6925a84 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ yarn_mappings=1.19.2+build.9 loader_version=0.14.9 # Mod Properties -mod_version = 0.1.0+1.19.2 +mod_version = 0.2.0+1.19.2 maven_group = com.logicalgeekboy.logical_zoom archives_base_name = logical_zoom