├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── io │ └── github │ └── foundationgames │ └── animatica │ ├── Animatica.java │ ├── animation │ ├── AnimatedTexture.java │ ├── AnimationLoader.java │ └── AnimationMeta.java │ ├── config │ └── AnimaticaConfig.java │ ├── mixin │ ├── IdentifierMixin.java │ ├── RenderSystemMixin.java │ └── VideoOptionsScreenMixin.java │ └── util │ ├── Flags.java │ ├── PropertyUtil.java │ ├── TextureUtil.java │ ├── Utilities.java │ └── exception │ ├── InvalidPropertyException.java │ ├── MissingPropertyException.java │ └── PropertyParseException.java └── resources ├── animatica.mixins.json ├── assets └── animatica │ ├── icon.png │ └── lang │ ├── en_us.json │ └── fr_fr.json └── fabric.mod.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [pull_request, push] 3 | 4 | jobs: 5 | build: 6 | strategy: 7 | matrix: 8 | java: [17] 9 | os: [ubuntu-20.04, windows-latest] 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - name: checkout repository 13 | uses: actions/checkout@v2 14 | - name: validate gradle wrapper 15 | uses: gradle/wrapper-validation-action@v1 16 | - name: setup jdk ${{ matrix.java }} 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: ${{ matrix.java }} 20 | - name: make gradle wrapper executable 21 | if: ${{ runner.os != 'Windows' }} 22 | run: chmod +x ./gradlew 23 | - name: build 24 | run: ./gradlew build 25 | - name: capture build artifacts 26 | if: ${{ runner.os == 'Linux' && matrix.java == '17' }} 27 | uses: actions/upload-artifact@v2 28 | with: 29 | name: Artifacts 30 | path: build/libs/ 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | 8 | # eclipse 9 | 10 | *.launch 11 | 12 | # idea 13 | 14 | .idea/ 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | # vscode 20 | 21 | .settings/ 22 | .vscode/ 23 | bin/ 24 | .classpath 25 | .project 26 | 27 | # macos 28 | 29 | *.DS_Store 30 | 31 | # fabric 32 | 33 | run/ 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | 167 | Copyright 2021 FoundationGames, LGPLv3 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Animatica 2 | A Minecraft mod for the Fabric mod loader intended to load the MCPatcher/OptiFine [animated texture format](https://github.com/sp614x/optifine/blob/master/OptiFineDoc/doc/custom_animations.txt).
3 | ## What resource packs does this support? 4 | Animatica adds support for resource packs that use **custom animated textures**
5 | This includes: 6 | - Animated GUI textures 7 | - Animated entity textures 8 | - Animated block entity textures 9 | - Animated armor textures 10 | - Other animated textures (Animated blocks and items are a vanilla feature) 11 | ## Animatica DOES NOT support: 12 | - Fully Custom GUI ([OptiGUI for Fabric](https://www.curseforge.com/minecraft/mc-mods/optigui)) 13 | - Custom entity models ([CEM for Fabric](https://www.curseforge.com/minecraft/mc-mods/custom-entity-models-cem)) 14 | - Custom entity model part animations in 3D ([CEM for Fabric](https://www.curseforge.com/minecraft/mc-mods/custom-entity-models-cem)) 15 | - Any other MCPatcher/OptiFine resource pack feature such as connected textures, etc 16 | 17 | **These features WILL NEVER be implemented in Animatica.**
18 | ### Animatica is ONLY for ANIMATED TEXTURES. 19 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'fabric-loom' version '1.2-SNAPSHOT' 3 | } 4 | 5 | sourceCompatibility = JavaVersion.VERSION_17 6 | targetCompatibility = JavaVersion.VERSION_17 7 | 8 | archivesBaseName = project.archives_base_name 9 | version = project.mod_version 10 | group = project.maven_group 11 | 12 | repositories { 13 | } 14 | 15 | dependencies { 16 | // To change the versions see the gradle.properties file 17 | minecraft "com.mojang:minecraft:${project.minecraft_version}" 18 | mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" 19 | modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" 20 | modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" 21 | } 22 | 23 | processResources { 24 | inputs.property "version", project.version 25 | 26 | filesMatching("fabric.mod.json") { 27 | expand "version": project.version 28 | } 29 | } 30 | 31 | tasks.withType(JavaCompile).configureEach { 32 | it.options.encoding = "UTF-8" 33 | it.options.release = 17 34 | } 35 | 36 | java { 37 | withSourcesJar() 38 | } 39 | 40 | jar { 41 | from("LICENSE") { 42 | rename { "${it}_${project.archivesBaseName}"} 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1G 2 | 3 | minecraft_version=1.20.1 4 | yarn_mappings=1.20.1+build.3 5 | loader_version=0.14.21 6 | 7 | #Fabric api 8 | fabric_version=0.83.1+1.20.1 9 | 10 | mod_version = 0.6+1.20 11 | maven_group = io.github.foundationgames 12 | archives_base_name = animatica 13 | 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundationGames/Animatica/2d08028eb0ae350f096c32f324a852770a4bc710/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | gradlePluginPortal() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/Animatica.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica; 2 | 3 | import io.github.foundationgames.animatica.animation.AnimationLoader; 4 | import io.github.foundationgames.animatica.config.AnimaticaConfig; 5 | import net.fabricmc.api.ClientModInitializer; 6 | import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; 7 | import net.fabricmc.fabric.api.resource.ResourceManagerHelper; 8 | import net.minecraft.resource.ResourceType; 9 | import net.minecraft.util.Identifier; 10 | import org.apache.logging.log4j.LogManager; 11 | import org.apache.logging.log4j.Logger; 12 | 13 | public class Animatica implements ClientModInitializer { 14 | public static final Logger LOG = LogManager.getLogger("Animatica"); 15 | public static final String NAMESPACE = "animatica"; 16 | 17 | public static final AnimaticaConfig CONFIG = new AnimaticaConfig(); 18 | 19 | @Override 20 | public void onInitializeClient() { 21 | ClientTickEvents.START_CLIENT_TICK.register(client -> AnimationLoader.INSTANCE.tickTextures()); 22 | 23 | ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(AnimationLoader.INSTANCE); 24 | } 25 | 26 | public static Identifier id(String path) { 27 | return new Identifier(NAMESPACE, path); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/animation/AnimatedTexture.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.animation; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import io.github.foundationgames.animatica.Animatica; 5 | import io.github.foundationgames.animatica.util.TextureUtil; 6 | import net.minecraft.client.texture.NativeImage; 7 | import net.minecraft.client.texture.NativeImageBackedTexture; 8 | import net.minecraft.resource.ResourceManager; 9 | import net.minecraft.util.Identifier; 10 | import net.minecraft.util.math.MathHelper; 11 | 12 | import java.io.IOException; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.Optional; 16 | 17 | public class AnimatedTexture extends NativeImageBackedTexture { 18 | public final Animation[] anims; 19 | private final NativeImage original; 20 | private int frame = 0; 21 | 22 | public static Optional tryCreate(ResourceManager resources, Identifier targetTexId, List anims) { 23 | try (var targetTexResource = resources.getResourceOrThrow(targetTexId).getInputStream()) { 24 | return Optional.of(new AnimatedTexture(resources, anims, NativeImage.read(targetTexResource))); 25 | } catch (IOException e) { Animatica.LOG.error(e); } 26 | 27 | return Optional.empty(); 28 | } 29 | 30 | public AnimatedTexture(ResourceManager resources, List metas, NativeImage image) throws IOException { 31 | super(new NativeImage(image.getFormat(), image.getWidth(), image.getHeight(), true)); 32 | 33 | this.anims = new Animation[metas.size()]; 34 | for (int i = 0; i < metas.size(); i++) { 35 | this.anims[i] = new Animation(metas.get(i), resources); 36 | } 37 | this.original = image; 38 | 39 | updateAndDraw(this.getImage(), true); 40 | this.upload(); 41 | } 42 | 43 | public boolean canLoop() { 44 | for (var anim : anims) { 45 | if (!anim.isOnFrameZero()) { 46 | return false; 47 | } 48 | } 49 | // All animations for this texture are at zero again, so the frame counter can be reset 50 | return true; 51 | } 52 | 53 | public boolean updateAndDraw(NativeImage image, boolean force) { 54 | boolean changed = false; 55 | 56 | if (canLoop()) { 57 | if (frame > 0) { 58 | frame = 0; 59 | } 60 | } else if (frame <= 0) { 61 | changed = true; 62 | } 63 | 64 | for (var anim : anims) { 65 | if (anim.isChanged()) { 66 | changed = true; 67 | break; 68 | } 69 | } 70 | 71 | if (changed || force) { 72 | image.copyFrom(this.original); 73 | 74 | Phase phase; 75 | for (var anim : anims) { 76 | phase = anim.getCurrentPhase(); 77 | if (phase instanceof InterpolatedPhase iPhase) { 78 | TextureUtil.blendCopy(anim.sourceTexture, 0, iPhase.prevV, 0, iPhase.v, anim.width, anim.height, image, anim.targetX, anim.targetY, iPhase.blend.getBlend(anim.getPhaseFrame())); 79 | } else { 80 | TextureUtil.copy(anim.sourceTexture, 0, phase.v, anim.width, anim.height, image, anim.targetX, anim.targetY); 81 | } 82 | } 83 | } 84 | 85 | for (var anim : anims) { 86 | anim.advance(); 87 | } 88 | frame++; 89 | 90 | return changed; 91 | } 92 | 93 | public void tick() { 94 | if (this.updateAndDraw(this.getImage(), false)) { 95 | this.upload(); 96 | } 97 | } 98 | 99 | @Override 100 | public void close() { 101 | for (var anim : anims) { 102 | anim.close(); 103 | } 104 | 105 | this.original.close(); 106 | super.close(); 107 | } 108 | 109 | // Represents an active animation from an animation meta file; progresses through phases while being drawn 110 | public static class Animation implements AutoCloseable { 111 | private final List phases; 112 | public final NativeImage sourceTexture; 113 | public final int targetX; 114 | public final int targetY; 115 | public final int width; 116 | public final int height; 117 | private final int duration; 118 | 119 | private int frame = 0; 120 | private Phase currentPhase = null; 121 | private int phaseFrame = 0; 122 | private boolean changed = true; 123 | 124 | // Assembles all animation phases for one texture animation being baked 125 | public Animation(AnimationMeta meta, ResourceManager resources) throws IOException { 126 | this.targetX = meta.targetX(); 127 | this.targetY = meta.targetY(); 128 | this.width = meta.width(); 129 | this.height = meta.height(); 130 | 131 | try (var source = resources.getResourceOrThrow(meta.source()).getInputStream()) { 132 | this.sourceTexture = NativeImage.read(source); 133 | } 134 | 135 | var phases = ImmutableList.builder(); 136 | int duration = 0; 137 | 138 | final int textureFrameCount = (int)Math.floor((float) sourceTexture.getHeight() / meta.height()); 139 | final int animFrameCount = Math.max(textureFrameCount, meta.getGreatestUsedFrame() + 1); 140 | 141 | // The int array stored for each frame must contain the frame mapping and duration 142 | List frames = new ArrayList<>(); 143 | for (int f = 0; f < animFrameCount; f++) { 144 | if (f >= textureFrameCount && !meta.frameMapping().containsKey(f)) { 145 | continue; 146 | } 147 | 148 | frames.add(new int[] { 149 | meta.frameMapping().getOrDefault(f, f), 150 | meta.frameDurations().getOrDefault(f, meta.defaultFrameDuration()) 151 | }); 152 | } 153 | 154 | for (int i = 0; i < frames.size(); i++) { 155 | int[] frame = frames.get(i); 156 | 157 | int fMap = frame[0]; 158 | int fDuration = frame[1]; 159 | 160 | int v = getVForFrame(fMap, textureFrameCount); 161 | int nextV = getVForFrame(frames.get(Math.floorMod(i + 1, frames.size()))[0], textureFrameCount); 162 | 163 | if (meta.interpolate()) { 164 | if (meta.interpolationDelay() > 0) { 165 | // Adds a static version of the current phase as a "delay" before the next interpolated phase (if specified in animation) 166 | phases.add(new Phase(meta.interpolationDelay(), v)); 167 | duration += meta.interpolationDelay(); 168 | } 169 | 170 | // Add interpolated animation phase 171 | final int interpolatedDuration = fDuration - meta.interpolationDelay(); 172 | phases.add(new InterpolatedPhase(interpolatedDuration, v, nextV, (phaseFrame) -> ((float) phaseFrame / interpolatedDuration))); 173 | duration += interpolatedDuration; 174 | } else { 175 | phases.add(new Phase(fDuration, v)); 176 | duration += fDuration; 177 | } 178 | } 179 | 180 | this.duration = duration; 181 | this.phases = phases.build(); 182 | 183 | updateCurrentPhase(); 184 | } 185 | 186 | public void updateCurrentPhase() { 187 | changed = false; 188 | int progress = frame; 189 | 190 | for (var phase : phases) { 191 | progress -= phase.duration; // Take away as much progress as each phase is long, until progress is below zero 192 | if (progress < 0) { 193 | if (currentPhase != phase) { 194 | // Marks baking anim as changed should it be in a new, unique phase 195 | changed = true; 196 | } 197 | if (phase instanceof InterpolatedPhase iPhase) changed = iPhase.hasChangingV(); // Marks baking anim as changed should its current phase be changing 198 | 199 | this.currentPhase = phase; 200 | this.phaseFrame = phase.duration + progress; // Adding progress to the phase duration results in how far it is into the phase 201 | 202 | return; 203 | } 204 | } 205 | } 206 | 207 | public Phase getCurrentPhase() { 208 | return currentPhase; 209 | } 210 | 211 | public int getPhaseFrame() { 212 | return phaseFrame; 213 | } 214 | 215 | public boolean isOnFrameZero() { 216 | return frame <= 0; 217 | } 218 | 219 | public boolean isChanged() { 220 | return changed; 221 | } 222 | 223 | public void advance() { 224 | frame++; 225 | if (frame >= duration) { 226 | frame = 0; 227 | } 228 | updateCurrentPhase(); 229 | } 230 | 231 | @Override 232 | public void close() { 233 | this.sourceTexture.close(); 234 | } 235 | 236 | private int getVForFrame(int frame, int textureFrameCount) { 237 | return MathHelper.clamp(frame * this.height, 0, (textureFrameCount - 1) * this.height); 238 | } 239 | } 240 | 241 | // Represents a phase that an animation is in (loosely defined by the animation file's "tile"s) 242 | // Base class represents the simplest possible type of animation phase, which only needs to generate 243 | // one texture to be used over a period of time 244 | public static class Phase { 245 | public final int duration; 246 | public final int v; 247 | 248 | public Phase(int duration, int v) { 249 | this.duration = duration; 250 | this.v = v; 251 | } 252 | 253 | @Override 254 | public String toString() { 255 | return "Animation Bakery Phase { v: "+this.v+" }"; 256 | } 257 | } 258 | 259 | // A phase that blends between its previous phase and itself, requiring the generation of many 260 | // more textures to construct the blend animation 261 | public static class InterpolatedPhase extends Phase { 262 | public final int prevV; 263 | public final BlendInterpolator blend; 264 | 265 | public InterpolatedPhase(int duration, int v1, int v2, BlendInterpolator blend) { 266 | super(duration, v2); 267 | this.prevV = v1; 268 | this.blend = blend; 269 | } 270 | 271 | public boolean hasChangingV() { 272 | return this.prevV != this.v; 273 | } 274 | } 275 | 276 | @FunctionalInterface 277 | public interface BlendInterpolator { 278 | float getBlend(int phaseFrame); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/animation/AnimationLoader.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.animation; 2 | 3 | import com.mojang.blaze3d.systems.RenderSystem; 4 | import io.github.foundationgames.animatica.Animatica; 5 | import io.github.foundationgames.animatica.util.Flags; 6 | import io.github.foundationgames.animatica.util.exception.PropertyParseException; 7 | import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; 8 | import net.minecraft.client.MinecraftClient; 9 | import net.minecraft.resource.Resource; 10 | import net.minecraft.resource.ResourceManager; 11 | import net.minecraft.util.Identifier; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | import java.io.IOException; 15 | import java.util.ArrayList; 16 | import java.util.HashMap; 17 | import java.util.HashSet; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.Properties; 21 | import java.util.Set; 22 | import java.util.function.BiConsumer; 23 | 24 | public final class AnimationLoader implements SimpleSynchronousResourceReloadListener { 25 | public static final String[] ANIM_PATHS = { 26 | "animatica/anim", 27 | "mcpatcher/anim", 28 | "optifine/anim" 29 | }; 30 | private static final Identifier ID = Animatica.id("animation_storage"); 31 | 32 | public static final AnimationLoader INSTANCE = new AnimationLoader(); 33 | 34 | private final Map animationIds = new HashMap<>(); 35 | private final Set animatedTextures = new HashSet<>(); 36 | 37 | private AnimationLoader() { 38 | } 39 | 40 | private static void findAllMCPAnimations(ResourceManager manager, BiConsumer action) { 41 | for (var path : ANIM_PATHS) { 42 | manager.findResources(path, p -> p.getPath().endsWith(".properties")).forEach(action); 43 | } 44 | } 45 | 46 | public @Nullable Identifier getAnimationId(Identifier id) { 47 | return animationIds.get(id); 48 | } 49 | 50 | public void tickTextures() { 51 | if (!RenderSystem.isOnRenderThread()) { 52 | RenderSystem.recordRenderCall(this::tickTextures); 53 | } else { 54 | for (var texture : animatedTextures) { 55 | texture.tick(); 56 | } 57 | } 58 | } 59 | 60 | @Override 61 | public Identifier getFabricId() { 62 | return ID; 63 | } 64 | 65 | @Override 66 | public void reload(ResourceManager manager) { 67 | this.animatedTextures.clear(); 68 | this.animationIds.clear(); 69 | 70 | if (!Animatica.CONFIG.animatedTextures) { 71 | return; 72 | } 73 | 74 | Flags.ALLOW_INVALID_ID_CHARS = true; 75 | 76 | var animations = new HashMap>(); 77 | 78 | findAllMCPAnimations(manager, (id, resource) -> { 79 | try { 80 | try (var resourceInputStream = resource.getInputStream()) { 81 | var ppt = new Properties(); 82 | ppt.load(resourceInputStream); 83 | 84 | var anim = AnimationMeta.of(id, ppt); 85 | 86 | var targetId = anim.target(); 87 | if (!animations.containsKey(targetId)) animations.put(targetId, new ArrayList<>()); 88 | animations.get(targetId).add(anim); 89 | } 90 | } catch (IOException | PropertyParseException e) { 91 | Animatica.LOG.error(e.getMessage()); 92 | } 93 | }); 94 | 95 | for (var targetId : animations.keySet()) { 96 | AnimatedTexture.tryCreate(manager, targetId, animations.get(targetId)) 97 | .ifPresent(tex -> { 98 | var animId = new Identifier(targetId.getNamespace(), targetId.getPath() + "-anim"); 99 | this.animationIds.put(targetId, animId); 100 | this.animatedTextures.add(tex); 101 | tex.registerTexture(MinecraftClient.getInstance().getTextureManager(), manager, animId, MinecraftClient.getInstance()); 102 | }); 103 | } 104 | 105 | Flags.ALLOW_INVALID_ID_CHARS = false; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/animation/AnimationMeta.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.animation; 2 | 3 | import io.github.foundationgames.animatica.util.PropertyUtil; 4 | import io.github.foundationgames.animatica.util.Utilities; 5 | import io.github.foundationgames.animatica.util.exception.InvalidPropertyException; 6 | import io.github.foundationgames.animatica.util.exception.PropertyParseException; 7 | import net.minecraft.util.Identifier; 8 | import net.minecraft.util.InvalidIdentifierException; 9 | 10 | import java.util.HashSet; 11 | import java.util.Map; 12 | import java.util.Properties; 13 | import java.util.Set; 14 | 15 | public record AnimationMeta( 16 | Identifier source, Identifier target, int targetX, 17 | int targetY, int width, int height, int defaultFrameDuration, boolean interpolate, 18 | int interpolationDelay, Map frameMapping, 19 | Map frameDurations 20 | ) { 21 | public static AnimationMeta of(Identifier file, Properties properties) throws PropertyParseException { 22 | Identifier source; 23 | Identifier target; 24 | try { 25 | source = Utilities.processPath(file, new Identifier(PropertyUtil.get(file, properties, "from"))); 26 | } catch (InvalidIdentifierException ex) { throw new InvalidPropertyException(file, "from", "resource location"); } 27 | try { 28 | target = Utilities.processPath(file, new Identifier(PropertyUtil.get(file, properties, "to"))); 29 | } catch (InvalidIdentifierException ex) { throw new InvalidPropertyException(file, "to", "resource location"); } 30 | return new AnimationMeta( 31 | source, 32 | target, 33 | PropertyUtil.getInt(file, properties, "x"), 34 | PropertyUtil.getInt(file, properties, "y"), 35 | PropertyUtil.getInt(file, properties, "w"), 36 | PropertyUtil.getInt(file, properties, "h"), 37 | PropertyUtil.getIntOr(file, properties, "duration", 1), 38 | PropertyUtil.getBoolOr(file, properties, "interpolate", false), 39 | PropertyUtil.getIntOr(file, properties, "skip", 0), 40 | PropertyUtil.intToIntMap(PropertyUtil.getSubProperties(properties, "tile")), 41 | PropertyUtil.intToIntMap(PropertyUtil.getSubProperties(properties, "duration")) 42 | ); 43 | } 44 | 45 | public int getGreatestUsedFrame() { 46 | Set frames = new HashSet<>(frameMapping.keySet()); 47 | frames.addAll(frameDurations.keySet()); 48 | 49 | int greatestFrame = 0; 50 | for (int frame : frames) { 51 | greatestFrame = Math.max(frame, greatestFrame); 52 | } 53 | 54 | return greatestFrame; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/config/AnimaticaConfig.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.config; 2 | 3 | import io.github.foundationgames.animatica.Animatica; 4 | import net.fabricmc.loader.api.FabricLoader; 5 | import net.minecraft.client.MinecraftClient; 6 | import net.minecraft.client.option.SimpleOption; 7 | 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.util.Properties; 12 | 13 | public class AnimaticaConfig { 14 | public static String ANIMATED_TEXTURES_KEY = "animated_textures"; 15 | 16 | public static final String FILE_NAME = "animatica.properties"; 17 | 18 | private final SimpleOption animatedTexturesOption; 19 | public boolean animatedTextures; 20 | 21 | public AnimaticaConfig() { 22 | try { 23 | load(); 24 | } catch (IOException e) { 25 | Animatica.LOG.error("Error loading config during initialization!", e); 26 | } 27 | 28 | this.animatedTexturesOption = SimpleOption.ofBoolean( 29 | "option.animatica.animated_textures", 30 | this.animatedTextures, 31 | value -> { 32 | this.animatedTextures = value; 33 | try { 34 | this.save(); 35 | } catch (IOException e) { Animatica.LOG.error("Error saving config while changing in game!", e); } 36 | MinecraftClient.getInstance().reloadResources(); 37 | } 38 | ); 39 | } 40 | 41 | public void writeTo(Properties properties) { 42 | properties.put(ANIMATED_TEXTURES_KEY, Boolean.toString(animatedTextures)); 43 | } 44 | 45 | public void readFrom(Properties properties) { 46 | this.animatedTextures = boolFrom(properties.getProperty(ANIMATED_TEXTURES_KEY), true); 47 | } 48 | 49 | public Path getFile() throws IOException { 50 | var file = FabricLoader.getInstance().getConfigDir().resolve(FILE_NAME); 51 | if (!Files.exists(file)) { 52 | Files.createFile(file); 53 | } 54 | 55 | return file; 56 | } 57 | 58 | public SimpleOption getAnimatedTexturesOption() { 59 | return animatedTexturesOption; 60 | } 61 | 62 | public void save() throws IOException { 63 | var file = getFile(); 64 | var properties = new Properties(); 65 | 66 | writeTo(properties); 67 | 68 | try (var out = Files.newOutputStream(file)) { 69 | properties.store(out, "Configuration file for Animatica"); 70 | } 71 | } 72 | 73 | public void load() throws IOException { 74 | var file = getFile(); 75 | var properties = new Properties(); 76 | 77 | try (var in = Files.newInputStream(file)) { 78 | properties.load(in); 79 | } 80 | 81 | readFrom(properties); 82 | } 83 | 84 | private static boolean boolFrom(String s, boolean defaultVal) { 85 | return s == null ? defaultVal : "true".equals(s); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/mixin/IdentifierMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.mixin; 2 | 3 | import io.github.foundationgames.animatica.Animatica; 4 | import io.github.foundationgames.animatica.util.Flags; 5 | import net.minecraft.util.Identifier; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.injection.At; 8 | import org.spongepowered.asm.mixin.injection.Inject; 9 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 11 | 12 | // Allows invalid characters in paths to support packs with extremely outdated formatting (because OptiFine does too) 13 | @Mixin(Identifier.class) 14 | public class IdentifierMixin { 15 | @Inject(method = "([Ljava/lang/String;)V", at = @At("TAIL")) 16 | private void animatica$reportInvalidIdentifierCharacters(String[] id, CallbackInfo ci) { 17 | if (Flags.ALLOW_INVALID_ID_CHARS && !animatica$isPathAllowed(id[1]) && !id[1].startsWith("~/")) { 18 | Animatica.LOG.warn("Legacy resource pack is using an invalid namespaced identifier '{}:{}'! DO NOT use non [a-z0-9_.-] characters for resource pack files and file names!", id[0], id[1]); 19 | } 20 | } 21 | 22 | @Inject(method = "isPathCharacterValid", at = @At("RETURN"), cancellable = true) 23 | private static void animatica$allowInvalidCharacters(char character, CallbackInfoReturnable cir) { 24 | if (Flags.ALLOW_INVALID_ID_CHARS) { 25 | cir.setReturnValue(true); 26 | } 27 | } 28 | 29 | private static boolean animatica$isPathAllowed(String path) { 30 | if (path == null) return true; 31 | for (char c : path.toCharArray()) { 32 | if (!(c == '_' || c == '-' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '/' || c == '.')) { 33 | return false; 34 | } 35 | } 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/mixin/RenderSystemMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.mixin; 2 | 3 | import com.mojang.blaze3d.systems.RenderSystem; 4 | import io.github.foundationgames.animatica.Animatica; 5 | import io.github.foundationgames.animatica.animation.AnimationLoader; 6 | import net.minecraft.util.Identifier; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.ModifyVariable; 10 | 11 | @Mixin(RenderSystem.class) 12 | public class RenderSystemMixin { 13 | @ModifyVariable(method = "_setShaderTexture(ILnet/minecraft/util/Identifier;)V", at = @At("HEAD"), index = 1) 14 | private static Identifier animatica$replaceWithAnimatedTexture(Identifier old) { 15 | if (Animatica.CONFIG.animatedTextures) { 16 | var anim = AnimationLoader.INSTANCE.getAnimationId(old); 17 | if (anim != null) { 18 | return anim; 19 | } 20 | } 21 | return old; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/mixin/VideoOptionsScreenMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.mixin; 2 | 3 | import io.github.foundationgames.animatica.Animatica; 4 | import net.minecraft.client.gui.screen.Screen; 5 | import net.minecraft.client.gui.screen.option.VideoOptionsScreen; 6 | import net.minecraft.client.option.SimpleOption; 7 | import net.minecraft.text.Text; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.injection.At; 10 | import org.spongepowered.asm.mixin.injection.ModifyArg; 11 | 12 | @Mixin(VideoOptionsScreen.class) 13 | public abstract class VideoOptionsScreenMixin extends Screen { 14 | protected VideoOptionsScreenMixin(Text title) { 15 | super(title); 16 | } 17 | 18 | @ModifyArg( 19 | method = "init", 20 | at = @At( 21 | value = "INVOKE", 22 | target = "Lnet/minecraft/client/gui/widget/OptionListWidget;addAll([Lnet/minecraft/client/option/SimpleOption;)V" 23 | ), 24 | index = 0 25 | ) 26 | private SimpleOption[] animatica$addTextureAnimationOptionButton(SimpleOption[] old) { 27 | var options = new SimpleOption[old.length + 1]; 28 | System.arraycopy(old, 0, options, 0, old.length); 29 | options[options.length - 1] = Animatica.CONFIG.getAnimatedTexturesOption(); 30 | return options; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/util/Flags.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.util; 2 | 3 | public enum Flags {; 4 | public static volatile boolean ALLOW_INVALID_ID_CHARS = false; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/util/PropertyUtil.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.util; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import io.github.foundationgames.animatica.util.exception.InvalidPropertyException; 5 | import io.github.foundationgames.animatica.util.exception.MissingPropertyException; 6 | import io.github.foundationgames.animatica.util.exception.PropertyParseException; 7 | import net.minecraft.util.Identifier; 8 | 9 | import java.util.Map; 10 | import java.util.Properties; 11 | 12 | public enum PropertyUtil {; 13 | public static String get(Identifier file, Properties properties, String key) throws PropertyParseException { 14 | var p = properties.getProperty(key); 15 | if (p == null) { 16 | throw new MissingPropertyException(file, key); 17 | } 18 | return p; 19 | } 20 | 21 | public static Properties getSubProperties(Properties properties, String key) { 22 | var p = new Properties(); 23 | final var prefix = key + "."; 24 | for (String k : properties.stringPropertyNames()) { 25 | if (k.startsWith(prefix)) { 26 | var newKey = k.replaceFirst(prefix, ""); 27 | p.setProperty(newKey, properties.getProperty(k)); 28 | } 29 | } 30 | return p; 31 | } 32 | 33 | public static int getInt(Identifier file, Properties properties, String key) throws PropertyParseException { 34 | int r; 35 | try { 36 | r = Integer.parseInt(get(file, properties, key)); 37 | } catch (NumberFormatException ignored) { 38 | throw new InvalidPropertyException(file, key, "integer (whole number)"); 39 | } 40 | return r; 41 | } 42 | 43 | public static int getIntOr(Identifier file, Properties properties, String key, int defaultVal) throws PropertyParseException { 44 | var p = properties.getProperty(key); 45 | if (p == null) { 46 | return defaultVal; 47 | } 48 | int r; 49 | try { 50 | r = Integer.parseInt(p); 51 | } catch (NumberFormatException ignored) { 52 | throw new InvalidPropertyException(file, key, "integer"); 53 | } 54 | return r; 55 | } 56 | 57 | public static boolean getBoolOr(Identifier file, Properties properties, String key, boolean defaultVal) throws PropertyParseException { 58 | var p = properties.getProperty(key); 59 | if (p == null) { 60 | return defaultVal; 61 | } 62 | if ("false".equals(p) || "true".equals(p)) { 63 | return "true".equals(p); 64 | } 65 | throw new InvalidPropertyException(file, key, "boolean (false/true)"); 66 | } 67 | 68 | public static Map intToIntMap(Properties in) { 69 | var builder = ImmutableMap.builder(); 70 | for (String k : in.stringPropertyNames()) { 71 | try { 72 | builder.put(Integer.parseInt(k), Integer.parseInt(in.getProperty(k))); 73 | } catch (NumberFormatException ignored) {} 74 | } 75 | return builder.build(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/util/TextureUtil.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.util; 2 | 3 | import net.minecraft.client.texture.NativeImage; 4 | import net.minecraft.util.math.MathHelper; 5 | 6 | public enum TextureUtil {; 7 | 8 | /** 9 | * Copy a section of an image into another image 10 | * 11 | * @param src The source image to copy from 12 | * @param u The u coordinate on the source image to start the selection from 13 | * @param v The v coordinate on the source image to start the selection from 14 | * @param w The width of the selection area 15 | * @param h The height of the selection area 16 | * @param dest The destination image to copy to 17 | * @param du The u coordinate on the destination image to place the selection at 18 | * @param dv The v coordinate on the destination image to place the selection at 19 | */ 20 | public static void copy(NativeImage src, int u, int v, int w, int h, NativeImage dest, int du, int dv) { 21 | // iterate through the entire section of the image to be copied over 22 | for (int rx = 0; rx < w; rx++) { 23 | for (int ry = 0; ry < h; ry++) { 24 | // the current x/y coordinates in the source image 25 | int srcX = u + rx; 26 | int srcY = v + ry; 27 | // the corresponding target x/y coordinates in the target image 28 | int trgX = du + rx; 29 | int trgY = dv + ry; 30 | 31 | // set the color of the target pixel on the destination image 32 | // to the color from the corresponding pixel on the source image 33 | dest.setColor(trgX, trgY, src.getColor(srcX, srcY)); 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * Copy a blend between 2 sections on a source image to a destination image 40 | * 41 | * @param src The source image to copy from 42 | * @param u0 The u coordinate on the source image to start the first selection from 43 | * @param v0 The v coordinate on the source image to start the first selection from 44 | * @param u1 The u coordinate on the source image to start the second selection from 45 | * @param v1 The v coordinate on the source image to start the second selection from 46 | * @param w The width of the selection area 47 | * @param h The height of the selection area 48 | * @param dest The destination image to copy to 49 | * @param du The u coordinate on the destination image to place the selection at 50 | * @param dv The v coordinate on the destination image to place the selection at 51 | * @param blend The blend between the first selection from the source and the 52 | * second (0 = solid first image, 1 = solid second image) 53 | */ 54 | public static void blendCopy(NativeImage src, int u0, int v0, int u1, int v1, int w, int h, NativeImage dest, int du, int dv, float blend) { 55 | // iterate through the entire section of the image to be copied over 56 | for (int rx = 0; rx < w; rx++) { 57 | for (int ry = 0; ry < h; ry++) { 58 | // the first set of x/y coordinates in the source image 59 | int srcX0 = u0 + rx; 60 | int srcY0 = v0 + ry; 61 | // the second set of x/y coordinates in the source image 62 | int srcX1 = u1 + rx; 63 | int srcY1 = v1 + ry; 64 | // the corresponding target x/y coordinates in the target image 65 | int trgX = du + rx; 66 | int trgY = dv + ry; 67 | 68 | // set the color of the target pixel on the destination image to a blend 69 | // of the colors from the corresponding pixels on the source image 70 | dest.setColor(trgX, trgY, lerpColor(src.getFormat(), src.getColor(srcX0, srcY0), src.getColor(srcX1, srcY1), blend)); 71 | } 72 | } 73 | } 74 | 75 | public static int lerpColor(NativeImage.Format format, int c1, int c2, float delta) { 76 | int a1 = (c1 >> format.getAlphaOffset()) & 0xFF; 77 | int r1 = (c1 >> format.getRedOffset()) & 0xFF; 78 | int g1 = (c1 >> format.getGreenOffset()) & 0xFF; 79 | int b1 = (c1 >> format.getBlueOffset()) & 0xFF; 80 | 81 | int a2 = (c2 >> format.getAlphaOffset()) & 0xFF; 82 | int r2 = (c2 >> format.getRedOffset()) & 0xFF; 83 | int g2 = (c2 >> format.getGreenOffset()) & 0xFF; 84 | int b2 = (c2 >> format.getBlueOffset()) & 0xFF; 85 | 86 | // If the first or second color is transparent, 87 | // don't lerp any leftover rgb values and instead 88 | // only use those of the non-transparent color 89 | if (a1 <= 0) { 90 | r1 = r2; 91 | g1 = g2; 92 | b1 = b2; 93 | } else if (a2 <= 0) { 94 | r2 = r1; 95 | g2 = g1; 96 | b2 = b1; 97 | } 98 | 99 | int oa = MathHelper.lerp(delta, a1, a2); 100 | int or = MathHelper.lerp(delta, r1, r2); 101 | int og = MathHelper.lerp(delta, g1, g2); 102 | int ob = MathHelper.lerp(delta, b1, b2); 103 | 104 | return (oa << format.getAlphaOffset()) | (or << format.getRedOffset()) | (og << format.getGreenOffset()) | (ob << format.getBlueOffset()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/util/Utilities.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.util; 2 | 3 | import net.minecraft.util.Identifier; 4 | 5 | public enum Utilities {; 6 | public static Identifier processPath(Identifier fileRelativeTo, Identifier path) { 7 | if (path.getPath().startsWith("./")) { 8 | int lInd = fileRelativeTo.getPath().lastIndexOf("/"); 9 | if (lInd > 0) { 10 | var builder = new StringBuilder(fileRelativeTo.getPath()); 11 | builder.replace(lInd, builder.length(), path.getPath().replaceFirst("\\./", "/")); 12 | return new Identifier(fileRelativeTo.getNamespace(), builder.toString()); 13 | } 14 | } else if (path.getPath().startsWith("~/")) { 15 | return new Identifier(path.getNamespace(), path.getPath().replaceFirst("~/", "optifine/")); 16 | } 17 | return path; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/util/exception/InvalidPropertyException.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.util.exception; 2 | 3 | import net.minecraft.util.Identifier; 4 | 5 | public class InvalidPropertyException extends PropertyParseException { 6 | public InvalidPropertyException(Identifier file, String key, String expectedType) { 7 | super(String.format("Property '%s' in file '%s' expected to be of type: %s", key, file, expectedType)); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/util/exception/MissingPropertyException.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.util.exception; 2 | 3 | import net.minecraft.util.Identifier; 4 | 5 | public class MissingPropertyException extends PropertyParseException { 6 | public MissingPropertyException(Identifier file, String key) { 7 | super(String.format("Expected property '%s' in file '%s'", key, file)); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/github/foundationgames/animatica/util/exception/PropertyParseException.java: -------------------------------------------------------------------------------- 1 | package io.github.foundationgames.animatica.util.exception; 2 | 3 | public abstract class PropertyParseException extends Exception { 4 | public PropertyParseException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/animatica.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "io.github.foundationgames.animatica.mixin", 5 | "compatibilityLevel": "JAVA_16", 6 | "client": [ 7 | "RenderSystemMixin", 8 | "IdentifierMixin", 9 | "VideoOptionsScreenMixin" 10 | ], 11 | "injectors": { 12 | "defaultRequire": 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/assets/animatica/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundationGames/Animatica/2d08028eb0ae350f096c32f324a852770a4bc710/src/main/resources/assets/animatica/icon.png -------------------------------------------------------------------------------- /src/main/resources/assets/animatica/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "option.animatica.animated_textures": "Custom Animations" 3 | } -------------------------------------------------------------------------------- /src/main/resources/assets/animatica/lang/fr_fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "option.animatica.animated_textures": "Animations Personalisées" 3 | } 4 | -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "animatica", 4 | "version": "${version}", 5 | 6 | "name": "Animatica", 7 | "description": "Allow for animating more game textures using the MCPatcher/OptiFine animation format", 8 | "authors": [ 9 | "FoundationGames" 10 | ], 11 | "contact": { 12 | "homepage": "https://github.com/FoundationGames/Animatica", 13 | "sources": "https://github.com/FoundationGames/Animatica" 14 | }, 15 | 16 | "license": "LGPLv3", 17 | "icon": "assets/animatica/icon.png", 18 | 19 | "environment": "client", 20 | "entrypoints": { 21 | "client": [ 22 | "io.github.foundationgames.animatica.Animatica" 23 | ] 24 | }, 25 | "mixins": [ 26 | "animatica.mixins.json" 27 | ], 28 | "depends": { 29 | "fabricloader": ">=0.11.3", 30 | "fabric": "*", 31 | "minecraft": ["1.19.4", "1.20.x"], 32 | "java": ">=16" 33 | } 34 | } 35 | --------------------------------------------------------------------------------