diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eee3e0a --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +* +!src +!src/** +!.gitignore +!build.gradle +!gradle.properties +!gradlew +!gradlew.bat +!LICENSE +!readme.md +!settings.gradle \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8175492 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2022 TopchetoEU +Copyright (c) 2020 Caden Kriese + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..3ee691b --- /dev/null +++ b/build.gradle @@ -0,0 +1,100 @@ +plugins { + id 'fabric-loom' version '0.11-SNAPSHOT' + id 'maven-publish' +} + +sourceCompatibility = JavaVersion.VERSION_17 +targetCompatibility = JavaVersion.VERSION_17 + +archivesBaseName = project.archives_base_name +version = project.mod_version as Object +group = project.maven_group as Object + +repositories { + maven { + url "https://maven.terraformersmc.com/releases" + } +} + +dependencies { + //to change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. + //TODO try this approach to fabric api in the future to avoid unneeded modules. + // for some reason it asks for like fabric-biomes and stuff right now. +// Set apiModules = [ +// "fabric-api-base", +// "fabric-registry-sync-v0" +// ] +// + // Add each module as a dependency +// apiModules.forEach { +// modImplementation(fabricApi.module(it, project.fabric_version)) +// } + + // Fabric API. + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + + // Cloth Config / Autoconfig + // modImplementation("me.shedaniel.cloth:config-2:${project.cloth_version}") { + // exclude(group: "net.fabricmc.fabric-api") + // exclude(module: "modmenu") + // } + // modImplementation("me.sargunvohra.mcmods:autoconfig1u:${project.autoconfig_version}") { + // exclude(group: "net.fabricmc.fabric-api") + // } + // include("me.shedaniel.cloth:config-2:${project.cloth_version}") + // include("me.sargunvohra.mcmods:autoconfig1u:${project.autoconfig_version}") + + modImplementation("com.terraformersmc:modmenu:${project.modmenu_version}") +} + + +processResources { + inputs.property "version", project.version + + filesMatching("fabric.mod.json") { + expand "version": project.version + } +} + +tasks.withType(JavaCompile).configureEach { + // Minecraft 1.18 (1.18-pre2) upwards uses Java 17. + it.options.release = 17 +} + +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() +} + +jar { + from("LICENSE") { + rename { "${it}_${project.archivesBaseName}"} + } +} + +// configure the maven publication +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + } + } + + // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. + repositories { + // Add repositories to publish to here. + // Notice: This block does NOT have the same function as the block in the top level. + // The repositories here will be used for publishing your artifact, not for + // retrieving dependencies. + } +} +loom { + accessWidenerPath = file("src/main/resources/smooth-chunks.accesswidener") +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..4e2f599 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,20 @@ +# Check for updates on https://modmuss50.me/fabric.html +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx4G + +# Fabric Properties +minecraft_version=1.18.2 +yarn_mappings=1.18.2+build.1 +loader_version=0.13.3 + +# Mod Properties +mod_version=0.1.0 +maven_group=me.topchetoeu +archives_base_name=smooth-chunks + +# Dependencies +fabric_version=0.47.8+1.18.2 + +autoconfig_version=3.3.1 +cloth_version=3.2.24 +modmenu_version=3.2.1 diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$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" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; +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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + 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 +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 +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 + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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\"" + fi + i=`expr $i + 1` + 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, 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" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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%" == "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%"=="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 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..912cde2 --- /dev/null +++ b/readme.md @@ -0,0 +1,27 @@ +# Smooth chunks + +This is a fork of [flogic](https://github.com/cadenkriese/smooth-chunks)'s mod (although code bases have nothing in common) + +## What is this? + +This is a mod that adds animations of currently loading chunks. This makes chunk loading generally seem much more pleasant than them appearing out of thin air. There are multiple built-in animations and ease types, and if this isnt't enough, there's an API which will allow you to add your own animations and eases (with ease) + +Generally, this is what you'd call an "eye-candy" mod. + +## Currently supported animations: + +- Rise +- Fall +- Scale + +## Currently supported interpolation types: + +- Linear +- Sine +- Quadratic +- Elastic + +## Future plans: + +In the future, I plan to extend the scope of this mod to not only smoothen the chunk loading, but entity spawning / despawning / dying, liquid flowing, crops growing, etc. + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..5b60df3 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + jcenter() + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/Descriptor.java b/src/main/java/me/topchetoeu/smoothchunks/Descriptor.java new file mode 100644 index 0000000..8e147ea --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/Descriptor.java @@ -0,0 +1,149 @@ +package me.topchetoeu.smoothchunks; + +import org.apache.commons.lang3.Validate; + +public final class Descriptor implements Cloneable { + public static interface StringModifier { + String modify(String original); + } + + private final T val; + private final String name; + private String displayName = null; + private String author = null; + private String description = null; + + /** + * Gets the value this descriptor describes + */ + public T get() { + return val; + } + /** + * Gets the author of the described object (null if not specified) + */ + public String getAuthor() { + return author; + } + /** + * Gets the display name of the described object (null if not specified) + */ + public String getDisplayName() { + return displayName; + } + /** + * Gets the name of the described object (never null) + */ + public String getDescription() { + return description; + } + /** + * Gets the name of the described object (never null) + */ + public String getName() { + return name; + } + + /** + * Gets the author, if not specified, will return a default value + */ + public String getAuthorOrDefault() { + if (author == null) return "Someone"; + return author; + } + /** + * Gets the display name, if not specified, will return a default value + */ + public String getDisplayNameOrDefault() { + if (displayName == null) return name; + return displayName; + } + /** + * Gets the author, if not specified, will return a default value + */ + public String getDescriptionOrDefault() { + if (description == null) return "Something that does stuff (probably)."; + return description; + } + + /** + * Sets the author name + * @param author The new author name (may be null) + * @return The instance upon which this method was called + */ + public Descriptor author(String author) { + this.author = author; + return this; + } + /** + * Sets the author name + * @param modifier The modifier that will be used to gain the new author name value (may not be null) + * @return The instance upon which this method was called + */ + public Descriptor author(StringModifier modifier) { + Validate.notNull(modifier, "modifier may not be null."); + this.author = modifier.modify(this.author); + return this; + } + + /** + * Sets the display name + * @param dn The new display name (may be null) + * @return The instance upon which this method was called + */ + public Descriptor displayName(String dn) { + this.displayName = dn; + return this; + } + /** + * Sets the display name + * @param modifier The modifier that will be used to gain the new display name value (may not be null) + * @return The instance upon which this method was called + */ + public Descriptor displayName(StringModifier modifier) { + Validate.notNull(modifier, "modifier may not be null."); + this.displayName = modifier.modify(this.displayName); + return this; + } + + public Descriptor clone() { + return new Descriptor(val, name) + .author(author) + .description(description) + .displayName(displayName); + } + + /** + * Sets the description + * @param desc The new description (may be null) + * @return The instance upon which this method was called + */ + public Descriptor description(String desc) { + this.description = desc; + return this; + } + /** + * Sets the description + * @param modifier The modifier that will be used to gain the new description value (may not be null) + * @return The instance upon which this method was called + */ + public Descriptor description(StringModifier modifier) { + Validate.notNull(modifier, "modifier may not be null."); + this.description = modifier.modify(this.description); + return this; + } + + @Override + public boolean equals(Object other) { + return other instanceof Descriptor && ((Descriptor)other).name == name; + } + + public Descriptor(T val, String name) { + Validate.notNull(val, "val may not be null."); + Validate.notNull(name, "name may not be null."); + + this.val = val; + this.name = name; + + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/Manager.java b/src/main/java/me/topchetoeu/smoothchunks/Manager.java new file mode 100644 index 0000000..d6327ac --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/Manager.java @@ -0,0 +1,82 @@ +package me.topchetoeu.smoothchunks; + +import java.util.Collection; +import java.util.Collections; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.commons.lang3.Validate; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; + +public final class Manager { + public static interface RegisterEvent { + public void register(Manager manager); + } + public static final Event> createEvent() { + return EventFactory.createArrayBacked(RegisterEvent.class, (listeners) -> (manager) -> { + for (RegisterEvent listener: listeners) { + listener.register(manager); + } + }); + } + + private String currName = null; + private Map> objects = new Hashtable<>(); + + /** + * Registers an object (throws if an object of the same name already exists) + * @param obj The object to register (may not be null) + */ + public void register(Descriptor obj) { + Validate.notNull(obj, "obj may not be null."); + + if (objects.containsKey(obj.getName())) throw new RuntimeException("The ease %s already exists.".formatted(obj.getName())); + objects.put(obj.getName(), new Descriptor(obj.get(), obj.getName()) + .displayName(obj.getDisplayName()) + .author(obj.getAuthor()) + .description(obj.getDescription()) + ); + } + + /** + * Gets an object by its name (null if such an object isn't registered) + */ + public Descriptor get(String name) { + return objects.get(name); + } + /** + * Gets the currently used object's descriptor (never null) + */ + public Descriptor get() { + return objects.get(currName); + } + /** + * Gets the currently used object (never null) + */ + public T getValue() { + return objects.get(currName).get(); + } + /** + * Set the currently used object by its name + * @param name The name of the animation to use (throws if the name doesn't correspond to a registered animation) + */ + public void set(String name) { + Validate.notNull(name, "name may not be null."); + if (!objects.containsKey(name)) throw new RuntimeException("The ease %s doesn't exist.".formatted(name)); + this.currName = name; + } + + /** + * Gets all the currently registered objects + */ + public Collection> getAll() { + return Collections.unmodifiableCollection(objects.values()); + } + + public Manager(T _default) { + register(new Descriptor<>(_default, "default").displayName("Default").author("TopchetoEU")); + set("default"); + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/SmoothChunks.java b/src/main/java/me/topchetoeu/smoothchunks/SmoothChunks.java new file mode 100644 index 0000000..2da51db --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/SmoothChunks.java @@ -0,0 +1,145 @@ +package me.topchetoeu.smoothchunks; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; + +import me.topchetoeu.smoothchunks.Manager.RegisterEvent; +import me.topchetoeu.smoothchunks.animation.Animation; +import me.topchetoeu.smoothchunks.animation.FallAnimation; +import me.topchetoeu.smoothchunks.animation.FlyInAnimation; +import me.topchetoeu.smoothchunks.animation.ProgressManager; +import me.topchetoeu.smoothchunks.animation.RiseAnimation; +import me.topchetoeu.smoothchunks.animation.ScaleAnimation; +import me.topchetoeu.smoothchunks.easing.Ease; +import me.topchetoeu.smoothchunks.easing.ElasticEase; +import me.topchetoeu.smoothchunks.easing.LinearEase; +import me.topchetoeu.smoothchunks.easing.SineEase; +import me.topchetoeu.smoothchunks.gui.SmoothChunksScreen; +import me.topchetoeu.smoothchunks.easing.QuadraticEase; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientChunkEvents; +import net.fabricmc.fabric.api.event.Event; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.util.math.BlockPos; + +public final class SmoothChunks implements ClientModInitializer, ModMenuApi { + private static SmoothChunks instance; + public static SmoothChunks getInstance() { + return instance; + } + + /** + * An event, fired once, when eases are being registered + */ + public final Event> EASES_REGISTERING = Manager.createEvent(); + /** + * An event, fired once, when animations are being registered + */ + public final Event> ANIMATIONS_REGISTERING = Manager.createEvent(); + + private ProgressManager progress; + private Manager ease; + private Manager animation; + + /** + * Gets the chunk progress manager + */ + public ProgressManager getProgressManager() { + return progress; + } + /** + * Gets the animation manager + */ + public Manager getAnimationManager() { + return animation; + } + /** + * Gets the ease manager + */ + public Manager getEaseManager() { + return ease; + } + + private static void registerEases(Manager manager) { + manager.register(new Descriptor<>(new LinearEase(), "linear") + .displayName("Linear") + .author("TopchetoEU") + .description("Animates with an even velocity") + ); + manager.register(new Descriptor<>(new SineEase(), "sine") + .displayName("Sine") + .author("TopchetoEU") + .description("Animation takes off relatively quickly, and ends smoothly. No abrupt ending is noticeable.") + ); + manager.register(new Descriptor<>(new QuadraticEase(), "quad") + .displayName("Quadratic") + .author("TopchetoEU") + .description("Animation takes off quickly, and then slows down. There is a noticeable stop in the animation.") + ); + manager.register(new Descriptor<>(new ElasticEase(), "elastic") + .displayName("Elastic") + .author("TopchetoEU") + .description("Animation takes off very quickly, overshoots, then undershoots, until it reaches the end.") + ); + + manager.set("elastic"); + } + private static void registerAnimations(Manager manager) { + manager.register(new Descriptor<>(new RiseAnimation(), "rise") + .displayName("Rise") + .author("TopchetoEU") + .description("Chunks will rise out of the ground (go from below upwards).") + ); + manager.register(new Descriptor<>(new FallAnimation(), "fall") + .displayName("Fall") + .author("TopchetoEU") + .description("Chunks will fall from the sky (go from above downwards).") + ); + manager.register(new Descriptor<>(new ScaleAnimation(), "scale") + .displayName("Scale") + .author("TopchetoEU") + .description("Chunks will scale up from 0 to normal size.") + ); + manager.register(new Descriptor<>(new FlyInAnimation(), "fly-in") + .displayName("Fly In") + .author("TopchetoEU") + .description("Chunks will slide torwards you until they get to their locations.") + ); + + manager.set("rise"); + } + + @Override public void onInitializeClient() { + instance = this; + + progress = new ProgressManager(); + ease = new Manager<>(x -> 1); + ease.get() + .description("Ends the animation as soon as it has started.") + .displayName("No animation"); + animation = new Manager<>((a, b, c, d, e, f, g, h) -> {}); + animation.get() + .description("Does nothing.") + .displayName("No animation"); + + registerEases(ease); + registerAnimations(animation); + + EASES_REGISTERING.invoker().register(ease); + ANIMATIONS_REGISTERING.invoker().register(animation); + + ClientChunkEvents.CHUNK_UNLOAD.register((world, chunk) -> { + BlockPos pos = chunk.getPos().getStartPos(); + progress.unload(pos.getX(), pos.getY(), pos.getZ()); + }); + } + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return new ConfigScreenFactory() { + @Override + public Screen create(Screen parent) { + return new SmoothChunksScreen(parent); + } + }; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/animation/Animation.java b/src/main/java/me/topchetoeu/smoothchunks/animation/Animation.java new file mode 100644 index 0000000..f872fca --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/animation/Animation.java @@ -0,0 +1,18 @@ +package me.topchetoeu.smoothchunks.animation; + +import net.minecraft.client.util.math.MatrixStack; + +public interface Animation { + /** + * Animations using the currently set ease + * @param progress The point at which the animation currently is (a value between 0 and 1) + * @param matrices The matrix stack used for rendering + * @param chunkX The current chunk's x (in blocks) which is animated + * @param chunkY The current chunk's y (in blocks) which is animated + * @param chunkZ The current chunk's z (in blocks) which is animated + * @param playerX The player's x (in blocks) + * @param playerY The player's y (in blocks) + * @param playerZ The player's z (in blocks) + */ + void animate(float progress, MatrixStack matrices, int chunkX, int chunkY, int chunkZ, float playerX, float playerY, float playerZ); +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/animation/FallAnimation.java b/src/main/java/me/topchetoeu/smoothchunks/animation/FallAnimation.java new file mode 100644 index 0000000..0214755 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/animation/FallAnimation.java @@ -0,0 +1,29 @@ +package me.topchetoeu.smoothchunks.animation; + +import net.minecraft.client.util.math.MatrixStack; + +public class FallAnimation implements Animation { + private float offset; + + public float getOffset() { + return offset; + } + public void setOffset(float offset) { + this.offset = offset; + } + + @Override + public void animate(float progress, MatrixStack matrices, int chunkX, int chunkY, int chunkZ, float playerX, float playerY, float playerZ) { + animate(progress, matrices); + } + public void animate(float progress, MatrixStack matrices) { + matrices.translate(0, offset * (1 - progress), 0); + } + + public FallAnimation(float offset) { + this.offset = offset; + } + public FallAnimation() { + offset = 50; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/animation/FlyInAnimation.java b/src/main/java/me/topchetoeu/smoothchunks/animation/FlyInAnimation.java new file mode 100644 index 0000000..4ffa5ff --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/animation/FlyInAnimation.java @@ -0,0 +1,27 @@ +package me.topchetoeu.smoothchunks.animation; + +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.math.Vec2f; + +public class FlyInAnimation implements Animation { + private float offset; + + public float getOffset() { + return offset; + } + public void setOffset(float offset) { + this.offset = offset; + } + + + @Override + public void animate(float progress, MatrixStack matrices, int chunkX, int chunkY, int chunkZ, float playerX, float playerY, float playerZ) { + Vec2f direction = new Vec2f(playerX, playerZ).add(new Vec2f(-chunkX, -chunkZ)).normalize().multiply(-offset); + + matrices.translate(direction.x * (1 - progress), 0, direction.y * (1 - progress)); + } + + public FlyInAnimation() { + offset = 50; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/animation/ProgressManager.java b/src/main/java/me/topchetoeu/smoothchunks/animation/ProgressManager.java new file mode 100644 index 0000000..fd1bc2d --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/animation/ProgressManager.java @@ -0,0 +1,146 @@ +package me.topchetoeu.smoothchunks.animation; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Hashtable; + +public final class ProgressManager { + public static class ChunkLoc { + public final int x; + public final int y; + public final int z; + + @Override + public boolean equals(Object obj) { + return obj instanceof ChunkLoc && ((ChunkLoc)obj).x == x && ((ChunkLoc)obj).y == y && ((ChunkLoc)obj).z == z; + } + + @Override + public int hashCode() { + return 137 * x + 149 * y + 163 * z; + } + + public ChunkLoc(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + } + + private Hashtable chunkToStage = new Hashtable<>(); + private HashSet chunks = new HashSet<>(); + private float duration = 1; + + public ProgressManager() { + + } + + public float getDuration() { + return duration; + } + public void setDuration(float duration) { + this.duration = duration; + } + + /** + * Removes all loaded chunks (called when a world is unloaded and loaded) + */ + public void reset() { + chunkToStage.clear(); + chunks.clear(); + } + /** + * Advances the animation for all tracked chunks, according to the duration and the specified delta + * @param delta The second delta to advance the animation by + */ + public void tick(float delta) { + for (ChunkLoc loc : new ArrayList<>(chunkToStage.keySet())) { + float val = chunkToStage.get(loc); + val += delta / duration; + if (val > 1f) chunkToStage.remove(loc); + chunkToStage.put(loc, val); + } + } + + /** + * Loads a specified chunk (starts tracking its animation) + * @param x The x of the chunk + * @param y The y of the chunk + * @param y The z of the chunk + */ + public void load(int x, int y, int z) { + if (isChunkLoaded(x, y, z)) return; + ChunkLoc loc = new ChunkLoc(x, y, z); + chunks.add(loc); + chunkToStage.put(loc, 0f); + } + /** + * Unloads a specified chunk (stops tracking its animation) + * @param x The x of the chunk + * @param y The y of the chunk + * @param y The z of the chunk + */ + public void unload(int x, int y, int z) { + ChunkLoc loc = new ChunkLoc(x, y, z); + chunkToStage.remove(loc); + chunks.remove(loc); + } + /** + * Unloads all loaded chunks + */ + public void unloadAll() { + chunkToStage.clear(); + chunks.clear(); + } + /** + * Unloads all the chunks that are outside the render distance specified + * @param viewDistance The view distance (in chunks, radius) + * @param playerChunkX The x coordinate of the chunk in which the player stands + * @param playerChunkY The y coordinate of the chunk in which the player stands + * @param playerChunkZ The z coordinate of the chunk in which the player stands + */ + public void unloadAllFar(int viewDistance, int playerChunkX, int playerChunkY, int playerChunkZ) { + float circleVD = viewDistance + 1.38f; + int squareVD = viewDistance; + + for (ChunkLoc loc : new ArrayList<>(chunks)) { + int chunkX = loc.x / 16; + int chunkZ = loc.z / 16; + + int diffSquareX = playerChunkX - chunkX ; + int diffSquareZ = playerChunkZ - chunkZ; + + int diffCircleX = playerChunkX - chunkX; + int diffCircleZ = playerChunkZ - chunkZ; + + int dist = diffCircleX * diffCircleX + diffCircleZ * diffCircleZ; + + if (dist > circleVD * circleVD) unload(loc.x, loc.y, loc.z); + if (Math.abs(diffSquareX) > squareVD || Math.abs(diffSquareZ) > squareVD) unload(loc.x, loc.y, loc.z); + } + } + + /** + * Checks whether or not a chunk is loaded + * @param x The x of the chunk + * @param y The y of the chunk + * @param y The z of the chunk + * @return A value indicating whether or not the specified chunk is tracked + */ + public boolean isChunkLoaded(int x, int y, int z) { + return chunks.contains(new ChunkLoc(x, y, z)); + } + + /** + * Gets the animation progress of the specified chunk + * @param x The x of the chunk + * @param y The y of the chunk + * @param y The z of the chunk + * @return A float value from 0 to 1 (0 if the chunk is not tracked), indicating the animation progress of the specified chunk + */ + public float getChunkProgress(int x, int y, int z) { + if (!isChunkLoaded(x, y, z)) return 0f; + if (!chunkToStage.containsKey(new ChunkLoc(x, y, z))) return 1f; + return chunkToStage.get(new ChunkLoc(x, y, z)); + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/animation/RiseAnimation.java b/src/main/java/me/topchetoeu/smoothchunks/animation/RiseAnimation.java new file mode 100644 index 0000000..15185c9 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/animation/RiseAnimation.java @@ -0,0 +1,29 @@ +package me.topchetoeu.smoothchunks.animation; + +import net.minecraft.client.util.math.MatrixStack; + +public final class RiseAnimation implements Animation { + private float offset; + + public float getOffset() { + return offset; + } + public void setOffset(float offset) { + this.offset = offset; + } + + @Override + public void animate(float progress, MatrixStack matrices, int chunkX, int chunkY, int chunkZ, float playerX, float playerY, float playerZ) { + animate(progress, matrices); + } + public void animate(float progress, MatrixStack matrices) { + matrices.translate(0, offset * (progress - 1), 0); + } + + public RiseAnimation(float offset) { + this.offset = offset; + } + public RiseAnimation() { + offset = 50; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/animation/ScaleAnimation.java b/src/main/java/me/topchetoeu/smoothchunks/animation/ScaleAnimation.java new file mode 100644 index 0000000..a5bd353 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/animation/ScaleAnimation.java @@ -0,0 +1,50 @@ +package me.topchetoeu.smoothchunks.animation; + +import net.minecraft.client.util.math.MatrixStack; + +public class ScaleAnimation implements Animation { + private boolean scaleY = true; + private float xOffset = 8, yOffset = 8, zOffset = 8; + + public boolean isScalingY() { + return scaleY; + } + public void setScalingY(boolean scaleY) { + this.scaleY = scaleY; + } + + public float getXOffset() { + return xOffset; + } + public void setXOffset(float xOffset) { + this.xOffset = xOffset; + } + + public float getYOffset() { + return yOffset; + } + public void setYOffset(float yOffset) { + this.yOffset = yOffset; + } + + public float getZOffset() { + return zOffset; + } + public void setZOffset(float zOffset) { + this.zOffset = zOffset; + } + + @Override + public void animate(float progress, MatrixStack matrices, int chunkX, int chunkY, int chunkZ, float playerX, float playerY, float playerZ) { + float scaleX = progress; + float scaleZ = progress; + + matrices.translate(xOffset, yOffset, zOffset); + matrices.scale(scaleX, 1, scaleZ); + matrices.translate(-xOffset, -yOffset, -zOffset); + } + + public ScaleAnimation() { + + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/easing/Ease.java b/src/main/java/me/topchetoeu/smoothchunks/easing/Ease.java new file mode 100644 index 0000000..4948b59 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/easing/Ease.java @@ -0,0 +1,34 @@ +package me.topchetoeu.smoothchunks.easing; + +public interface Ease { + /** + * Converts the linear progress of an animation to an eased progress + * @param x The progress of the animation being eased + * @return The new, eased progress + */ + float ease(float x); + + /** + * Converts a function to an ease out version of itself. + * Mathematically, the function is being "rotated" 180 degrees + * @param func The function to convert + * @return The ease out version of the function + */ + public static Ease easeOut(Ease func) { + return x -> 1 - func.ease(1 - x); + } + /** + * Converts a function to an ease in-out version of itself. + * Mathematically, the function is being split into two, where in the interval [0; 0.5], the ease-out function is being used, and in the other interval [0.5; 1], the ease-in function is being used + * @param func The function to convert + * @return The ease in-out version of the function + */ + public static Ease easeInOut(Ease func) { + return x -> { + float x2 = 2 * x; + + if (x < 0.5f) return (1 - func.ease(1 - x2)) / 2; + else return (1 + func.ease(x2 - 1)) / 2; + }; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/easing/ElasticEase.java b/src/main/java/me/topchetoeu/smoothchunks/easing/ElasticEase.java new file mode 100644 index 0000000..7c0ab25 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/easing/ElasticEase.java @@ -0,0 +1,29 @@ +package me.topchetoeu.smoothchunks.easing; + +public class ElasticEase implements Ease { + private float steepness = 1; + private int periods = 3; + + public int getPeriods() { + return periods; + } + public void setPeriods(int periods) { + this.periods = periods; + } + + public float getSteepness() { + return steepness; + } + public void setSteepness(float steepness) { + this.steepness = steepness; + } + + @Override + public float ease(float x) { + float amplitude = (float)Math.pow(2, -steepness * x) * (1 - x); + float wave = (float)Math.sin(2 * Math.PI * periods * x - Math.PI / 2); + + return amplitude * wave + 1; + } + +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/easing/LinearEase.java b/src/main/java/me/topchetoeu/smoothchunks/easing/LinearEase.java new file mode 100644 index 0000000..293f4c7 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/easing/LinearEase.java @@ -0,0 +1,7 @@ +package me.topchetoeu.smoothchunks.easing; + +public class LinearEase implements Ease { + public float ease(float x) { + return x; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/easing/QuadraticEase.java b/src/main/java/me/topchetoeu/smoothchunks/easing/QuadraticEase.java new file mode 100644 index 0000000..2fe5839 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/easing/QuadraticEase.java @@ -0,0 +1,8 @@ +package me.topchetoeu.smoothchunks.easing; + +public class QuadraticEase implements Ease { + @Override + public float ease(float x) { + return x * x; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/easing/SineEase.java b/src/main/java/me/topchetoeu/smoothchunks/easing/SineEase.java new file mode 100644 index 0000000..f37acbd --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/easing/SineEase.java @@ -0,0 +1,8 @@ +package me.topchetoeu.smoothchunks.easing; + +public class SineEase implements Ease { + @Override + public float ease(float x) { + return (float)Math.sin(x * Math.PI / 2); + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/gui/Boundbox.java b/src/main/java/me/topchetoeu/smoothchunks/gui/Boundbox.java new file mode 100644 index 0000000..0b8f393 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/gui/Boundbox.java @@ -0,0 +1,29 @@ +package me.topchetoeu.smoothchunks.gui; + +public class Boundbox implements BoundboxProvider { + public float x, y, width, height; + + @Override + public float getX() { + return x; + } + @Override + public float getY() { + return y; + } + @Override + public float getWidth() { + return width; + } + @Override + public float getHeight() { + return height; + } + + public Boundbox(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/gui/BoundboxProvider.java b/src/main/java/me/topchetoeu/smoothchunks/gui/BoundboxProvider.java new file mode 100644 index 0000000..a824afe --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/gui/BoundboxProvider.java @@ -0,0 +1,8 @@ +package me.topchetoeu.smoothchunks.gui; + +public interface BoundboxProvider { + float getX(); + float getY(); + float getWidth(); + float getHeight(); +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/gui/Button.java b/src/main/java/me/topchetoeu/smoothchunks/gui/Button.java new file mode 100644 index 0000000..ff857a2 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/gui/Button.java @@ -0,0 +1,164 @@ +package me.topchetoeu.smoothchunks.gui; + +import org.apache.commons.lang3.Validate; +import org.lwjgl.glfw.GLFW; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.DrawableHelper; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.math.ColorHelper.Argb; + +public class Button extends DrawableHelper implements Drawable, Element, Selectable, BoundboxProvider { + public static interface ClickAction { + void onClick(); + } + + public ClickAction clickAction; + public final MinecraftClient client; + private boolean clicked = false; + private boolean focused = false; + private boolean hovered = false; + + + private Text text = Text.of(""); + public int paddingX = 5, paddingY = 2; + public int x, y; + + public float getX() { + return x; + } + public float getY() { + return y; + } + + public float getWidth() { + return 2 + paddingX * 2 + client.textRenderer.getWidth(text); + } + public float getHeight() { + return 1 + paddingY * 2 + client.textRenderer.fontHeight; + } + + public Text getText() { + return text; + } + public void setText(Text text) { + Validate.notNull(text, "text may not be null."); + this.text = text; + } + public void setText(String text) { + Validate.notNull(text, "text may not be null."); + this.text = Text.of(text); + } + + public void click() { + if (clickAction != null) clickAction.onClick(); + } + + @Override + public void appendNarrations(NarrationMessageBuilder msgBuilder) { + msgBuilder.put(NarrationPart.HINT, "Button " + text); + } + + @Override + public SelectionType getType() { + if (focused) return SelectionType.FOCUSED; + if (hovered) return SelectionType.HOVERED; + return SelectionType.NONE; + } + @Override + public boolean changeFocus(boolean lookForwards) { + focused = !focused; + return focused; + } + + @Override + public void render(MatrixStack matrices, int x, int y, float delta) { + int white = Argb.getArgb(255, 255, 255, 255); + matrices.push(); + matrices.translate(this.x, this.y, getZOffset()); + + hovered = isMouseOver(x, y); + + if (hovered) { + fill(matrices, 0, 0, (int)getWidth(), (int)getHeight(), Argb.getArgb(32, 255, 255, 255)); + } + if (clicked) { + fill(matrices, 0, 0, (int)getWidth(), (int)getHeight(), Argb.getArgb(127, 255, 255, 255)); + } + + drawHorizontalLine(matrices, 0, (int)getWidth() - 1, (int)getHeight() - 1, white); + + if (focused) { + drawHorizontalLine(matrices, 0, (int)getWidth() - 1, 0, white); + drawVerticalLine(matrices, 0, 0, (int)getHeight() - 1, white); + drawVerticalLine(matrices, (int)getWidth() - 1, 0, (int)getHeight() - 1, white); + } + + client.textRenderer.draw(matrices, text, paddingX + 1, paddingY + 1, white); + + matrices.pop(); + } + + @Override + public boolean isMouseOver(double x, double y) { + if (clicked) return true; + + x -= this.x; + y -= this.y; + + return x >= 0 && x < getWidth() && y >= 0 && y < getHeight(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_ENTER) { + click(); + clicked = true; + hovered = true; + return true; + } + return false; + } + @Override + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_ENTER) { + clicked = false; + return true; + } + return false; + } + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button != 0) return false; + if (isMouseOver(mouseX, mouseY)) { + clicked = true; + hovered = true; + } + return false; + // return true; + } + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (button != 0) return false; + if (clicked) { + click(); + clicked = false; + return true; + } + return false; + } + + public Button(int x, int y, Text text, ClickAction clickAction) { + this.clickAction = clickAction; + this.client = MinecraftClient.getInstance(); + setText(text); + this.x = x; + this.y = y; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/gui/ChunkPreview.java b/src/main/java/me/topchetoeu/smoothchunks/gui/ChunkPreview.java new file mode 100644 index 0000000..883e96b --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/gui/ChunkPreview.java @@ -0,0 +1,338 @@ +package me.topchetoeu.smoothchunks.gui; + +import org.lwjgl.glfw.GLFW; +import org.lwjgl.opengl.GL11; + +import com.mojang.blaze3d.systems.RenderSystem; + +import me.topchetoeu.smoothchunks.SmoothChunks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.DrawableHelper; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; +import net.minecraft.client.render.BufferBuilder; +import net.minecraft.client.render.BufferRenderer; +import net.minecraft.client.render.GameRenderer; +import net.minecraft.client.render.Tessellator; +import net.minecraft.client.render.VertexFormat; +import net.minecraft.client.render.VertexFormats; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.math.Matrix4f; +import net.minecraft.util.math.Quaternion; +import net.minecraft.util.math.Vec3f; +import net.minecraft.util.math.Vector4f; +import net.minecraft.util.math.ColorHelper.Argb; + +public class ChunkPreview extends DrawableHelper implements Drawable, Element, Selectable, BoundboxProvider { + public static interface ClickAction { + void onClick(); + } + + public ClickAction clickAction; + public final MinecraftClient client; + private boolean clicked = false; + private boolean rotating = false; + private boolean focused = false; + private boolean hovered = false; + private float globalProgress = 0; + private float duration = 0; + private float mouseStartX, mouseStartY; + private float rotX, rotY, scale = 1; + + public float x, y; + public float padding = 5; + public float width = 100, height = 100; + + public float getX() { + return x; + } + public float getY() { + return y; + } + + public float getWidth() { + return width; + } + public float getHeight() { + return height; + } + + public void click() { + refresh(); + } + + @Override + public void appendNarrations(NarrationMessageBuilder msgBuilder) { + msgBuilder.put(NarrationPart.HINT, "Chunk preview"); + } + + @Override + public SelectionType getType() { + if (focused) return SelectionType.FOCUSED; + if (hovered || clicked || rotating) return SelectionType.HOVERED; + return SelectionType.NONE; + } + @Override + public boolean changeFocus(boolean lookForwards) { + focused = !focused; + return focused; + } + + private void refresh() { + globalProgress = 0; + } + + private static boolean checkZ(float z) { + return z < 3; + } + + private static void myFill(MatrixStack matrices, float x1, float y1, float x2, float y2, float a, float r, float g, float b) { + if (x1 < x2) { + float tmp = x1; + x1 = x2; + x2 = tmp; + } + if (y1 < y2) { + float tmp = y1; + y1 = y2; + y2 = tmp; + } + + Vector4f p1 = new Vector4f(x1, y1, 0, 1); + Vector4f p2 = new Vector4f(x1, y2, 0, 1); + Vector4f p3 = new Vector4f(x2, y2, 0, 1); + Vector4f p4 = new Vector4f(x2, y1, 0, 1); + + p1.transform(matrices.peek().getPositionMatrix()); + p2.transform(matrices.peek().getPositionMatrix()); + p3.transform(matrices.peek().getPositionMatrix()); + p4.transform(matrices.peek().getPositionMatrix()); + + p1.multiply(1 / p1.getW()); + p2.multiply(1 / p2.getW()); + p3.multiply(1 / p3.getW()); + p4.multiply(1 / p4.getW()); + + // System.out.println(p1.getZ()); + + if (checkZ(p1.getZ()) && checkZ(p2.getZ()) && checkZ(p3.getZ()) && checkZ(p4.getZ())) { + BufferBuilder bufferBuilder = Tessellator.getInstance().getBuffer(); + RenderSystem.enableBlend(); + RenderSystem.disableCull(); + RenderSystem.disableTexture(); + RenderSystem.defaultBlendFunc(); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + bufferBuilder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR); + bufferBuilder.vertex(p1.getX(), p1.getY(), p1.getZ()).color(r, g, b, a).next(); + bufferBuilder.vertex(p2.getX(), p2.getY(), p2.getZ()).color(r, g, b, a).next(); + bufferBuilder.vertex(p3.getX(), p3.getY(), p3.getZ()).color(r, g, b, a).next(); + bufferBuilder.vertex(p4.getX(), p4.getY(), p4.getZ()).color(r, g, b, a).next(); + bufferBuilder.end(); + BufferRenderer.draw(bufferBuilder); + RenderSystem.enableTexture(); + RenderSystem.disableBlend(); + } + + } + + private final Quaternion rotation = Quaternion.fromEulerXyzDegrees(new Vec3f(-90, 0, 0)); + + private void setupDepth(MatrixStack matrices) { + RenderSystem.enableDepthTest(); + RenderSystem.disableBlend(); + RenderSystem.clearDepth(-1); + RenderSystem.clear(GL11.GL_DEPTH_BUFFER_BIT, true); + RenderSystem.depthFunc(GL11.GL_ALWAYS); + RenderSystem.depthMask(true); + matrices.push(); + matrices.scale(.5f, .5f, 1); + matrices.translate(0, 0, .1f); + if (hovered) { + myFill(matrices, -1, -1, 1, 1, 0.1f, 1, 1, 1); + } + else { + myFill(matrices, -1, -1, 1, 1, 0.004f, 0, 0, 0); + } + matrices.pop(); + RenderSystem.depthMask(false); + RenderSystem.depthFunc(GL11.GL_LESS); + RenderSystem.enableBlend(); + } + private void renderChunk(MatrixStack matrices, int x, int y, int n, float delta) { + matrices.push(); + matrices.translate(x * 16 - 8, 0, y * 16 - 8); + + // x += n; + // y += n; + + float progress = globalProgress / duration - (float)Math.sqrt(x * x + y * y) / (float)n / 2; + if (progress < 0) progress = 0; + if (progress > 1) progress = 1; + if (progress < 0.999) { + float _progress = SmoothChunks.getInstance().getEaseManager().getValue().ease(progress); + SmoothChunks.getInstance().getAnimationManager().getValue().animate( + _progress, matrices, + x * 16, 0, y * 16, 0, 0, 0 + ); + // matrices.translate(0, 0, 16); + } + matrices.translate(0, 0, 16); + matrices.multiply(rotation); + myFill(matrices, 2, 1, 14, 2, progress, 1, 1, 1); + myFill(matrices, 2, 14, 14, 15, progress, 1, 1, 1); + myFill(matrices, 1, 1, 2, 15, progress, 1, 1, 1); + myFill(matrices, 14, 1, 15, 15, progress, 1, 1, 1); + + matrices.pop(); + } + private void renderChunks(MatrixStack matrices, float delta, int n) { + duration = SmoothChunks.getInstance().getProgressManager().getDuration(); + // globalProgress += (lastTime - (lastTime = System.nanoTime())) / -1000000000f; + globalProgress += delta * 0.05f; + matrices.push(); + + matrices.scale(width, width, 1); + matrices.translate(.5f, .5f, 0); + setupDepth(matrices); + + matrices.multiplyPositionMatrix(Matrix4f.viewboxMatrix(75, 1, .00001f, 100)); + matrices.translate(0, 0, -2); + matrices.scale(0.0625f, 0.0625f, 0.0625f); + matrices.scale(1f / (n * 2 + 1), 1f / (n * 2 + 1), 1f / (n * 2 + 1)); + // matrices.multiply(Quaternion.fromEulerXyzDegrees(new Vec3f(150, -45, 0))); + matrices.scale(scale, scale, scale); + + matrices.multiply(Quaternion.fromEulerXyzDegrees(new Vec3f(rotY + 150, 0, 0))); + matrices.multiply(Quaternion.fromEulerXyzDegrees(new Vec3f(0, rotX - 45, 0))); + + // matrices.translate(-8, 0, 8); + + for (int x = -n; x <= n; x++) { + for (int y = -n; y <= n; y++) { + renderChunk(matrices, x, y, n, delta); + } + } + + RenderSystem.depthMask(true); + RenderSystem.disableDepthTest(); + RenderSystem.clearDepth(1); + RenderSystem.clear(GL11.GL_DEPTH_BUFFER_BIT, true); + matrices.pop(); + } + + @Override + public void render(MatrixStack matrices, int x, int y, float delta) { + int white = Argb.getArgb(255, 255, 255, 255); + matrices.push(); + matrices.translate(this.x, this.y, getZOffset()); + + hovered = isMouseOver(x, y); + + renderChunks(matrices, delta, 5); + + drawHorizontalLine(matrices, 0, (int)getWidth() - 1, (int)getHeight() - 1, white); + + if (focused) { + drawHorizontalLine(matrices, 0, (int)getWidth() - 1, 0, white); + drawVerticalLine(matrices, 0, 0, (int)getHeight() - 1, white); + drawVerticalLine(matrices, (int)getWidth() - 1, 0, (int)getHeight() - 1, white); + } + + + matrices.pop(); + } + + @Override + public boolean isMouseOver(double x, double y) { + if (clicked) return true; + + x -= this.x; + y -= this.y; + + return x >= 0 && x < getWidth() && y >= 0 && y < getHeight(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_ENTER) { + click(); + clicked = true; + hovered = true; + return true; + } + return false; + } + @Override + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_ENTER) { + clicked = false; + return true; + } + return false; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + if (isMouseOver(mouseX, mouseY)) { + scale /= Math.pow(2, -amount / 3); + return true; + } + return false; + } + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (isMouseOver(mouseX, mouseY)) { + if (button == 0) { + click(); + clicked = true; + hovered = true; + return true; + } + else if (button == 2) { + rotating = true; + hovered = true; + mouseStartX = (float)mouseX; + mouseStartY = (float)mouseY; + return true; + } + } + return false; + } + @Override + public void mouseMoved(double mouseX, double mouseY) { + if (rotating) { + rotX += mouseStartX - (float)mouseX; + rotY += mouseStartY - (float)mouseY; + mouseStartX = (float)mouseX; + mouseStartY = (float)mouseY; + } + } + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (button == 0 && clicked) { + clicked = false; + return true; + } + if (button == 2 && rotating) { + rotating = false; + return true; + } + return false; + } + + public ChunkPreview(int x, int y) { + this.client = MinecraftClient.getInstance(); + this.x = x; + this.y = y; + } + public ChunkPreview(int x, int y, int w, int h) { + this.client = MinecraftClient.getInstance(); + this.x = x; + this.y = y; + this.width = w; + this.height = h; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/gui/HorizontalSection.java b/src/main/java/me/topchetoeu/smoothchunks/gui/HorizontalSection.java new file mode 100644 index 0000000..99d37e5 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/gui/HorizontalSection.java @@ -0,0 +1,106 @@ +package me.topchetoeu.smoothchunks.gui; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import net.minecraft.client.gui.Element; + +public final class HorizontalSection extends Section { + private class Line { + public Map widths = new Hashtable<>(); + public List elements = new ArrayList<>(); + public float width = 0, height = 0; + } + + private float targetWidth; + + public float getTargetWidth(float width) { + return this.targetWidth; + } + public void setTargetWidth(float width) { + this.targetWidth = width; + recalculate(); + } + + private List getLines() { + Line currLine = new Line(); + List lines = new ArrayList<>(); + + for (Element el : children.get()) { + var box = children.getBoundbox(el); + var x = box.getX() + box.getWidth(); + var y = box.getY() + box.getHeight(); + + if (currLine.width + x > targetWidth) { + lines.add(currLine); + currLine = new Line(); + } + + currLine.width += x; + currLine.elements.add(el); + currLine.widths.put(el, x); + + if (currLine.height < y) currLine.height = y; + } + lines.add(currLine); + + return lines; + } + + private void recalculateLeft(Line line, float offsetY) { + float currX = 0; + for (Element el : line.elements) { + children.offsets.put(el, new Offset(currX, offsetY)); + currX += line.widths.get(el); + } + } + private void recalculateCenter(Line line, float offsetY) { + recalculateLeft(line, offsetY); + + for (Element el : line.elements) { + children.offsets.get(el).x += (targetWidth - line.width) / 2; + } + } + private void recalculateRight(Line line, float offsetY) { + recalculateLeft(line, offsetY); + + for (Element el : line.elements) { + children.offsets.get(el).x += (targetWidth - line.width); + } + } + private void recalculateJustified(Line line, float offsetY) { + recalculateLeft(line, offsetY); + int i = 0; + + if (line.elements.size() < 2) return; + + for (Element el : line.elements) { + children.offsets.get(el).x += (targetWidth - line.width) / (line.elements.size() - 1) * i; + i++; + } + } + + @Override + protected final void recalculate() { + width = targetWidth; + height = title == null ? 0 : mc.textRenderer.fontHeight; + + for (Line line : getLines()) { + if (line.width > targetWidth) { + recalculateLeft(line, height); + } + else { + switch (order) { + case Start: recalculateLeft(line, height); break; + case Middle: recalculateCenter(line, height); break; + case End: recalculateRight(line, height); break; + case Justified: recalculateJustified(line, height); break; + } + } + height += line.height; + if (width < line.width) width = line.width; + } + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/gui/Label.java b/src/main/java/me/topchetoeu/smoothchunks/gui/Label.java new file mode 100644 index 0000000..d346390 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/gui/Label.java @@ -0,0 +1,77 @@ +package me.topchetoeu.smoothchunks.gui; + +import org.apache.commons.lang3.Validate; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.DrawableHelper; +import net.minecraft.client.gui.Element; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.math.ColorHelper.Argb; + +public class Label extends DrawableHelper implements Drawable, Element, BoundboxProvider { + public final MinecraftClient client; + + private Text text = Text.of(""); + private int height = 0; + public int paddingX = 0, paddingY = 0; + public int x, y; + public int maxWidth; + + public Label setMaxWidth(int val) { + maxWidth = val; + return this; + } + + public float getX() { + return x; + } + public float getY() { + return y; + } + + public float getWidth() { + if (maxWidth <= 0) return 2 + paddingX * 2 + client.textRenderer.getWidth(text); + else return 2 + paddingX * 2 + maxWidth; + } + public float getHeight() { + return 1 + paddingY * 2 + height; + } + + public Text getText() { + return text; + } + public void setText(Text text) { + Validate.notNull(text, "text may not be null."); + this.text = text; + } + public void setText(String text) { + Validate.notNull(text, "text may not be null."); + this.text = Text.of(text); + } + + @Override + public void render(MatrixStack matrices, int x, int y, float delta) { + int white = Argb.getArgb(255, 255, 255, 255); + matrices.push(); + matrices.translate(this.x, this.y, getZOffset()); + + if (maxWidth <= 0) { + client.textRenderer.draw(matrices, text, paddingX + 1, paddingY + 1, white); + height = client.textRenderer.fontHeight; + } + else { + height = (int)SelectionScreen.drawWarpedText(client.textRenderer, matrices, text, paddingX + 1, paddingY + 1, maxWidth); + } + + matrices.pop(); + } + + public Label(int x, int y, Text text) { + this.client = MinecraftClient.getInstance(); + setText(text); + this.x = x; + this.y = y; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/gui/Section.java b/src/main/java/me/topchetoeu/smoothchunks/gui/Section.java new file mode 100644 index 0000000..87339a3 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/gui/Section.java @@ -0,0 +1,242 @@ +package me.topchetoeu.smoothchunks.gui; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.AbstractParentElement; +import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.math.ColorHelper.Argb; + +public abstract class Section extends AbstractParentElement implements Drawable, Selectable, BoundboxProvider { + protected class Offset { + public float x, y; + + public Offset(float x, float y) { + this.x = x; + this.y = y; + } + } + + public class ChildList { + protected final Map offsets = new Hashtable<>(); + protected final Map boundboxes = new Hashtable<>(); + protected final List children = new ArrayList<>(); + protected final List selectables = new ArrayList<>(); + protected final List drawables = new ArrayList<>(); + + protected Offset getOffsetAndPos(Object element) { + Offset of = new Offset(offsets.get(element).x, offsets.get(element).y); + of.x += boundboxes.get(element).getX(); + of.y += boundboxes.get(element).getY(); + return of; + } + protected Offset getOffset(Object element) { + return offsets.get(element); + } + public BoundboxProvider getBoundbox(Object element) { + return boundboxes.get(element); + } + + public T addSelectableChild(T element) { + return addSelectableChild(element, element); + } + public T addChild(T element) { + return addChild(element, element); + } + + public T addSelectableChild(T element, BoundboxProvider boundbox) { + this.boundboxes.put(element, boundbox); + this.offsets.put(element, new Offset(0, 0)); + this.drawables.add(element); + this.selectables.add(element); + this.children.add(element); + recalculate(); + return element; + } + public T addChild(T element, BoundboxProvider boundbox) { + this.boundboxes.put(element, boundbox); + this.offsets.put(element, new Offset(0, 0)); + this.drawables.add(element); + this.children.add(element); + recalculate(); + return element; + } + + public List get() { + return Collections.unmodifiableList(children); + } + + public void clear() { + focusedIndex = -1; + offsets.clear(); + boundboxes.clear(); + children.clear(); + selectables.clear(); + drawables.clear(); + } + + private ChildList() {} + } + + public enum OrderType { + Start, + Middle, + End, + Justified, + } + + public final ChildList children = new ChildList(); + public OrderType order = OrderType.Start; + protected int focusedIndex = 0; + protected float width, height; + protected MinecraftClient mc = MinecraftClient.getInstance(); + public Text title; + public float x, y; + + public float getX() { + return x; + } + public float getY() { + return y; + } + + public float getWidth() { + return width; + } + public float getHeight() { + return height; + } + + @Override + public List children() { + return children.get(); + } + + @Override + public Element getFocused() { + if (focusedIndex < 0) return null; + else return children().get(focusedIndex); + } + + @Override + public void setFocused(Element element) { + focusedIndex = children().indexOf(element); + } + + @Override + public void appendNarrations(NarrationMessageBuilder var1) { + } + + protected abstract void recalculate(); + + @Override + public SelectionType getType() { + if (this.children.selectables.stream().anyMatch(v -> v.getType() == SelectionType.HOVERED)) { + return SelectionType.HOVERED; + } + + return SelectionType.NONE; + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + recalculate(); + + matrices.push(); + matrices.translate(x, y, getZOffset()); + + if (title != null) { + mc.textRenderer.draw(matrices, title, 5, 0, Argb.getArgb(255, 255, 255, 255)); + drawHorizontalLine(matrices, 0, (int)width, mc.textRenderer.fontHeight, Argb.getArgb(255, 255, 255, 255)); + } + + for (Drawable d : children.drawables) { + Offset o = children.getOffset(d); + + matrices.push(); + matrices.translate(o.x, o.y, 0); + d.render(matrices, mouseX - (int)o.x - (int)x, mouseY - (int)o.y - (int)y, delta); + matrices.pop(); + } + + matrices.pop(); + } + + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return getType() == SelectionType.HOVERED || (mouseX >= x && mouseX <= width && mouseY >= y && mouseY <= height); + } + + @Override + public Optional hoveredElement(double mouseX, double mouseY) { + for (Selectable element : this.children.selectables) { + var offset = children.getOffsetAndPos(element); + if (element.getType() != SelectionType.HOVERED || !((Element)element).isMouseOver(mouseX - offset.x, mouseY - offset.y)) continue; + return Optional.of((Element)element); + } + return Optional.empty(); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double delta) { + for (Element element : this.children()) { + var offset = children.getOffsetAndPos(element); + if (!element.mouseScrolled(mouseX - offset.x, mouseY - offset.y, delta)) continue; + return true; + } + return false; + } + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + for (Element element : this.children()) { + var offset = children.getOffsetAndPos(element); + if (!element.mouseClicked(mouseX - offset.x, mouseY - offset.y, button)) continue; + this.setFocused(element); + if (button == 0) { + this.setDragging(true); + } + return true; + } + return false; + } + + @Override + public void mouseMoved(double mouseX, double mouseY) { + for (Element element : this.children()) { + var offset = children.getOffsetAndPos(element); + element.mouseMoved(mouseX - offset.x, mouseY - offset.y); + } + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + this.setDragging(false); + + for (Element element : children.get()) { + var offset = children.getOffsetAndPos(element); + if (element.mouseReleased(mouseX - (int)offset.x, mouseY - (int)offset.y, button)) return true; + } + + return false; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + if (this.getFocused() != null && this.isDragging() && button == 0) { + var offset = children.getOffsetAndPos(getFocused()); + return this.getFocused().mouseDragged(mouseX - offset.x, mouseY - offset.y, button, deltaX, deltaY); + } + return false; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/gui/SelectionScreen.java b/src/main/java/me/topchetoeu/smoothchunks/gui/SelectionScreen.java new file mode 100644 index 0000000..d25c754 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/gui/SelectionScreen.java @@ -0,0 +1,134 @@ +package me.topchetoeu.smoothchunks.gui; + +import java.util.LinkedHashMap; +import java.util.Map; + +import me.topchetoeu.smoothchunks.Descriptor; +import me.topchetoeu.smoothchunks.Manager; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.ColorHelper.Argb; + +public class SelectionScreen extends Screen { + public static interface SelectAction { + void onSelect(Descriptor desc); + } + + public final Screen parent; + public final Manager manager; + public SelectAction selectAction; + private Descriptor selectedElement; + private Descriptor hoveredElement = null; + + public Descriptor getSelected() { + return selectedElement; + } + + public static float drawWarpedText(TextRenderer textRenderer, MatrixStack matrices, Text text, int x, int y, int width) { + var lines = textRenderer.wrapLines(text, width); + float offset = 0; + + for (var line : lines) { + textRenderer.draw(matrices, line, x, y + offset, 0xFFFFFFFF); + offset += textRenderer.fontHeight; + } + + return offset; + } + + private float renderElement(MatrixStack matrices, int mouseX, int mouseY, Descriptor element) { + float y = 0; + matrices.push(); + matrices.translate(20, 5, 0); + textRenderer.draw(matrices, new LiteralText(element.getDisplayNameOrDefault()).formatted(Formatting.BOLD), 0, y, 0xFFFFFFFF); + y += textRenderer.fontHeight + 3; + textRenderer.draw(matrices, new LiteralText("Author: " + element.getAuthorOrDefault()), 0, y, 0xFFFFFFFF); + y += textRenderer.fontHeight + 2; + y += drawWarpedText(textRenderer, matrices, new LiteralText(element.getDescriptionOrDefault()).formatted(Formatting.ITALIC), 0, (int)y, width - 40); + y += 5; + matrices.pop(); + + return y; + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + if (parent == null) renderBackground(matrices); + else parent.render(matrices, Integer.MIN_VALUE, Integer.MIN_VALUE, delta); + + fill(matrices, 0, 0, width, height, Argb.getArgb(220, 0, 0, 0)); + + Map, Integer> heights = new LinkedHashMap<>(); + + matrices.push(); + var currDesc = new Descriptor(manager.getValue(), manager.get().getName()) + .author(manager.get().getAuthor()) + .description(manager.get().getDescription()) + .displayName(manager.get().getDisplayNameOrDefault() + " (currently selected)"); + float offset = renderElement(matrices, mouseX, mouseY, currDesc); + heights.put(currDesc, (int)offset); + matrices.translate(0, offset, 0); + for (Descriptor desc : manager.getAll()) { + offset = renderElement(matrices, mouseX, mouseY, desc); + heights.put(desc, (int)offset); + matrices.translate(0, offset, 0); + } + matrices.pop(); + + int y1 = 5; + + for (var pair : heights.entrySet()) { + int y2 = y1 + pair.getValue(); + + if (mouseY >= y1 - 3 && mouseY <= y2 - 5 && mouseX >= 15 && mouseX <= width - 15) { + hoveredElement = pair.getKey(); + drawHorizontalLine(matrices, 15, width - 15, (int)y1 - 3, 0xFFFFFFFF); + drawHorizontalLine(matrices, 15, width - 15, (int)y2 - 5, 0xFFFFFFFF); + drawVerticalLine(matrices, 15, y1 - 3, y2 - 5, 0xFFFFFFFF); + drawVerticalLine(matrices, width - 15, y1 - 3, y2 - 5, 0xFFFFFFFF); + System.out.println(hoveredElement.getName()); + break; + } + + y1 = y2; + } + + super.render(matrices, mouseX, mouseY, delta); + } + + @Override + protected void init() { + super.init(); + parent.init(client, width, height); + } + + @Override + public void close() { + MinecraftClient.getInstance().setScreen(parent); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && hoveredElement != null) { + System.out.print(hoveredElement.getName()); + selectAction.onSelect(hoveredElement); + selectedElement = hoveredElement; + close(); + return true; + } + return false; + } + + public SelectionScreen(Screen parent, Manager manager, SelectAction selectAction) { + super(Text.of("Selection")); + + this.manager = manager; + this.parent = parent; + this.selectAction = selectAction; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/gui/SmoothChunksScreen.java b/src/main/java/me/topchetoeu/smoothchunks/gui/SmoothChunksScreen.java new file mode 100644 index 0000000..156f9f5 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/gui/SmoothChunksScreen.java @@ -0,0 +1,108 @@ +package me.topchetoeu.smoothchunks.gui; + +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.sound.PositionedSoundInstance; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.ColorHelper.Argb; +import me.topchetoeu.smoothchunks.Manager; +import me.topchetoeu.smoothchunks.SmoothChunks; +import me.topchetoeu.smoothchunks.gui.Section.OrderType; +import net.minecraft.client.MinecraftClient; + +public class SmoothChunksScreen extends Screen { + public final Screen parent; + private final HorizontalSection mainSection = new HorizontalSection(); + + public static void playClick() { + MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0f)); + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + if (parent == null) renderBackground(matrices); + else parent.render(matrices, Integer.MIN_VALUE, Integer.MIN_VALUE, delta); + + fill(matrices, 0, 0, width, height, Argb.getArgb(191, 0, 0, 0)); + mainSection.render(matrices, mouseX, mouseY, delta); + // super.render(matrices, mouseX, mouseY, delta); + } + + @Override + protected void init() { + super.init(); + parent.init(client, width, height); + + addDrawableChild(mainSection); + mainSection.children.clear(); + mainSection.setTargetWidth(width - 15); + mainSection.order = OrderType.Justified; + mainSection.children.addSelectableChild(selectionsSection()); + mainSection.children.addSelectableChild(previewSection()); + // setZOffset(parent.getZOffset() + 1); + } + + @Override + public void close() { + MinecraftClient.getInstance().setScreen(parent); + } + @Override + public void mouseMoved(double mouseX, double mouseY) { + mainSection.mouseMoved(mouseX, mouseY); + } + + private Section previewSection() { + var res = new HorizontalSection(); + res.x = res.y = 5; + res.title = Text.of("Preview:"); + + res.children.addSelectableChild(new ChunkPreview(0, 0, 150, 150)); + + return res; + } + private Section selectionSection(Manager manager, String name) { + var res = new HorizontalSection(); + res.x = 5; + res.y = 10; + res.title = Text.of(name + ":"); + res.setTargetWidth(width / 2); + + var selectScreen = new SelectionScreen<>(this, manager, val -> manager.set(val.getName())); + var buttonSection = new HorizontalSection(); + buttonSection.setTargetWidth(width / 2); + buttonSection.order = OrderType.Justified; + buttonSection.children.addChild(new Label( + 5, 7, + new LiteralText(manager.get().getDisplayName()).formatted(Formatting.BOLD) + )); + buttonSection.children.addSelectableChild(new Button(5, 5, Text.of("Select ..."), () -> client.setScreen(selectScreen))); + res.children.addSelectableChild(buttonSection); + res.children.addChild(new Label( + 5, 3, + new LiteralText("Author: " + manager.get().getAuthorOrDefault()) + ).setMaxWidth(width / 2)); + res.children.addChild(new Label( + 5, 3, + new LiteralText(manager.get().getDescriptionOrDefault()).formatted(Formatting.ITALIC) + ).setMaxWidth(width / 2)); + + return res; + } + private Section selectionsSection() { + var res = new HorizontalSection(); + res.x = res.y = 5; + res.title = Text.of("Animation config:"); + res.children.addSelectableChild(selectionSection(SmoothChunks.getInstance().getAnimationManager(), "Animation")); + res.children.addSelectableChild(selectionSection(SmoothChunks.getInstance().getEaseManager(), "Ease")); + return res; + } + public SmoothChunksScreen(Screen parent) { + super(Text.of("Smooth Chunks Config")); + mainSection.x = mainSection.y = 5; + + this.parent = parent; + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/gui/VerticalSection.java b/src/main/java/me/topchetoeu/smoothchunks/gui/VerticalSection.java new file mode 100644 index 0000000..6c60c93 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/gui/VerticalSection.java @@ -0,0 +1,106 @@ +package me.topchetoeu.smoothchunks.gui; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import net.minecraft.client.gui.Element; + +public final class VerticalSection extends Section { + private class Line { + public Map heights = new Hashtable<>(); + public List elements = new ArrayList<>(); + public float width = 0, height = 0; + } + + private float targetHeight; + + public float getTargetHeight(float width) { + return this.targetHeight; + } + public void setTargetHeight(float width) { + this.targetHeight = width; + recalculate(); + } + + private List getLines() { + Line currLine = new Line(); + List lines = new ArrayList<>(); + + for (Element el : children.get()) { + var box = children.getBoundbox(el); + var x = box.getX() + box.getWidth(); + var y = box.getY() + box.getHeight(); + + if (currLine.height + y > targetHeight) { + lines.add(currLine); + currLine = new Line(); + } + + currLine.height += y; + currLine.elements.add(el); + currLine.heights.put(el, y); + + if (currLine.width < x) currLine.width = x; + } + lines.add(currLine); + + return lines; + } + + private void recalculateTop(Line line, float offsetX) { + float currY = 0; + for (Element el : line.elements) { + children.offsets.put(el, new Offset(offsetX, currY)); + currY += line.heights.get(el); + } + } + private void recalculateCenter(Line line, float offsetX) { + recalculateTop(line, offsetX); + + for (Element el : line.elements) { + children.offsets.get(el).y += (targetHeight - line.height) / 2; + } + } + private void recalculateBottom(Line line, float offsetX) { + recalculateTop(line, offsetX); + + for (Element el : line.elements) { + children.offsets.get(el).y += (targetHeight - line.height); + } + } + private void recalculateJustified(Line line, float offsetX) { + recalculateTop(line, offsetX); + int i = 0; + + if (line.elements.size() < 2) return; + + for (Element el : line.elements) { + children.offsets.get(el).y += (targetHeight - line.height) / (line.elements.size() - 1) * i; + i++; + } + } + + @Override + protected final void recalculate() { + width = 0; + height = title == null ? 0 : mc.textRenderer.fontHeight + targetHeight; + + for (Line line : getLines()) { + if (line.height > targetHeight) { + recalculateTop(line, width); + } + else { + switch (order) { + case Start: recalculateTop(line, width); break; + case Middle: recalculateCenter(line, width); break; + case End: recalculateBottom(line, width); break; + case Justified: recalculateJustified(line, width); break; + } + } + width += line.width; + if (height < line.height) height = line.height; + } + } +} diff --git a/src/main/java/me/topchetoeu/smoothchunks/mixin/BuiltChunkMixin.java b/src/main/java/me/topchetoeu/smoothchunks/mixin/BuiltChunkMixin.java new file mode 100644 index 0000000..21b8349 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/mixin/BuiltChunkMixin.java @@ -0,0 +1,22 @@ +package me.topchetoeu.smoothchunks.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import me.topchetoeu.smoothchunks.SmoothChunks; +import net.minecraft.client.render.chunk.ChunkBuilder; +import net.minecraft.util.math.BlockPos; + +// From flogic's mod +@Mixin(ChunkBuilder.BuiltChunk.class) +abstract class BuiltChunkMixin { + @Inject(method = "clear", at = @At(value = "TAIL"), cancellable = true) + public void clear(CallbackInfo ci) { + // ci.cancel(); + // return; + BlockPos origin = ((ChunkBuilder.BuiltChunk)(Object)this).getOrigin(); + SmoothChunks.getInstance().getProgressManager().unload(origin.getX(), 0, origin.getZ()); + } +} \ No newline at end of file diff --git a/src/main/java/me/topchetoeu/smoothchunks/mixin/WorldRendererMixin.java b/src/main/java/me/topchetoeu/smoothchunks/mixin/WorldRendererMixin.java new file mode 100644 index 0000000..ea50187 --- /dev/null +++ b/src/main/java/me/topchetoeu/smoothchunks/mixin/WorldRendererMixin.java @@ -0,0 +1,98 @@ +package me.topchetoeu.smoothchunks.mixin; + +import net.minecraft.client.gl.GlUniform; +import net.minecraft.client.render.BuiltChunkStorage; +import net.minecraft.client.render.Camera; +import net.minecraft.client.render.GameRenderer; +import net.minecraft.client.render.LightmapTextureManager; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.Shader; +import net.minecraft.client.render.VertexFormat; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.client.render.chunk.ChunkBuilder.BuiltChunk; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.math.Matrix4f; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import it.unimi.dsi.fastutil.objects.ObjectListIterator; +import me.topchetoeu.smoothchunks.SmoothChunks; +import me.topchetoeu.smoothchunks.animation.ProgressManager; + +@Mixin(WorldRenderer.class) +abstract class WorldRendererMixin { + private long lastTime = System.nanoTime(); + + @Accessor abstract BuiltChunkStorage getChunks(); + + private ProgressManager getProgressManager() { + return SmoothChunks.getInstance().getProgressManager(); + } + + @Inject(method = "render", at = @At(value = "HEAD")) + private void renderStart(MatrixStack matrices, float tickDelta, long limitTime, boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightmapTextureManager lightmapTextureManager, Matrix4f positionMatrix, CallbackInfo ci) { + long currTime = System.nanoTime(); + getProgressManager().tick((currTime - lastTime) / 1000000000f); + lastTime = currTime; + } + + @Inject(method = "renderLayer", at = @At(value = "HEAD")) + private void renderLayerAfter(RenderLayer renderLayer, MatrixStack matrices, double playerX, double playerY, double playerZ, Matrix4f positionMatrix, CallbackInfo ci) { + int chunkX = (int)(playerX / 16); + int chunkY = (int)(playerY / 16); + int chunkZ = (int)(playerZ / 16); + + if (playerX < 0) chunkX--; + if (playerY < 0) chunkY--; + if (playerZ < 0) chunkZ--; + + getProgressManager().unloadAllFar((int)((WorldRenderer)(Object)this).getViewDistance(), chunkX, chunkY, chunkZ); + } + @Inject(method = "renderLayer", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gl/VertexBuffer;drawVertices()V"), locals=LocalCapture.CAPTURE_FAILHARD) + private void renderChunkBefore(RenderLayer renderLayer, MatrixStack matrices, double playerX, double playerY, double playerZ, Matrix4f positionMatrix, CallbackInfo ci, + boolean _1, ObjectListIterator _2, VertexFormat _3, Shader shader, GlUniform _4, boolean _5, WorldRenderer.ChunkInfo _6, BuiltChunk chunk) { + + matrices.push(); + + int x = chunk.getOrigin().getX(); + int y = chunk.getOrigin().getY(); + int z = chunk.getOrigin().getZ(); + + shader.fogStart.set(Float.POSITIVE_INFINITY); + + if (getProgressManager().isChunkLoaded(x, 0, z)) { + float progress = getProgressManager().getChunkProgress(x, 0, z); + + if (progress < 0.999) { + progress = SmoothChunks.getInstance().getEaseManager().getValue().ease(progress); + + float centerX = (float)playerX - x; + float centerY = (float)playerY - y; + float centerZ = (float)playerZ - z; + + matrices.translate(-centerX, -centerY, -centerZ); + SmoothChunks.getInstance().getAnimationManager().getValue().animate(progress, matrices, x, y, z, (float)playerX, (float)playerY, (float)playerZ); + matrices.translate(centerX, centerY, centerZ); + } + } + else { + matrices.scale(0, 0, 0); + } + + shader.modelViewMat.set(matrices.peek().getPositionMatrix()); + matrices.pop(); + shader.bind(); + } + @Inject(method = "renderLayer", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gl/VertexBuffer;drawVertices()V"), locals=LocalCapture.CAPTURE_FAILHARD) + private void renderChunkAfter(RenderLayer renderLayer, MatrixStack matrices, double playerX, double playerY, double playerZ, Matrix4f positionMatrix, CallbackInfo ci, + boolean _1, ObjectListIterator _2, VertexFormat _3, Shader _4, GlUniform _5, boolean _6, WorldRenderer.ChunkInfo _7, BuiltChunk chunk) { + int x = chunk.getOrigin().getX(); + int z = chunk.getOrigin().getZ(); + getProgressManager().load(x, 0, z); + } +} diff --git a/src/main/resources/assets/smooth-chunks/icon.png b/src/main/resources/assets/smooth-chunks/icon.png new file mode 100644 index 0000000..fe68be4 Binary files /dev/null and b/src/main/resources/assets/smooth-chunks/icon.png differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..6cc623f --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "id": "smooth-chunks", + "version": "0.1.0", + "name": "Smooth Chunks", + "description": "Smooth chunk load animations.", + "authors": [ "TopchetoEU" ], + "license": "MIT", + "icon": "assets/smooth-chunks/icon.png", + "environment": "client", + "accessWidener": "smooth-chunks.accesswidener", + "entrypoints": { + "client": [ + "me.topchetoeu.smoothchunks.SmoothChunks" + ], + "modmenu": [ + "me.topchetoeu.smoothchunks.SmoothChunks" + ] + }, + "mixins": [ + "smooth-chunks.mixins.json" + ], + "depends": { + "fabricloader": ">=0.9.3+build.207", + "fabric": "*", + "minecraft": ">=1.18.2", + "modmenu": ">=3.2.1" + } +} diff --git a/src/main/resources/smooth-chunks.accesswidener b/src/main/resources/smooth-chunks.accesswidener new file mode 100644 index 0000000..0e107e4 --- /dev/null +++ b/src/main/resources/smooth-chunks.accesswidener @@ -0,0 +1,4 @@ +accessWidener v1 named + +accessible class net/minecraft/client/render/chunk/ChunkBuilder$BuiltChunk$RebuildTask +accessible class net/minecraft/client/render/WorldRenderer$ChunkInfo \ No newline at end of file diff --git a/src/main/resources/smooth-chunks.mixins.json b/src/main/resources/smooth-chunks.mixins.json new file mode 100644 index 0000000..d7e0296 --- /dev/null +++ b/src/main/resources/smooth-chunks.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "me.topchetoeu.smoothchunks.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [], + "client": [ + "WorldRendererMixin", + "BuiltChunkMixin" + ], + "injectors": { + "defaultRequire": 1 + } +}