Compare commits
213 Commits
untagged-9
...
1.14.4-For
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa91aed58e | ||
|
|
802862f151 | ||
|
|
2252fb3b42 | ||
|
|
4efa831296 | ||
|
|
a3d99c3076 | ||
|
|
c4ee768025 | ||
|
|
b4f18c1e1d | ||
|
|
2a06c18884 | ||
|
|
2ba99f40e7 | ||
|
|
46cbe64328 | ||
|
|
7b739c172f | ||
|
|
8319d721c7 | ||
|
|
1b0e93b050 | ||
|
|
1ea2b6b946 | ||
|
|
558c9a2c34 | ||
|
|
f102bf4bda | ||
|
|
f5cbf48dfa | ||
|
|
d9cc03511a | ||
|
|
d0265483d2 | ||
|
|
20da2a27bd | ||
|
|
02509fa44d | ||
|
|
6801304bd1 | ||
|
|
d96ac1c94c | ||
|
|
ac015b12df | ||
|
|
71f0be0c93 | ||
|
|
bcc1b04e6b | ||
|
|
cbee4916ed | ||
|
|
418d955f54 | ||
|
|
3121542c07 | ||
|
|
ab08457ae7 | ||
|
|
5b1b35a891 | ||
|
|
787b3993b5 | ||
|
|
c593ff9bcb | ||
|
|
afa619f838 | ||
|
|
e704a0af94 | ||
|
|
0997b83367 | ||
|
|
fe3030ef77 | ||
|
|
7aa510189a | ||
|
|
36a3a38ff1 | ||
|
|
7f4ee4b0a3 | ||
|
|
a4c6d1eecd | ||
|
|
73223c15c3 | ||
|
|
b55d90802f | ||
|
|
c5261216b2 | ||
|
|
50399052b0 | ||
|
|
e29d224df4 | ||
|
|
3b7c44b062 | ||
|
|
32bf60492d | ||
|
|
9685971fd4 | ||
|
|
dacc63b11a | ||
|
|
e20a3fbc54 | ||
|
|
39df1951df | ||
|
|
4761eb266f | ||
|
|
25c302ecb6 | ||
|
|
dec1f6ff03 | ||
|
|
f145ff221e | ||
|
|
c865c8e5ad | ||
|
|
47781cad91 | ||
|
|
e329ce0270 | ||
|
|
7cae04d7b4 | ||
|
|
0bbf206569 | ||
|
|
55095e7252 | ||
|
|
8eebb98c9d | ||
|
|
2a8a9c2703 | ||
|
|
7da89e24f1 | ||
|
|
fb078ab365 | ||
|
|
b5d87bb148 | ||
|
|
6c98940d3e | ||
|
|
813719c7f2 | ||
|
|
800fb4db9f | ||
|
|
38b35c910b | ||
|
|
61076789db | ||
|
|
fde6c47ed3 | ||
|
|
a9fba1a18e | ||
|
|
81ef954524 | ||
|
|
674d22fdbb | ||
|
|
381b154413 | ||
|
|
568549e260 | ||
|
|
6d62cb9ac0 | ||
|
|
5bea5cde99 | ||
|
|
28cead464e | ||
|
|
8ffca417fb | ||
|
|
8f9a35f40e | ||
|
|
ef90adf577 | ||
|
|
370e2bb38c | ||
|
|
fefd5e5633 | ||
|
|
bae81e8085 | ||
|
|
479e4cadfa | ||
|
|
821d618395 | ||
|
|
eb6058c4ee | ||
|
|
625a3bd543 | ||
|
|
37d091daed | ||
|
|
d852faad96 | ||
|
|
da8d7ec237 | ||
|
|
31f64749b1 | ||
|
|
66558932a9 | ||
|
|
ceb3e5b116 | ||
|
|
1a1aa81c0f | ||
|
|
522fc1de33 | ||
|
|
90c13a3a8e | ||
|
|
66b4df2850 | ||
|
|
7df142cf50 | ||
|
|
2b7582c5af | ||
|
|
56e3dc5d24 | ||
|
|
f7044e5225 | ||
|
|
59e4d0c602 | ||
|
|
931dca6f3f | ||
|
|
c356c3ef57 | ||
|
|
468d0f34b6 | ||
|
|
6f42152cde | ||
|
|
44bfc93e1b | ||
|
|
70591a484e | ||
|
|
fdc14595db | ||
|
|
aa8226b46b | ||
|
|
ad78529d2a | ||
|
|
8c922fd2e8 | ||
|
|
9e9666f69f | ||
|
|
2c0e95ba5b | ||
|
|
85a4707494 | ||
|
|
f0a447bbbb | ||
|
|
4a4d39b523 | ||
|
|
e00ccd5919 | ||
|
|
f032814d99 | ||
|
|
62294bb2bb | ||
|
|
dec1ffd71c | ||
|
|
488078b50f | ||
|
|
1bd353577f | ||
|
|
913496473d | ||
|
|
a8e6c6c470 | ||
|
|
8bdb5ca8fd | ||
|
|
5efc974133 | ||
|
|
7f3617ef59 | ||
|
|
400b965e02 | ||
|
|
f96409f9a1 | ||
|
|
2ca330fd29 | ||
|
|
acf477d709 | ||
|
|
97d5d6320c | ||
|
|
6831050c77 | ||
|
|
70ec7e5289 | ||
|
|
f66aabea67 | ||
|
|
0635f1e19e | ||
|
|
44a8abeb4b | ||
|
|
b5af0fe1c5 | ||
|
|
69db3d6608 | ||
|
|
6903bd2de2 | ||
|
|
9a544b1b53 | ||
|
|
bbd4df418c | ||
|
|
626bc69dad | ||
|
|
66ed1c098f | ||
|
|
f37cb273f1 | ||
|
|
a6cc354965 | ||
|
|
8fe4346922 | ||
|
|
2f45abcd7c | ||
|
|
57cae957f8 | ||
|
|
befb64b8fc | ||
|
|
a0aad5d608 | ||
|
|
c0685d829b | ||
|
|
4209d1eea3 | ||
|
|
ac2001694b | ||
|
|
50c4882855 | ||
|
|
07dc8888ef | ||
|
|
efebf29c64 | ||
|
|
983703133a | ||
|
|
ee19120651 | ||
|
|
cbaebfa26a | ||
|
|
1ff5d45840 | ||
|
|
7e667d483a | ||
|
|
6d5c03ba6a | ||
|
|
f47aedf84d | ||
|
|
6ee27c2a99 | ||
|
|
abf037d8a9 | ||
|
|
c0be72bb37 | ||
|
|
42c14790af | ||
|
|
087e8d5685 | ||
|
|
aaca43fe2c | ||
|
|
8e251dc038 | ||
|
|
1a6ffb251b | ||
|
|
efb49125b8 | ||
|
|
6032a120d8 | ||
|
|
f68f0e5edd | ||
|
|
7393fed5fd | ||
|
|
41b080646a | ||
|
|
111f1b3907 | ||
|
|
662b4f50f0 | ||
|
|
d9a042b356 | ||
|
|
ef574a13ac | ||
|
|
b246c7acd0 | ||
|
|
35ce80c602 | ||
|
|
4fdabbaf69 | ||
|
|
0a6c433530 | ||
|
|
3e6f98885f | ||
|
|
4720667f53 | ||
|
|
5616a68f5a | ||
|
|
d1480ed3be | ||
|
|
023e286fd4 | ||
|
|
6c8f40f4e2 | ||
|
|
6306a5b03b | ||
|
|
b0193bb108 | ||
|
|
eafd36b4b1 | ||
|
|
dac7033e18 | ||
|
|
36c6b775db | ||
|
|
0f985e996b | ||
|
|
e926f018e7 | ||
|
|
3716804ffb | ||
|
|
2d70de00e7 | ||
|
|
9b717b549b | ||
|
|
81abad82e9 | ||
|
|
601679f070 | ||
|
|
f0073b0f2a | ||
|
|
5bdbb366eb | ||
|
|
2e09f1f10a | ||
|
|
8460103030 | ||
|
|
f44043bb0b |
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.idea/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
run/
|
||||
.gradle/
|
||||
build/
|
||||
classes/
|
||||
temp/
|
||||
59
build.gradle.kts
Normal file
59
build.gradle.kts
Normal file
@@ -0,0 +1,59 @@
|
||||
plugins {
|
||||
kotlin("jvm").version("1.3.61")
|
||||
id("net.minecraftforge.gradle").version("3.0.157")
|
||||
id("org.spongepowered.mixin").version("0.7-SNAPSHOT")
|
||||
}
|
||||
apply(plugin = "org.spongepowered.mixin")
|
||||
|
||||
repositories {
|
||||
maven("http://files.minecraftforge.net/maven")
|
||||
maven("https://repo.spongepowered.org/maven")
|
||||
maven("https://minecraft.curseforge.com/api/maven")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
"minecraft"("net.minecraftforge:forge:${properties["mcVersion"]}-${properties["forgeVersion"]}")
|
||||
|
||||
"implementation"("kottle:Kottle:${properties["kottleVersion"]}")
|
||||
"implementation"("org.spongepowered:mixin:0.8-SNAPSHOT")
|
||||
}
|
||||
|
||||
configurations["annotationProcessor"].extendsFrom(configurations["implementation"])
|
||||
sourceSets {
|
||||
get("main").ext["refMap"] = "betterfoliage.refmap.json"
|
||||
}
|
||||
|
||||
minecraft {
|
||||
mappings(properties["mappingsChannel"] as String, properties["mappingsVersion"] as String)
|
||||
accessTransformer(file("src/main/resources/META-INF/accesstransformer.cfg"))
|
||||
|
||||
runs.create("client") {
|
||||
workingDirectory(file("run"))
|
||||
properties["forge.logging.markers"] = "CORE"
|
||||
properties["forge.logging.console.level"] = "debug"
|
||||
mods.create("betterfoliage") {
|
||||
source(sourceSets["main"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlin {
|
||||
target.compilations.configureEach {
|
||||
kotlinOptions.freeCompilerArgs += listOf("-Xno-param-assertions", "-Xno-call-assertions")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.getByName<Jar>("jar") {
|
||||
archiveName = "BetterFoliage-${project.version}-Forge-${properties["mcVersion"]}.jar"
|
||||
manifest {
|
||||
from(file("src/main/resources/META-INF/MANIFEST.MF"))
|
||||
attributes["Implementation-Version"] = project.version
|
||||
}
|
||||
exclude("net")
|
||||
filesMatching("META-INF/mods.toml") { expand(project.properties) }
|
||||
}
|
||||
14
gradle.properties
Normal file
14
gradle.properties
Normal file
@@ -0,0 +1,14 @@
|
||||
org.gradle.jvmargs=-Xmx2G
|
||||
org.gradle.daemon=false
|
||||
|
||||
group = com.github.octarine-noise
|
||||
jarName = BetterFoliage-Forge
|
||||
|
||||
version = 2.5.0
|
||||
|
||||
mcVersion = 1.14.4
|
||||
forgeVersion = 28.1.109
|
||||
mappingsChannel = snapshot
|
||||
mappingsVersion = 20190719-1.14.3
|
||||
|
||||
kottleVersion = 1.4.0
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
172
gradlew
vendored
Normal file
172
gradlew
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## 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=""
|
||||
|
||||
# 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, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((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"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
84
gradlew.bat
vendored
Normal file
84
gradlew.bat
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
13
settings.gradle.kts
Normal file
13
settings.gradle.kts
Normal file
@@ -0,0 +1,13 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven("http://files.minecraftforge.net/maven")
|
||||
maven("https://repo.spongepowered.org/maven")
|
||||
gradlePluginPortal()
|
||||
}
|
||||
resolutionStrategy {
|
||||
eachPlugin {
|
||||
if (requested.id.let { it.namespace == "net.minecraftforge" && it.name == "gradle"} ) useModule("net.minecraftforge.gradle:ForgeGradle:${requested.version}")
|
||||
if (requested.id.let { it.namespace == "org.spongepowered" && it.name == "mixin"} ) useModule("org.spongepowered:mixingradle:${requested.version}")
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/main/java/mods/betterfoliage/MixinConnector.java
Normal file
19
src/main/java/mods/betterfoliage/MixinConnector.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package mods.betterfoliage;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixins;
|
||||
import org.spongepowered.asm.mixin.connect.IMixinConnector;
|
||||
|
||||
public class MixinConnector implements IMixinConnector {
|
||||
@Override
|
||||
public void connect() {
|
||||
Mixins.addConfiguration("betterfoliage.common.mixins.json");
|
||||
|
||||
try {
|
||||
Class.forName("optifine.OptiFineTransformationService");
|
||||
Mixins.addConfiguration("betterfoliage.optifine.mixins.json");
|
||||
} catch (ClassNotFoundException e) {
|
||||
Mixins.addConfiguration("betterfoliage.vanilla.mixins.json");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
29
src/main/java/mods/betterfoliage/mixin/MixinBlock.java
Normal file
29
src/main/java/mods/betterfoliage/mixin/MixinBlock.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package mods.betterfoliage.mixin;
|
||||
|
||||
import mods.betterfoliage.client.Hooks;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.util.Direction;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.shapes.VoxelShape;
|
||||
import net.minecraft.world.IBlockReader;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
/**
|
||||
* Mixin overriding the {@link VoxelShape} used for the neighbor block in {@link Block}.shouldSideBeRendered().
|
||||
*
|
||||
* This way log blocks can be made to not block rendering, without altering any {@link Block} or
|
||||
* {@link BlockState} properties with potential gameplay ramifications.
|
||||
*/
|
||||
@Mixin(Block.class)
|
||||
public class MixinBlock {
|
||||
private static final String shouldSideBeRendered = "Lnet/minecraft/block/Block;shouldSideBeRendered(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;)Z";
|
||||
private static final String getVoxelShape = "Lnet/minecraft/block/BlockState;func_215702_a(Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;)Lnet/minecraft/util/math/shapes/VoxelShape;";
|
||||
|
||||
@Redirect(method = shouldSideBeRendered, at = @At(value = "INVOKE", target = getVoxelShape, ordinal = 1))
|
||||
private static VoxelShape getVoxelShapeOverride(BlockState state, IBlockReader reader, BlockPos pos, Direction dir) {
|
||||
return Hooks.getVoxelShapeOverride(state, reader, pos, dir);
|
||||
}
|
||||
}
|
||||
27
src/main/java/mods/betterfoliage/mixin/MixinBlockState.java
Normal file
27
src/main/java/mods/betterfoliage/mixin/MixinBlockState.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package mods.betterfoliage.mixin;
|
||||
|
||||
import mods.betterfoliage.client.Hooks;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.IBlockReader;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
/**
|
||||
* Mixin to override the result of {@link BlockState}.getAmbientOcclusionLightValue().
|
||||
*
|
||||
* Needed to avoid excessive darkening of Round Logs at the corners, now that they are not full blocks.
|
||||
*/
|
||||
@Mixin(BlockState.class)
|
||||
@SuppressWarnings({"UnnecessaryQualifiedMemberReference", "deprecation"})
|
||||
public class MixinBlockState {
|
||||
private static final String callFrom = "Lnet/minecraft/block/BlockState;func_215703_d(Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;)F";
|
||||
private static final String callTo = "Lnet/minecraft/block/Block;func_220080_a(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;)F";
|
||||
|
||||
@Redirect(method = callFrom, at = @At(value = "INVOKE", target = callTo))
|
||||
float getAmbientOcclusionValue(Block block, BlockState state, IBlockReader reader, BlockPos pos) {
|
||||
return Hooks.getAmbientOcclusionLightValueOverride(block.func_220080_a(state, reader, pos), state);
|
||||
}
|
||||
}
|
||||
28
src/main/java/mods/betterfoliage/mixin/MixinChunkRender.java
Normal file
28
src/main/java/mods/betterfoliage/mixin/MixinChunkRender.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package mods.betterfoliage.mixin;
|
||||
|
||||
import mods.betterfoliage.client.Hooks;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.client.renderer.BlockRendererDispatcher;
|
||||
import net.minecraft.client.renderer.BufferBuilder;
|
||||
import net.minecraft.client.renderer.chunk.ChunkRender;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.IEnviromentBlockReader;
|
||||
import net.minecraftforge.client.MinecraftForgeClient;
|
||||
import net.minecraftforge.client.model.data.IModelData;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
@Mixin(ChunkRender.class)
|
||||
public class MixinChunkRender {
|
||||
|
||||
private static final String rebuildChunk = "rebuildChunk(FFFLnet/minecraft/client/renderer/chunk/ChunkRenderTask;)V";
|
||||
private static final String renderBlock = "Lnet/minecraft/client/renderer/BlockRendererDispatcher;renderBlock(Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/world/IEnviromentBlockReader;Lnet/minecraft/client/renderer/BufferBuilder;Ljava/util/Random;Lnet/minecraftforge/client/model/data/IModelData;)Z";
|
||||
|
||||
@Redirect(method = rebuildChunk, at = @At(value = "INVOKE", target = renderBlock))
|
||||
public boolean renderBlock(BlockRendererDispatcher dispatcher, BlockState state, BlockPos pos, IEnviromentBlockReader reader, BufferBuilder buffer, Random random, IModelData modelData) {
|
||||
return Hooks.renderWorldBlock(dispatcher, state, pos, reader, buffer, random, modelData, MinecraftForgeClient.getRenderLayer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package mods.betterfoliage.mixin;
|
||||
|
||||
import mods.betterfoliage.client.Hooks;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.client.renderer.chunk.ChunkRender;
|
||||
import net.minecraft.util.BlockRenderLayer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
@Mixin(ChunkRender.class)
|
||||
public class MixinChunkRenderVanilla {
|
||||
|
||||
private static final String rebuildChunk = "rebuildChunk(FFFLnet/minecraft/client/renderer/chunk/ChunkRenderTask;)V";
|
||||
private static final String canRenderInLayer = "Lnet/minecraft/block/BlockState;canRenderInLayer(Lnet/minecraft/util/BlockRenderLayer;)Z";
|
||||
|
||||
@Redirect(method = rebuildChunk, at = @At(value = "INVOKE", target = canRenderInLayer))
|
||||
boolean canRenderInLayer(BlockState state, BlockRenderLayer layer) {
|
||||
return Hooks.canRenderInLayerOverride(state, layer);
|
||||
}
|
||||
}
|
||||
44
src/main/java/mods/betterfoliage/mixin/MixinClientWorld.java
Normal file
44
src/main/java/mods/betterfoliage/mixin/MixinClientWorld.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package mods.betterfoliage.mixin;
|
||||
|
||||
import mods.betterfoliage.client.Hooks;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.client.renderer.WorldRenderer;
|
||||
import net.minecraft.client.world.ClientWorld;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.IBlockReader;
|
||||
import net.minecraft.world.World;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
@Mixin(ClientWorld.class)
|
||||
public class MixinClientWorld {
|
||||
|
||||
private static final String worldAnimateTick = "Lnet/minecraft/client/world/ClientWorld;animateTick(IIIILjava/util/Random;ZLnet/minecraft/util/math/BlockPos$MutableBlockPos;)V";
|
||||
private static final String blockAnimateTick = "Lnet/minecraft/block/Block;animateTick(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Ljava/util/Random;)V";
|
||||
|
||||
private static final String worldNotify = "Lnet/minecraft/client/world/ClientWorld;notifyBlockUpdate(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Lnet/minecraft/block/BlockState;I)V";
|
||||
private static final String rendererNotify = "Lnet/minecraft/client/renderer/WorldRenderer;notifyBlockUpdate(Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Lnet/minecraft/block/BlockState;I)V";
|
||||
|
||||
/**
|
||||
* Inject a callback to call for every random display tick. Used for adding custom particle effects to blocks.
|
||||
*/
|
||||
@Redirect(method = worldAnimateTick, at = @At(value = "INVOKE", target = blockAnimateTick))
|
||||
void onAnimateTick(Block block, BlockState state, World world, BlockPos pos, Random random) {
|
||||
Hooks.onRandomDisplayTick(block, state, world, pos, random);
|
||||
block.animateTick(state, world, pos, random);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject callback to get notified of client-side blockstate changes.
|
||||
* Used to invalidate caches in the {@link mods.betterfoliage.client.chunk.ChunkOverlayManager}
|
||||
*/
|
||||
@Redirect(method = worldNotify, at = @At(value = "INVOKE", target = rendererNotify))
|
||||
void onClientBlockChanged(WorldRenderer renderer, IBlockReader world, BlockPos pos, BlockState oldState, BlockState newState, int flags) {
|
||||
Hooks.onClientBlockChanged((ClientWorld) world, pos, oldState, newState, flags);
|
||||
renderer.notifyBlockUpdate(world, pos, oldState, newState, flags);
|
||||
}
|
||||
}
|
||||
29
src/main/java/mods/betterfoliage/mixin/MixinModelBakery.java
Normal file
29
src/main/java/mods/betterfoliage/mixin/MixinModelBakery.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package mods.betterfoliage.mixin;
|
||||
|
||||
import mods.betterfoliage.BetterFoliage;
|
||||
import net.minecraft.client.renderer.model.ModelBakery;
|
||||
import net.minecraft.client.renderer.texture.AtlasTexture;
|
||||
import net.minecraft.profiler.IProfiler;
|
||||
import net.minecraft.resources.IResourceManager;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
@Mixin(ModelBakery.class)
|
||||
abstract public class MixinModelBakery {
|
||||
|
||||
private static final String processLoading = "processLoading(Lnet/minecraft/profiler/IProfiler;)V";
|
||||
private static final String stitch = "Lnet/minecraft/client/renderer/texture/AtlasTexture;stitch(Lnet/minecraft/resources/IResourceManager;Ljava/lang/Iterable;Lnet/minecraft/profiler/IProfiler;)Lnet/minecraft/client/renderer/texture/AtlasTexture$SheetData;";
|
||||
|
||||
@Redirect(method = processLoading, at = @At(value = "INVOKE", target = stitch))
|
||||
AtlasTexture.SheetData onStitchModelTextures(AtlasTexture atlas, IResourceManager manager, Iterable<ResourceLocation> idList, IProfiler profiler) {
|
||||
return BetterFoliage.INSTANCE.getBlockSprites().finish(
|
||||
atlas.stitch(
|
||||
manager,
|
||||
BetterFoliage.INSTANCE.getBlockSprites().prepare(this, manager, idList, profiler),
|
||||
profiler
|
||||
), profiler
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package mods.betterfoliage.mixin;
|
||||
|
||||
import mods.betterfoliage.client.Hooks;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.util.Direction;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.shapes.VoxelShape;
|
||||
import net.minecraft.world.IBlockReader;
|
||||
import net.optifine.util.BlockUtils;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
@Mixin(BlockUtils.class)
|
||||
public class MixinOptifineBlockUtils {
|
||||
private static final String shouldSideBeRenderedCached = "shouldSideBeRenderedCached(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;Lnet/optifine/render/RenderEnv;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;)Z";
|
||||
private static final String getVoxelShape = "Lnet/minecraft/block/BlockState;func_215702_a(Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;)Lnet/minecraft/util/math/shapes/VoxelShape;";
|
||||
|
||||
@SuppressWarnings("UnresolvedMixinReference")
|
||||
@Redirect(method = shouldSideBeRenderedCached, at = @At(value = "INVOKE", target = getVoxelShape, ordinal = 1))
|
||||
private static VoxelShape getVoxelShapeOverride(BlockState state, IBlockReader reader, BlockPos pos, Direction dir) {
|
||||
return Hooks.getVoxelShapeOverride(state, reader, pos, dir);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package mods.betterfoliage.mixin;
|
||||
|
||||
import mods.betterfoliage.client.Hooks;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.client.renderer.chunk.ChunkRender;
|
||||
import net.minecraft.util.BlockRenderLayer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Coerce;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.Slice;
|
||||
|
||||
@Mixin(ChunkRender.class)
|
||||
public class MixinOptifineChunkRender {
|
||||
|
||||
private static final String rebuildChunk = "rebuildChunk(FFFLnet/minecraft/client/renderer/chunk/ChunkRenderTask;)V";
|
||||
private static final String invokeReflector = "Lnet/optifine/reflect/Reflector;callBoolean(Ljava/lang/Object;Lnet/optifine/reflect/ReflectorMethod;[Ljava/lang/Object;)Z";
|
||||
private static final String forgeBlockCanRender = "Lnet/minecraft/client/renderer/chunk/ChunkRender;FORGE_BLOCK_CAN_RENDER_IN_LAYER:Z";
|
||||
|
||||
@Redirect(
|
||||
method = rebuildChunk,
|
||||
at = @At(value = "INVOKE", target = invokeReflector),
|
||||
slice = @Slice(
|
||||
from = @At(value = "FIELD", target = forgeBlockCanRender)
|
||||
)
|
||||
)
|
||||
@SuppressWarnings("UnresolvedMixinReference")
|
||||
boolean canRenderInLayer(Object state, @Coerce Object reflector, Object[] layer) {
|
||||
return Hooks.canRenderInLayerOverride((BlockState) state, (BlockRenderLayer) layer[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package mods.betterfoliage.mixin;
|
||||
|
||||
import mods.betterfoliage.BetterFoliage;
|
||||
import mods.octarinecore.client.resource.AsnycSpriteProviderManager;
|
||||
import net.minecraft.client.particle.ParticleManager;
|
||||
import net.minecraft.client.renderer.texture.AtlasTexture;
|
||||
import net.minecraft.profiler.IProfiler;
|
||||
import net.minecraft.resources.IResourceManager;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
@Mixin(ParticleManager.class)
|
||||
public class MixinParticleManager {
|
||||
|
||||
private static final String reload = "reload(Lnet/minecraft/resources/IFutureReloadListener$IStage;Lnet/minecraft/resources/IResourceManager;Lnet/minecraft/profiler/IProfiler;Lnet/minecraft/profiler/IProfiler;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;";
|
||||
private static final String stitch = "Lnet/minecraft/client/renderer/texture/AtlasTexture;stitch(Lnet/minecraft/resources/IResourceManager;Ljava/lang/Iterable;Lnet/minecraft/profiler/IProfiler;)Lnet/minecraft/client/renderer/texture/AtlasTexture$SheetData;";
|
||||
|
||||
// ewww :S
|
||||
@SuppressWarnings("UnresolvedMixinReference")
|
||||
@Redirect(method = "*", at = @At(value = "INVOKE", target = stitch))
|
||||
AtlasTexture.SheetData onStitchModelTextures(AtlasTexture atlas, IResourceManager manager, Iterable<ResourceLocation> idList, IProfiler profiler) {
|
||||
return BetterFoliage.INSTANCE.getParticleSprites().finish(
|
||||
atlas.stitch(
|
||||
manager,
|
||||
BetterFoliage.INSTANCE.getParticleSprites().prepare(this, manager, idList, profiler),
|
||||
profiler
|
||||
), profiler
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package mods.betterfoliage.mixin;
|
||||
|
||||
import mods.betterfoliage.client.integration.ShadersModIntegration;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.client.renderer.BlockModelRenderer;
|
||||
import net.minecraft.client.renderer.BufferBuilder;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.IEnviromentBlockReader;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyArg;
|
||||
|
||||
@Mixin(BlockModelRenderer.class)
|
||||
public class MixinShadersBlockModelRenderer {
|
||||
|
||||
private static final String renderModel = "renderModel(Lnet/minecraft/world/IEnviromentBlockReader;Lnet/minecraft/client/renderer/model/IBakedModel;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/client/renderer/BufferBuilder;ZLjava/util/Random;JLnet/minecraftforge/client/model/data/IModelData;)Z";
|
||||
private static final String pushEntity = "Lnet/optifine/shaders/SVertexBuilder;pushEntity(Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/world/IEnviromentBlockReader;Lnet/minecraft/client/renderer/BufferBuilder;)V";
|
||||
|
||||
@SuppressWarnings("UnresolvedMixinReference")
|
||||
@ModifyArg(method = renderModel, at = @At(value = "INVOKE", target = pushEntity), remap = false)
|
||||
BlockState overrideBlockState(BlockState state, BlockPos pos, IEnviromentBlockReader world, BufferBuilder buffer) {
|
||||
return ShadersModIntegration.getBlockStateOverride(state, world, pos);
|
||||
}
|
||||
}
|
||||
5
src/main/java/net/optifine/util/BlockUtils.java
Normal file
5
src/main/java/net/optifine/util/BlockUtils.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package net.optifine.util;
|
||||
|
||||
public class BlockUtils {
|
||||
// whyyyy?
|
||||
}
|
||||
76
src/main/kotlin/mods/betterfoliage/BetterFoliage.kt
Normal file
76
src/main/kotlin/mods/betterfoliage/BetterFoliage.kt
Normal file
@@ -0,0 +1,76 @@
|
||||
package mods.betterfoliage
|
||||
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.client.render.RisingSoulTextures
|
||||
import mods.octarinecore.client.gui.textComponent
|
||||
import mods.octarinecore.client.resource.AsnycSpriteProviderManager
|
||||
import mods.octarinecore.client.resource.AsyncSpriteProvider
|
||||
import mods.octarinecore.client.resource.Atlas
|
||||
import mods.octarinecore.client.resource.GeneratedBlockTexturePack
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.particle.ParticleManager
|
||||
import net.minecraft.client.renderer.model.ModelBakery
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.util.text.TextFormatting
|
||||
import net.minecraft.util.text.TranslationTextComponent
|
||||
import net.minecraftforge.registries.ForgeRegistries
|
||||
import org.apache.logging.log4j.Level
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.simple.SimpleLogger
|
||||
import org.apache.logging.log4j.util.PropertiesUtil
|
||||
import java.io.File
|
||||
import java.io.PrintStream
|
||||
import java.util.*
|
||||
|
||||
object BetterFoliage {
|
||||
var log = LogManager.getLogger("BetterFoliage")
|
||||
var logDetail = SimpleLogger(
|
||||
"BetterFoliage",
|
||||
Level.DEBUG,
|
||||
false, false, true, false,
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
null,
|
||||
PropertiesUtil(Properties()),
|
||||
PrintStream(File("logs/betterfoliage.log").apply {
|
||||
parentFile.mkdirs()
|
||||
if (!exists()) createNewFile()
|
||||
})
|
||||
)
|
||||
|
||||
val blockSprites = AsnycSpriteProviderManager<ModelBakery>("bf-blocks-extra")
|
||||
val particleSprites = AsnycSpriteProviderManager<ParticleManager>("bf-particles-extra")
|
||||
val asyncPack = GeneratedBlockTexturePack("bf_gen", "Better Foliage generated assets", logDetail)
|
||||
|
||||
fun getSpriteManager(atlas: Atlas) = when(atlas) {
|
||||
Atlas.BLOCKS -> blockSprites
|
||||
Atlas.PARTICLES -> particleSprites
|
||||
} as AsnycSpriteProviderManager<Any>
|
||||
|
||||
init {
|
||||
blockSprites.providers.add(asyncPack)
|
||||
}
|
||||
|
||||
fun log(level: Level, msg: String) {
|
||||
log.log(level, "[BetterFoliage] $msg")
|
||||
logDetail.log(level, msg)
|
||||
}
|
||||
|
||||
fun logDetail(msg: String) {
|
||||
logDetail.log(Level.DEBUG, msg)
|
||||
}
|
||||
|
||||
fun logRenderError(state: BlockState, location: BlockPos) {
|
||||
if (state in Client.suppressRenderErrors) return
|
||||
Client.suppressRenderErrors.add(state)
|
||||
|
||||
val blockName = ForgeRegistries.BLOCKS.getKey(state.block).toString()
|
||||
val blockLoc = "${location.x},${location.y},${location.z}"
|
||||
Minecraft.getInstance().ingameGUI.chatGUI.printChatMessage(TranslationTextComponent(
|
||||
"betterfoliage.rendererror",
|
||||
textComponent(blockName, TextFormatting.GOLD),
|
||||
textComponent(blockLoc, TextFormatting.GOLD)
|
||||
))
|
||||
logDetail("Error rendering block $state at $blockLoc")
|
||||
}
|
||||
}
|
||||
35
src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt
Normal file
35
src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt
Normal file
@@ -0,0 +1,35 @@
|
||||
package mods.betterfoliage
|
||||
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.client.config.BlockConfig
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.octarinecore.client.resource.AsnycSpriteProviderManager
|
||||
import mods.octarinecore.client.resource.GeneratedBlockTexturePack
|
||||
import net.alexwells.kottle.FMLKotlinModLoadingContext
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.particle.ParticleManager
|
||||
import net.minecraft.client.renderer.model.ModelBakery
|
||||
import net.minecraftforge.fml.ModLoadingContext
|
||||
import net.minecraftforge.fml.common.Mod
|
||||
import net.minecraftforge.fml.config.ModConfig
|
||||
import org.apache.logging.log4j.Level.DEBUG
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.simple.SimpleLogger
|
||||
import org.apache.logging.log4j.util.PropertiesUtil
|
||||
import java.io.File
|
||||
import java.io.PrintStream
|
||||
import java.util.*
|
||||
|
||||
@Mod(BetterFoliageMod.MOD_ID)
|
||||
object BetterFoliageMod {
|
||||
const val MOD_ID = "betterfoliage"
|
||||
|
||||
val bus = FMLKotlinModLoadingContext.get().modEventBus
|
||||
|
||||
init {
|
||||
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, Config.build())
|
||||
Minecraft.getInstance().resourcePackList.addPackFinder(BetterFoliage.asyncPack.finder)
|
||||
bus.register(BlockConfig)
|
||||
Client.init()
|
||||
}
|
||||
}
|
||||
78
src/main/kotlin/mods/betterfoliage/client/Client.kt
Normal file
78
src/main/kotlin/mods/betterfoliage/client/Client.kt
Normal file
@@ -0,0 +1,78 @@
|
||||
package mods.betterfoliage.client
|
||||
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.chunk.ChunkOverlayManager
|
||||
import mods.betterfoliage.client.config.BlockConfig
|
||||
import mods.betterfoliage.client.integration.*
|
||||
import mods.betterfoliage.client.render.*
|
||||
import mods.betterfoliage.client.texture.AsyncGrassDiscovery
|
||||
import mods.betterfoliage.client.texture.AsyncLeafDiscovery
|
||||
import mods.betterfoliage.client.texture.LeafParticleRegistry
|
||||
import mods.octarinecore.client.gui.textComponent
|
||||
import mods.octarinecore.client.render.RenderDecorator
|
||||
import mods.octarinecore.client.resource.IConfigChangeListener
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.util.text.TextFormatting
|
||||
import net.minecraft.util.text.TranslationTextComponent
|
||||
import net.minecraftforge.registries.ForgeRegistries
|
||||
import org.apache.logging.log4j.Level
|
||||
|
||||
/**
|
||||
* Object responsible for initializing (and holding a reference to) all the infrastructure of the mod
|
||||
* except for the call hooks.
|
||||
*/
|
||||
object Client {
|
||||
var renderers= emptyList<RenderDecorator>()
|
||||
var configListeners = emptyList<IConfigChangeListener>()
|
||||
|
||||
val suppressRenderErrors = mutableSetOf<BlockState>()
|
||||
|
||||
fun init() {
|
||||
// init renderers
|
||||
renderers = listOf(
|
||||
RenderGrass(),
|
||||
RenderMycelium(),
|
||||
RenderLeaves(),
|
||||
RenderCactus(),
|
||||
RenderLilypad(),
|
||||
RenderReeds(),
|
||||
RenderAlgae(),
|
||||
RenderCoral(),
|
||||
RenderLog(),
|
||||
RenderNetherrack(),
|
||||
RenderConnectedGrass(),
|
||||
RenderConnectedGrassLog()
|
||||
)
|
||||
|
||||
// init other singletons
|
||||
val singletons = listOf(
|
||||
BlockConfig,
|
||||
ChunkOverlayManager,
|
||||
LeafWindTracker,
|
||||
RisingSoulTextures
|
||||
)
|
||||
|
||||
// init mod integrations
|
||||
val integrations = listOf(
|
||||
ShadersModIntegration,
|
||||
OptifineCustomColors
|
||||
// ForestryIntegration,
|
||||
// IC2RubberIntegration,
|
||||
// TechRebornRubberIntegration
|
||||
)
|
||||
|
||||
LeafParticleRegistry.init()
|
||||
|
||||
// add basic block support instances as last
|
||||
AsyncLeafDiscovery.init()
|
||||
AsyncGrassDiscovery.init()
|
||||
AsyncLogDiscovery.init()
|
||||
AsyncCactusDiscovery.init()
|
||||
|
||||
configListeners = listOf(renderers, singletons, integrations).flatten().filterIsInstance<IConfigChangeListener>()
|
||||
configListeners.forEach { it.onConfigChange() }
|
||||
}
|
||||
}
|
||||
|
||||
112
src/main/kotlin/mods/betterfoliage/client/Hooks.kt
Normal file
112
src/main/kotlin/mods/betterfoliage/client/Hooks.kt
Normal file
@@ -0,0 +1,112 @@
|
||||
@file:JvmName("Hooks")
|
||||
package mods.betterfoliage.client
|
||||
|
||||
import mods.betterfoliage.client.chunk.ChunkOverlayManager
|
||||
import mods.betterfoliage.client.config.BlockConfig
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.render.*
|
||||
import mods.octarinecore.ThreadLocalDelegate
|
||||
import mods.octarinecore.client.render.*
|
||||
import mods.octarinecore.client.render.lighting.DefaultLightingCtx
|
||||
import mods.octarinecore.common.plus
|
||||
import net.minecraft.block.Block
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.block.Blocks
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.renderer.BlockRendererDispatcher
|
||||
import net.minecraft.client.renderer.BufferBuilder
|
||||
import net.minecraft.client.world.ClientWorld
|
||||
import net.minecraft.util.BlockRenderLayer
|
||||
import net.minecraft.util.BlockRenderLayer.CUTOUT
|
||||
import net.minecraft.util.BlockRenderLayer.CUTOUT_MIPPED
|
||||
import net.minecraft.util.Direction
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.util.math.shapes.VoxelShape
|
||||
import net.minecraft.util.math.shapes.VoxelShapes
|
||||
import net.minecraft.world.IBlockReader
|
||||
import net.minecraft.world.IEnviromentBlockReader
|
||||
import net.minecraft.world.World
|
||||
import net.minecraftforge.client.model.data.IModelData
|
||||
import java.util.*
|
||||
|
||||
fun getAmbientOcclusionLightValueOverride(original: Float, state: BlockState): Float {
|
||||
if (Config.enabled && Config.roundLogs.enabled && BlockConfig.logBlocks.matchesClass(state.block)) return Config.roundLogs.dimming.toFloat();
|
||||
return original
|
||||
}
|
||||
|
||||
fun getUseNeighborBrightnessOverride(original: Boolean, state: BlockState): Boolean {
|
||||
return original || (Config.enabled && Config.roundLogs.enabled && BlockConfig.logBlocks.matchesClass(state.block));
|
||||
}
|
||||
|
||||
fun onClientBlockChanged(worldClient: ClientWorld, pos: BlockPos, oldState: BlockState, newState: BlockState, flags: Int) {
|
||||
ChunkOverlayManager.onBlockChange(worldClient, pos)
|
||||
}
|
||||
|
||||
fun onRandomDisplayTick(block: Block, state: BlockState, world: World, pos: BlockPos, random: Random) {
|
||||
if (Config.enabled &&
|
||||
Config.risingSoul.enabled &&
|
||||
state.block == Blocks.SOUL_SAND &&
|
||||
world.isAirBlock(pos + up1) &&
|
||||
Math.random() < Config.risingSoul.chance) {
|
||||
EntityRisingSoulFX(world, pos).addIfValid()
|
||||
}
|
||||
|
||||
if (Config.enabled &&
|
||||
Config.fallingLeaves.enabled &&
|
||||
BlockConfig.leafBlocks.matchesClass(state.block) &&
|
||||
world.isAirBlock(pos + down1) &&
|
||||
Math.random() < Config.fallingLeaves.chance) {
|
||||
EntityFallingLeavesFX(world, pos).addIfValid()
|
||||
}
|
||||
}
|
||||
|
||||
fun getVoxelShapeOverride(state: BlockState, reader: IBlockReader, pos: BlockPos, dir: Direction): VoxelShape {
|
||||
if (LogRegistry[state, reader, pos] != null) return VoxelShapes.empty()
|
||||
return state.func_215702_a(reader, pos, dir)
|
||||
}
|
||||
|
||||
val lightingCtx by ThreadLocalDelegate { DefaultLightingCtx(BasicBlockCtx(NonNullWorld, BlockPos.ZERO)) }
|
||||
fun renderWorldBlock(dispatcher: BlockRendererDispatcher,
|
||||
state: BlockState,
|
||||
pos: BlockPos,
|
||||
reader: IEnviromentBlockReader,
|
||||
buffer: BufferBuilder,
|
||||
random: Random,
|
||||
modelData: IModelData,
|
||||
layer: BlockRenderLayer
|
||||
): Boolean {
|
||||
// build context
|
||||
val blockCtx = CachedBlockCtx(reader, pos)
|
||||
val renderCtx = RenderCtx(dispatcher, buffer, layer, random)
|
||||
lightingCtx.reset(blockCtx)
|
||||
val combinedCtx = CombinedContext(blockCtx, renderCtx, lightingCtx)
|
||||
|
||||
// loop render decorators
|
||||
val doBaseRender = state.canRenderInLayer(layer) || (layer == targetCutoutLayer && state.canRenderInLayer(otherCutoutLayer))
|
||||
Client.renderers.forEach { renderer ->
|
||||
if (renderer.isEligible(combinedCtx)) {
|
||||
// render on the block's default layer
|
||||
// also render on the cutout layer if the renderer requires it
|
||||
|
||||
val doCutoutRender = renderer.renderOnCutout && layer == targetCutoutLayer
|
||||
val stopRender = renderer.onlyOnCutout && !layer.isCutout
|
||||
|
||||
if ((doBaseRender || doCutoutRender) && !stopRender) {
|
||||
renderer.render(combinedCtx)
|
||||
return combinedCtx.hasRendered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no render decorators have taken on this block, proceed to normal rendering
|
||||
combinedCtx.render()
|
||||
return combinedCtx.hasRendered
|
||||
}
|
||||
|
||||
fun canRenderInLayerOverride(state: BlockState, layer: BlockRenderLayer) = state.canRenderInLayer(layer) || layer == targetCutoutLayer
|
||||
|
||||
fun canRenderInLayerOverrideOptifine(state: BlockState, optifineReflector: Any?, layerArray: Array<Any>) =
|
||||
canRenderInLayerOverride(state, layerArray[0] as BlockRenderLayer)
|
||||
|
||||
val targetCutoutLayer: BlockRenderLayer get() = if (Minecraft.getInstance().gameSettings.mipmapLevels > 0) CUTOUT_MIPPED else CUTOUT
|
||||
val otherCutoutLayer: BlockRenderLayer get() = if (Minecraft.getInstance().gameSettings.mipmapLevels > 0) CUTOUT else CUTOUT_MIPPED
|
||||
125
src/main/kotlin/mods/betterfoliage/client/chunk/Overlay.kt
Normal file
125
src/main/kotlin/mods/betterfoliage/client/chunk/Overlay.kt
Normal file
@@ -0,0 +1,125 @@
|
||||
package mods.betterfoliage.client.chunk
|
||||
|
||||
import mods.octarinecore.ChunkCacheOF
|
||||
import mods.octarinecore.client.render.BlockCtx
|
||||
import mods.octarinecore.metaprog.get
|
||||
import mods.octarinecore.metaprog.isInstance
|
||||
import net.minecraft.client.renderer.chunk.ChunkRenderCache
|
||||
import net.minecraft.client.world.ClientWorld
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.util.math.ChunkPos
|
||||
import net.minecraft.world.IEnviromentBlockReader
|
||||
import net.minecraft.world.IWorldReader
|
||||
import net.minecraft.world.dimension.DimensionType
|
||||
import net.minecraftforge.common.MinecraftForge
|
||||
import net.minecraftforge.event.world.ChunkEvent
|
||||
import net.minecraftforge.event.world.WorldEvent
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent
|
||||
import java.util.*
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.MutableMap
|
||||
import kotlin.collections.associateWith
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.collections.mutableListOf
|
||||
import kotlin.collections.mutableMapOf
|
||||
import kotlin.collections.set
|
||||
|
||||
val IEnviromentBlockReader.dimType: DimensionType get() = when {
|
||||
this is IWorldReader -> dimension.type
|
||||
this is ChunkRenderCache -> world.dimension.type
|
||||
this.isInstance(ChunkCacheOF) -> this[ChunkCacheOF.chunkCache].world.dimension.type
|
||||
else -> throw IllegalArgumentException("DimensionType of world with class ${this::class.qualifiedName} cannot be determined!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents some form of arbitrary non-persistent data that can be calculated and cached for each block position
|
||||
*/
|
||||
interface ChunkOverlayLayer<T> {
|
||||
fun calculate(ctx: BlockCtx): T
|
||||
fun onBlockUpdate(world: IEnviromentBlockReader, pos: BlockPos)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query, lazy calculation and lifecycle management of multiple layers of chunk overlay data.
|
||||
*/
|
||||
object ChunkOverlayManager {
|
||||
|
||||
var tempCounter = 0
|
||||
|
||||
init {
|
||||
MinecraftForge.EVENT_BUS.register(this)
|
||||
}
|
||||
|
||||
val chunkData = IdentityHashMap<DimensionType, MutableMap<ChunkPos, ChunkOverlayData>>()
|
||||
val layers = mutableListOf<ChunkOverlayLayer<*>>()
|
||||
|
||||
/**
|
||||
* Get the overlay data for a given layer and position
|
||||
*
|
||||
* @param layer Overlay layer to query
|
||||
* @param reader World to use if calculation of overlay value is necessary
|
||||
* @param pos Block position
|
||||
*/
|
||||
fun <T> get(layer: ChunkOverlayLayer<T>, ctx: BlockCtx): T? {
|
||||
val data = chunkData[ctx.world.dimType]?.get(ChunkPos(ctx.pos)) ?: return null
|
||||
data.get(layer, ctx.pos).let { value ->
|
||||
if (value !== ChunkOverlayData.UNCALCULATED) return value
|
||||
val newValue = layer.calculate(ctx)
|
||||
data.set(layer, ctx.pos, newValue)
|
||||
return newValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the overlay data for a given layer and position
|
||||
*
|
||||
* @param layer Overlay layer to clear
|
||||
* @param pos Block position
|
||||
*/
|
||||
fun <T> clear(dimension: DimensionType, layer: ChunkOverlayLayer<T>, pos: BlockPos) {
|
||||
chunkData[dimension]?.get(ChunkPos(pos))?.clear(layer, pos)
|
||||
}
|
||||
|
||||
fun onBlockChange(world: ClientWorld, pos: BlockPos) {
|
||||
if (chunkData[world.dimType]?.containsKey(ChunkPos(pos)) == true) {
|
||||
layers.forEach { layer -> layer.onBlockUpdate(world, pos) }
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
fun handleLoadWorld(event: WorldEvent.Load) = (event.world as? ClientWorld)?.let { world ->
|
||||
chunkData[world.dimType] = mutableMapOf()
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
fun handleUnloadWorld(event: WorldEvent.Unload) = (event.world as? ClientWorld)?.let { world ->
|
||||
chunkData.remove(world.dimType)
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
fun handleLoadChunk(event: ChunkEvent.Load) = (event.world as? ClientWorld)?.let { world ->
|
||||
chunkData[world.dimType]?.let { chunks ->
|
||||
// check for existence first because Optifine fires a TON of these
|
||||
if (event.chunk.pos !in chunks.keys) chunks[event.chunk.pos] = ChunkOverlayData(layers)
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
fun handleUnloadChunk(event: ChunkEvent.Unload) = (event.world as? ClientWorld)?.let { world ->
|
||||
chunkData[world.dimType]?.remove(event.chunk.pos)
|
||||
}
|
||||
}
|
||||
|
||||
class ChunkOverlayData(layers: List<ChunkOverlayLayer<*>>) {
|
||||
val BlockPos.isValid: Boolean get() = y in validYRange
|
||||
val rawData = layers.associateWith { emptyOverlay() }
|
||||
fun <T> get(layer: ChunkOverlayLayer<T>, pos: BlockPos): T? = if (pos.isValid) rawData[layer]?.get(pos.x and 15)?.get(pos.z and 15)?.get(pos.y) as T? else null
|
||||
fun <T> set(layer: ChunkOverlayLayer<T>, pos: BlockPos, data: T) = if (pos.isValid) rawData[layer]?.get(pos.x and 15)?.get(pos.z and 15)?.set(pos.y, data) else null
|
||||
fun <T> clear(layer: ChunkOverlayLayer<T>, pos: BlockPos) = if (pos.isValid) rawData[layer]?.get(pos.x and 15)?.get(pos.z and 15)?.set(pos.y, UNCALCULATED) else null
|
||||
|
||||
companion object {
|
||||
val UNCALCULATED = object {}
|
||||
fun emptyOverlay() = Array(16) { Array(16) { Array<Any?>(256) { UNCALCULATED }}}
|
||||
val validYRange = 0 until 256
|
||||
}
|
||||
}
|
||||
169
src/main/kotlin/mods/betterfoliage/client/config/Config.kt
Normal file
169
src/main/kotlin/mods/betterfoliage/client/config/Config.kt
Normal file
@@ -0,0 +1,169 @@
|
||||
package mods.betterfoliage.client.config
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.integration.ShadersModIntegration
|
||||
import mods.octarinecore.common.config.*
|
||||
import net.minecraft.util.ResourceLocation
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent
|
||||
import net.minecraftforge.fml.config.ModConfig
|
||||
|
||||
private fun featureEnable() = boolean(true).lang("enabled")
|
||||
|
||||
// Config singleton
|
||||
object Config : DelegatingConfig(BetterFoliageMod.MOD_ID, BetterFoliageMod.MOD_ID) {
|
||||
|
||||
val enabled by boolean(true)
|
||||
val nVidia by boolean(false)
|
||||
|
||||
object leaves : ConfigCategory() {
|
||||
val enabled by featureEnable()
|
||||
val snowEnabled by boolean(true)
|
||||
val hOffset by double(max=0.4, default=0.2).lang("hOffset")
|
||||
val vOffset by double(max=0.4, default=0.1).lang("vOffset")
|
||||
val size by double(min=0.75, max=2.5, default=1.4).lang("size")
|
||||
val dense by boolean(false)
|
||||
val hideInternal by boolean(true)
|
||||
}
|
||||
|
||||
object shortGrass : ConfigCategory(){
|
||||
val grassEnabled by boolean(true)
|
||||
val myceliumEnabled by boolean(true)
|
||||
val snowEnabled by boolean(true)
|
||||
val hOffset by double(max=0.4, default=0.2).lang("hOffset")
|
||||
val heightMin by double(min=0.1, max=2.5, default=0.6).lang("heightMin")
|
||||
val heightMax by double(min=0.1, max=2.5, default=0.8).lang("heightMax")
|
||||
val size by double(min=0.5, max=1.5, default=1.0).lang("size")
|
||||
val population by int(max=64, default=64).lang("population")
|
||||
val useGenerated by boolean(false)
|
||||
val shaderWind by boolean(true).lang("shaderWind")
|
||||
val saturationThreshold by double(default=0.1)
|
||||
}
|
||||
|
||||
object connectedGrass : ConfigCategory(){
|
||||
val enabled by boolean(true)
|
||||
val snowEnabled by boolean(false)
|
||||
}
|
||||
|
||||
object roundLogs : ConfigCategory(){
|
||||
val enabled by featureEnable()
|
||||
val radiusSmall by double(max=0.5, default=0.25)
|
||||
val radiusLarge by double(max=0.5, default=0.44)
|
||||
val dimming by double(default = 0.7)
|
||||
val connectSolids by boolean(false)
|
||||
val lenientConnect by boolean(true)
|
||||
val connectPerpendicular by boolean(true)
|
||||
val connectGrass by boolean(true)
|
||||
val defaultY by boolean(false)
|
||||
val zProtection by double(min = 0.9, default = 0.99)
|
||||
}
|
||||
|
||||
object cactus : ConfigCategory(){
|
||||
val enabled by featureEnable()
|
||||
val size by double(min=0.5, max=1.5, default=0.8).lang("size")
|
||||
val sizeVariation by double(max=0.5, default=0.1)
|
||||
val hOffset by double(max=0.5, default=0.1).lang("hOffset")
|
||||
}
|
||||
|
||||
object lilypad : ConfigCategory(){
|
||||
val enabled by featureEnable()
|
||||
val hOffset by double(max=0.25, default=0.1).lang("hOffset")
|
||||
val flowerChance by int(max=64, default=16, min=0)
|
||||
}
|
||||
|
||||
object reed : ConfigCategory(){
|
||||
val enabled by featureEnable()
|
||||
val hOffset by double(max=0.4, default=0.2).lang("hOffset")
|
||||
val heightMin by double(min=1.5, max=3.5, default=1.7).lang("heightMin")
|
||||
val heightMax by double(min=1.5, max=3.5, default=2.2).lang("heightMax")
|
||||
val population by int(max=64, default=32).lang("population")
|
||||
val minBiomeTemp by double(default=0.4)
|
||||
val minBiomeRainfall by double(default=0.4)
|
||||
// val biomes by biomeList { it.filterTemp(0.4f, null) && it.filterRain(0.4f, null) }
|
||||
val shaderWind by boolean(true).lang("shaderWind")
|
||||
}
|
||||
|
||||
object algae : ConfigCategory(){
|
||||
val enabled by featureEnable()
|
||||
val hOffset by double(max=0.25, default=0.1).lang("hOffset")
|
||||
val size by double(min=0.5, max=1.5, default=1.0).lang("size")
|
||||
val heightMin by double(min=0.1, max=1.5, default=0.5).lang("heightMin")
|
||||
val heightMax by double(min=0.1, max=1.5, default=1.0).lang("heightMax")
|
||||
val population by int(max=64, default=48).lang("population")
|
||||
// val biomes by biomeList { it.filterClass("river", "ocean") }
|
||||
val shaderWind by boolean(true).lang("shaderWind")
|
||||
}
|
||||
|
||||
object coral : ConfigCategory(){
|
||||
val enabled by featureEnable()
|
||||
val shallowWater by boolean(false)
|
||||
val hOffset by double(max=0.4, default=0.2).lang("hOffset")
|
||||
val vOffset by double(max=0.4, default=0.1).lang("vOffset")
|
||||
val size by double(min=0.5, max=1.5, default=0.7).lang("size")
|
||||
val crustSize by double(min=0.5, max=1.5, default=1.4)
|
||||
val chance by int(max=64, default=32)
|
||||
val population by int(max=64, default=48).lang("population")
|
||||
// val biomes by biomeList { it.filterClass("river", "ocean", "beach") }
|
||||
}
|
||||
|
||||
object netherrack : ConfigCategory(){
|
||||
val enabled by featureEnable()
|
||||
val hOffset by double(max=0.4, default=0.2).lang("hOffset")
|
||||
val heightMin by double(min=0.1, max=1.5, default=0.6).lang("heightMin")
|
||||
val heightMax by double(min=0.1, max=1.5, default=0.8).lang("heightMax")
|
||||
val size by double(min=0.5, max=1.5, default=1.0).lang("size")
|
||||
}
|
||||
|
||||
object fallingLeaves : ConfigCategory(){
|
||||
val enabled by featureEnable()
|
||||
val speed by double(min=0.01, max=0.15, default=0.05)
|
||||
val windStrength by double(min=0.1, max=2.0, default=0.5)
|
||||
val stormStrength by double(min=0.1, max=2.0, default=0.8)
|
||||
val size by double(min=0.25, max=1.5, default=0.75).lang("size")
|
||||
val chance by double(min=0.001, max=1.0, default=0.02)
|
||||
val perturb by double(min=0.01, max=1.0, default=0.25)
|
||||
val lifetime by double(min=1.0, max=15.0, default=5.0)
|
||||
val opacityHack by boolean(true)
|
||||
}
|
||||
|
||||
object risingSoul : ConfigCategory(){
|
||||
val enabled by featureEnable()
|
||||
val chance by double(min=0.001, max=1.0, default=0.02)
|
||||
val perturb by double(min=0.01, max=0.25, default=0.05)
|
||||
val headSize by double(min=0.25, max=1.5, default=1.0)
|
||||
val trailSize by double(min=0.25, max=1.5, default=0.75)
|
||||
val opacity by double(min=0.05, max=1.0, default=0.5)
|
||||
val sizeDecay by double(min=0.5, max=1.0, default=0.97)
|
||||
val opacityDecay by double(min=0.5, max=1.0, default=0.97)
|
||||
val lifetime by double(min=1.0, max=15.0, default=4.0)
|
||||
val trailLength by int(min=2, max=128, default=48)
|
||||
val trailDensity by int(min=1, max=16, default=3)
|
||||
}
|
||||
}
|
||||
|
||||
object BlockConfig {
|
||||
private val list = mutableListOf<Any>()
|
||||
|
||||
val leafBlocks = blocks("leaves_blocks_default.cfg")
|
||||
val leafModels = models("leaves_models_default.cfg")
|
||||
val grassBlocks = blocks("grass_blocks_default.cfg")
|
||||
val grassModels = models("grass_models_default.cfg")
|
||||
val mycelium = blocks("mycelium_blocks_default.cfg")
|
||||
// val dirt = blocks("dirt_default.cfg")
|
||||
val crops = blocks("crop_default.cfg")
|
||||
val logBlocks = blocks("log_blocks_default.cfg")
|
||||
val logModels = models("log_models_default.cfg")
|
||||
val lilypad = blocks("lilypad_default.cfg")
|
||||
|
||||
init { BetterFoliageMod.bus.register(this) }
|
||||
private fun blocks(cfgName: String) = ConfigurableBlockMatcher(BetterFoliage.logDetail, ResourceLocation(BetterFoliageMod.MOD_ID, cfgName)).apply { list.add(this) }
|
||||
private fun models(cfgName: String) = ModelTextureListConfiguration(BetterFoliage.logDetail, ResourceLocation(BetterFoliageMod.MOD_ID, cfgName)).apply { list.add(this) }
|
||||
|
||||
@SubscribeEvent
|
||||
fun onConfig(event: ModConfig.ModConfigEvent) {
|
||||
list.forEach { when(it) {
|
||||
is ConfigurableBlockMatcher -> it.readDefaults()
|
||||
is ModelTextureListConfiguration -> it.readDefaults()
|
||||
} }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package mods.betterfoliage.client.integration
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.client.config.BlockConfig
|
||||
import mods.betterfoliage.client.render.AsyncLogDiscovery
|
||||
import mods.betterfoliage.client.render.column.ColumnTextureInfo
|
||||
import mods.betterfoliage.client.render.column.SimpleColumnInfo
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.betterfoliage.client.texture.LeafInfo
|
||||
import mods.betterfoliage.client.texture.defaultRegisterLeaf
|
||||
import mods.octarinecore.HasLogger
|
||||
import mods.octarinecore.Map
|
||||
import mods.octarinecore.ResourceLocation
|
||||
import mods.octarinecore.String
|
||||
import mods.octarinecore.client.resource.*
|
||||
import mods.octarinecore.metaprog.*
|
||||
import mods.octarinecore.metaprog.ClassRef.Companion.boolean
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.renderer.model.ModelBakery
|
||||
import net.minecraft.resources.IResourceManager
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.world.IBlockReader
|
||||
import net.minecraftforge.fml.ModList
|
||||
import org.apache.logging.log4j.Level
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
|
||||
val TextureLeaves = ClassRef<Any>("forestry.arboriculture.models.TextureLeaves")
|
||||
val TextureLeaves_leafTextures = FieldRef(TextureLeaves, "leafTextures", Map)
|
||||
val TextureLeaves_plain = FieldRef(TextureLeaves, "plain", ResourceLocation)
|
||||
val TextureLeaves_fancy = FieldRef(TextureLeaves, "fancy", ResourceLocation)
|
||||
val TextureLeaves_pollinatedPlain = FieldRef(TextureLeaves, "pollinatedPlain", ResourceLocation)
|
||||
val TextureLeaves_pollinatedFancy = FieldRef(TextureLeaves, "pollinatedFancy", ResourceLocation)
|
||||
|
||||
|
||||
val TileLeaves = ClassRef<Any>("forestry.arboriculture.tiles.TileLeaves")
|
||||
val TileLeaves_getLeaveSprite = MethodRef(TileLeaves, "getLeaveSprite", ResourceLocation, boolean)
|
||||
val PropertyWoodType = ClassRef<Any>("forestry.arboriculture.blocks.PropertyWoodType")
|
||||
val IWoodType = ClassRef<Any>("forestry.api.arboriculture.IWoodType")
|
||||
val IWoodType_barkTex = MethodRef(IWoodType, "getBarkTexture", String)
|
||||
val IWoodType_heartTex = MethodRef(IWoodType, "getHeartTexture", String)
|
||||
|
||||
val PropertyTreeType = ClassRef<Any>("forestry.arboriculture.blocks.PropertyTreeType")
|
||||
val IAlleleTreeSpecies = ClassRef<Any>("forestry.api.arboriculture.IAlleleTreeSpecies")
|
||||
val ILeafSpriteProvider = ClassRef<Any>("forestry.api.arboriculture.ILeafSpriteProvider")
|
||||
val TreeDefinition = ClassRef<Any>("forestry.arboriculture.genetics.TreeDefinition")
|
||||
|
||||
val IAlleleTreeSpecies_getLeafSpriteProvider = MethodRef(IAlleleTreeSpecies, "getLeafSpriteProvider", ILeafSpriteProvider)
|
||||
val TreeDefinition_species = FieldRef(TreeDefinition, "species", IAlleleTreeSpecies)
|
||||
val ILeafSpriteProvider_getSprite = MethodRef(ILeafSpriteProvider, "getSprite", ResourceLocation, boolean, boolean)
|
||||
|
||||
object ForestryIntegration {
|
||||
init {
|
||||
if (ModList.get().isLoaded("forestry") && allAvailable(TileLeaves_getLeaveSprite, IAlleleTreeSpecies_getLeafSpriteProvider, ILeafSpriteProvider_getSprite)) {
|
||||
// Just keep it inactive for now until Forestry updates
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ForestryLeafDiscovery : HasLogger, AsyncSpriteProvider<ModelBakery>, ModelRenderRegistry<LeafInfo> {
|
||||
override val logger = BetterFoliage.logDetail
|
||||
var idToValue = emptyMap<Identifier, LeafInfo>()
|
||||
|
||||
override fun get(state: BlockState, world: IBlockReader, pos: BlockPos): LeafInfo? {
|
||||
// check variant property (used in decorative leaves)
|
||||
state.values.entries.find {
|
||||
PropertyTreeType.isInstance(it.key) && TreeDefinition.isInstance(it.value)
|
||||
} ?.let {
|
||||
val species = it.value[TreeDefinition_species]
|
||||
val spriteProvider = species[IAlleleTreeSpecies_getLeafSpriteProvider]()
|
||||
val textureLoc = spriteProvider[ILeafSpriteProvider_getSprite](false, Minecraft.isFancyGraphicsEnabled())
|
||||
return idToValue[textureLoc]
|
||||
}
|
||||
|
||||
// extract leaf texture information from TileEntity
|
||||
val tile = world.getTileEntity(pos) ?: return null
|
||||
if (!TileLeaves.isInstance(tile)) return null
|
||||
val textureLoc = tile[TileLeaves_getLeaveSprite](Minecraft.isFancyGraphicsEnabled())
|
||||
return idToValue[textureLoc]
|
||||
}
|
||||
|
||||
override fun setup(manager: IResourceManager, bakeryF: CompletableFuture<ModelBakery>, atlasFuture: AtlasFuture): StitchPhases {
|
||||
val futures = mutableMapOf<Identifier, CompletableFuture<LeafInfo>>()
|
||||
|
||||
return StitchPhases(
|
||||
discovery = bakeryF.thenRunAsync {
|
||||
val allLeaves = TextureLeaves_leafTextures.getStatic()
|
||||
allLeaves.entries.forEach { (type, leaves) ->
|
||||
log("base leaf type $type")
|
||||
leaves!!
|
||||
listOf(
|
||||
leaves[TextureLeaves_plain], leaves[TextureLeaves_pollinatedPlain],
|
||||
leaves[TextureLeaves_fancy], leaves[TextureLeaves_pollinatedFancy]
|
||||
).forEach { textureLocation ->
|
||||
futures[textureLocation] = defaultRegisterLeaf(textureLocation, atlasFuture)
|
||||
}
|
||||
}
|
||||
},
|
||||
cleanup = atlasFuture.runAfter {
|
||||
idToValue = futures.mapValues { it.value.get() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object ForestryLogDiscovery : ModelDiscovery<ColumnTextureInfo>() {
|
||||
override val logger = BetterFoliage.logDetail
|
||||
override fun processModel(ctx: ModelDiscoveryContext, atlas: AtlasFuture): CompletableFuture<ColumnTextureInfo>? {
|
||||
// respect class list to avoid triggering on fences, stairs, etc.
|
||||
if (!BlockConfig.logBlocks.matchesClass(ctx.state.block)) return null
|
||||
|
||||
// find wood type property
|
||||
val woodType = ctx.state.values.entries.find {
|
||||
PropertyWoodType.isInstance(it.key) && IWoodType.isInstance(it.value)
|
||||
}
|
||||
if (woodType != null) {
|
||||
logger.log(Level.DEBUG, "ForestryLogRegistry: block state ${ctx.state}")
|
||||
logger.log(Level.DEBUG, "ForestryLogRegistry: variant ${woodType.value}")
|
||||
|
||||
// get texture names for wood type
|
||||
val bark = woodType.value[IWoodType_barkTex]()
|
||||
val heart = woodType.value[IWoodType_heartTex]()
|
||||
logger.log(Level.DEBUG, "ForestryLogSupport: textures [heart=$heart, bark=$bark]")
|
||||
|
||||
val heartSprite = atlas.sprite(heart)
|
||||
val barkSprite = atlas.sprite(bark)
|
||||
return atlas.mapAfter {
|
||||
SimpleColumnInfo(AsyncLogDiscovery.getAxis(ctx.state), heartSprite.get(), heartSprite.get(), listOf(barkSprite.get()))
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package mods.betterfoliage.client.integration
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.octarinecore.*
|
||||
import mods.octarinecore.client.render.CombinedContext
|
||||
import mods.octarinecore.metaprog.allAvailable
|
||||
import mods.octarinecore.metaprog.reflectField
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.renderer.model.BakedQuad
|
||||
import net.minecraft.client.renderer.vertex.DefaultVertexFormats
|
||||
import net.minecraft.util.Direction.UP
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import org.apache.logging.log4j.Level
|
||||
|
||||
/**
|
||||
* Integration for OptiFine custom block colors.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
object OptifineCustomColors {
|
||||
|
||||
val isColorAvailable = allAvailable(CustomColors, CustomColors.getColorMultiplier)
|
||||
|
||||
init {
|
||||
BetterFoliage.log(Level.INFO, "Optifine custom color support is ${if (isColorAvailable) "enabled" else "disabled" }")
|
||||
}
|
||||
|
||||
val renderEnv by ThreadLocalDelegate { OptifineRenderEnv() }
|
||||
val fakeQuad = BakedQuad(IntArray(0), 1, UP, null, true, DefaultVertexFormats.BLOCK)
|
||||
|
||||
fun getBlockColor(ctx: CombinedContext): Int {
|
||||
val ofColor = if (isColorAvailable && Minecraft.getInstance().gameSettings.reflectField<Boolean>("ofCustomColors") == true) {
|
||||
renderEnv.reset(ctx.state, ctx.pos)
|
||||
CustomColors.getColorMultiplier.invokeStatic(fakeQuad, ctx.state, ctx.world, ctx.pos, renderEnv.wrapped) as? Int
|
||||
} else null
|
||||
return if (ofColor == null || ofColor == -1) ctx.lightingCtx.color else ofColor
|
||||
}
|
||||
}
|
||||
|
||||
class OptifineRenderEnv {
|
||||
val wrapped: Any = RenderEnv.element!!.getDeclaredConstructor(BlockState.element, BlockPos.element).let {
|
||||
it.isAccessible = true
|
||||
it.newInstance(null, null)
|
||||
}
|
||||
|
||||
fun reset(state: BlockState, pos: BlockPos) {
|
||||
RenderEnv.reset.invoke(wrapped, state, pos)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package mods.betterfoliage.client.integration
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.client.render.LogRegistry
|
||||
import mods.betterfoliage.client.render.column.ColumnTextureInfo
|
||||
import mods.betterfoliage.client.render.column.SimpleColumnInfo
|
||||
import mods.betterfoliage.client.resource.Sprite
|
||||
import mods.octarinecore.client.render.CombinedContext
|
||||
import mods.octarinecore.client.render.Quad
|
||||
import mods.octarinecore.client.render.lighting.QuadIconResolver
|
||||
import mods.octarinecore.client.resource.*
|
||||
import mods.octarinecore.common.rotate
|
||||
import mods.octarinecore.metaprog.ClassRef
|
||||
import mods.octarinecore.metaprog.allAvailable
|
||||
import net.minecraft.client.renderer.model.BlockModel
|
||||
import net.minecraft.util.Direction
|
||||
import net.minecraft.util.Direction.*
|
||||
import net.minecraft.util.ResourceLocation
|
||||
import net.minecraftforge.fml.ModList
|
||||
import org.apache.logging.log4j.Level
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
|
||||
object IC2RubberIntegration {
|
||||
|
||||
val BlockRubWood = ClassRef<Any>("ic2.core.block.BlockRubWood")
|
||||
|
||||
init {
|
||||
if (ModList.get().isLoaded("ic2") && allAvailable(BlockRubWood)) {
|
||||
// keep it inactive for now until IC2 updates
|
||||
// BetterFoliage.log(Level.INFO, "IC2 rubber support initialized")
|
||||
// LogRegistry.registries.add(IC2LogDiscovery)
|
||||
// BetterFoliage.blockSprites.providers.add(IC2LogDiscovery)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Probably unneeded, as TechReborn went Fabric-only
|
||||
/*
|
||||
object TechRebornRubberIntegration {
|
||||
|
||||
val BlockRubberLog = ClassRef<Any>("techreborn.blocks.BlockRubberLog")
|
||||
|
||||
init {
|
||||
if (ModList.get().isLoaded("techreborn") && allAvailable(BlockRubberLog)) {
|
||||
BetterFoliage.log(Level.INFO, "TechReborn rubber support initialized")
|
||||
LogRegistry.registries.add(TechRebornLogDiscovery)
|
||||
BetterFoliage.blockSprites.providers.add(TechRebornLogDiscovery)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
class RubberLogInfo(
|
||||
axis: Axis?,
|
||||
val spotDir: Direction,
|
||||
topTexture: Sprite,
|
||||
bottomTexture: Sprite,
|
||||
val spotTexture: Sprite,
|
||||
sideTextures: List<Sprite>
|
||||
) : SimpleColumnInfo(axis, topTexture, bottomTexture, sideTextures) {
|
||||
|
||||
override val side: QuadIconResolver = { ctx: CombinedContext, idx: Int, quad: Quad ->
|
||||
val worldFace = (if ((idx and 1) == 0) SOUTH else EAST).rotate(ctx.modelRotation)
|
||||
if (worldFace == spotDir) spotTexture else {
|
||||
val sideIdx = if (this.sideTextures.size > 1) (ctx.semiRandom(1) + dirToIdx[worldFace.ordinal]) % this.sideTextures.size else 0
|
||||
this.sideTextures[sideIdx]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object IC2LogDiscovery : ModelDiscovery<ColumnTextureInfo>() {
|
||||
override val logger = BetterFoliage.logDetail
|
||||
|
||||
override fun processModel(ctx: ModelDiscoveryContext, atlas: AtlasFuture): CompletableFuture<ColumnTextureInfo>? {
|
||||
// check for proper block class, existence of ModelBlock, and "state" blockstate property
|
||||
if (!IC2RubberIntegration.BlockRubWood.isInstance(ctx.state.block)) return null
|
||||
val blockLoc = ctx.models.firstOrNull() as Pair<BlockModel, ResourceLocation> ?: return null
|
||||
val type = ctx.state.values.entries.find { it.key.getName() == "state" }?.value?.toString() ?: return null
|
||||
|
||||
// logs with no rubber spot
|
||||
if (blockLoc.derivesFrom(ResourceLocation("block/cube_column"))) {
|
||||
val axis = when(type) {
|
||||
"plain_y" -> Axis.Y
|
||||
"plain_x" -> Axis.X
|
||||
"plain_z" -> Axis.Z
|
||||
else -> null
|
||||
}
|
||||
val textureNames = listOf("end", "side").map { blockLoc.first.resolveTextureName(it) }
|
||||
if (textureNames.any { it == "missingno" }) return null
|
||||
log("IC2LogSupport: block state ${ctx.state.toString()}")
|
||||
log("IC2LogSupport: axis=$axis, end=${textureNames[0]}, side=${textureNames[1]}")
|
||||
val endSprite = atlas.sprite(textureNames[0])
|
||||
val sideSprite = atlas.sprite(textureNames[1])
|
||||
return atlas.mapAfter {
|
||||
SimpleColumnInfo(axis, endSprite.get(), endSprite.get(), listOf(sideSprite.get()))
|
||||
}
|
||||
}
|
||||
|
||||
// logs with rubber spot
|
||||
val spotDir = when(type) {
|
||||
"dry_north", "wet_north" -> NORTH
|
||||
"dry_south", "wet_south" -> SOUTH
|
||||
"dry_west", "wet_west" -> WEST
|
||||
"dry_east", "wet_east" -> EAST
|
||||
else -> null
|
||||
}
|
||||
val textureNames = listOf("up", "down", "north", "south").map { blockLoc.first.resolveTextureName(it) }
|
||||
if (textureNames.any { it == "missingno" }) return null
|
||||
log("IC2LogSupport: block state ${ctx.state.toString()}")
|
||||
log("IC2LogSupport: spotDir=$spotDir, up=${textureNames[0]}, down=${textureNames[1]}, side=${textureNames[2]}, spot=${textureNames[3]}")
|
||||
val upSprite = atlas.sprite(textureNames[0])
|
||||
val downSprite = atlas.sprite(textureNames[1])
|
||||
val sideSprite = atlas.sprite(textureNames[2])
|
||||
val spotSprite = atlas.sprite(textureNames[3])
|
||||
return if (spotDir != null) atlas.mapAfter {
|
||||
RubberLogInfo(Axis.Y, spotDir, upSprite.get(), downSprite.get(), spotSprite.get(), listOf(sideSprite.get()))
|
||||
} else atlas.mapAfter {
|
||||
SimpleColumnInfo(Axis.Y, upSprite.get(), downSprite.get(), listOf(sideSprite.get()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
object TechRebornLogDiscovery : ModelDiscovery<ColumnTextureInfo>() {
|
||||
override val logger = BetterFoliage.logDetail
|
||||
|
||||
override fun processModel(ctx: ModelDiscoveryContext, atlas: AtlasFuture): CompletableFuture<ColumnTextureInfo>? {
|
||||
// check for proper block class, existence of ModelBlock
|
||||
if (!TechRebornRubberIntegration.BlockRubberLog.isInstance(ctx.state.block)) return null
|
||||
val blockLoc = ctx.models.map { it as? Pair<BlockModel, ResourceLocation> }.firstOrNull() ?: return null
|
||||
|
||||
val hasSap = ctx.state.values.entries.find { it.key.getName() == "hassap" }?.value as? Boolean ?: return null
|
||||
val sapSide = ctx.state.values.entries.find { it.key.getName() == "sapside" }?.value as? Direction ?: return null
|
||||
|
||||
log("$logName: block state ${ctx.state}")
|
||||
if (hasSap) {
|
||||
val textureNames = listOf("end", "side", "sapside").map { blockLoc.first.resolveTextureName(it) }
|
||||
log("$logName: spotDir=$sapSide, end=${textureNames[0]}, side=${textureNames[2]}, spot=${textureNames[3]}")
|
||||
if (textureNames.all { it != "missingno" }) {
|
||||
val endSprite = atlas.sprite(textureNames[0])
|
||||
val sideSprite = atlas.sprite(textureNames[1])
|
||||
val sapSprite = atlas.sprite(textureNames[2])
|
||||
return atlas.mapAfter {
|
||||
RubberLogInfo(Axis.Y, sapSide, endSprite.get(), endSprite.get(), sapSprite.get(), listOf(sideSprite.get()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val textureNames = listOf("end", "side").map { blockLoc.first.resolveTextureName(it) }
|
||||
log("$logName: end=${textureNames[0]}, side=${textureNames[1]}")
|
||||
if (textureNames.all { it != "missingno" }) {
|
||||
val endSprite = atlas.sprite(textureNames[0])
|
||||
val sideSprite = atlas.sprite(textureNames[1])
|
||||
return atlas.mapAfter {
|
||||
SimpleColumnInfo(Axis.Y, endSprite.get(), endSprite.get(), listOf(sideSprite.get()))
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,68 @@
|
||||
package mods.betterfoliage.client.integration
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.client.config.BlockConfig
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.texture.GrassRegistry
|
||||
import mods.betterfoliage.client.texture.LeafRegistry
|
||||
import mods.octarinecore.*
|
||||
import mods.octarinecore.client.render.CombinedContext
|
||||
import mods.octarinecore.metaprog.allAvailable
|
||||
import mods.octarinecore.metaprog.get
|
||||
import net.minecraft.block.BlockRenderType
|
||||
import net.minecraft.block.BlockRenderType.MODEL
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.block.Blocks
|
||||
import net.minecraft.client.renderer.BufferBuilder
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.world.IEnviromentBlockReader
|
||||
import org.apache.logging.log4j.Level.INFO
|
||||
|
||||
/**
|
||||
* Integration for ShadersMod.
|
||||
*/
|
||||
object ShadersModIntegration {
|
||||
|
||||
@JvmStatic val isAvailable = allAvailable(SVertexBuilder, SVertexBuilder.pushState, SVertexBuilder.pushNum, SVertexBuilder.pop)
|
||||
|
||||
val defaultLeaves = Blocks.OAK_LEAVES.defaultState
|
||||
val defaultGrass = Blocks.GRASS.defaultState
|
||||
|
||||
/**
|
||||
* Called from transformed ShadersMod code.
|
||||
* @see mods.betterfoliage.loader.BetterFoliageTransformer
|
||||
*/
|
||||
@JvmStatic fun getBlockStateOverride(state: BlockState, world: IEnviromentBlockReader, pos: BlockPos): BlockState {
|
||||
if (LeafRegistry[state, world, pos] != null) return defaultLeaves
|
||||
if (BlockConfig.crops.matchesClass(state.block)) return defaultGrass
|
||||
return state
|
||||
}
|
||||
|
||||
init {
|
||||
BetterFoliage.log(INFO, "ShadersMod integration is ${if (isAvailable) "enabled" else "disabled" }")
|
||||
}
|
||||
|
||||
inline fun renderAs(ctx: CombinedContext, renderType: BlockRenderType, enabled: Boolean = true, func: ()->Unit) =
|
||||
renderAs(ctx, ctx.state, renderType, enabled, func)
|
||||
|
||||
/** Quads rendered inside this block will use the given block entity data in shader programs. */
|
||||
inline fun renderAs(ctx: CombinedContext, state: BlockState, renderType: BlockRenderType, enabled: Boolean = true, func: ()->Unit) {
|
||||
if (isAvailable && enabled) {
|
||||
val buffer = ctx.renderCtx.renderBuffer
|
||||
val sVertexBuilder = buffer[BufferBuilder_sVertexBuilder]
|
||||
SVertexBuilder.pushState.invoke(sVertexBuilder, ctx.state, ctx.pos, ctx.world, buffer)
|
||||
func()
|
||||
SVertexBuilder.pop.invoke(sVertexBuilder)
|
||||
} else {
|
||||
func()
|
||||
}
|
||||
}
|
||||
|
||||
/** Quads rendered inside this block will behave as tallgrass blocks in shader programs. */
|
||||
inline fun grass(ctx: CombinedContext, enabled: Boolean = true, func: ()->Unit) =
|
||||
renderAs(ctx, defaultGrass, MODEL, enabled, func)
|
||||
|
||||
/** Quads rendered inside this block will behave as leaf blocks in shader programs. */
|
||||
inline fun leaves(ctx: CombinedContext, enabled: Boolean = true, func: ()->Unit) =
|
||||
renderAs(ctx, defaultLeaves, MODEL, enabled, func)
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.texture.LeafParticleRegistry
|
||||
import mods.betterfoliage.client.texture.LeafRegistry
|
||||
import mods.octarinecore.PI2
|
||||
import mods.octarinecore.client.render.AbstractEntityFX
|
||||
import mods.octarinecore.client.render.lighting.HSB
|
||||
import mods.octarinecore.common.Double3
|
||||
import mods.octarinecore.minmax
|
||||
import mods.octarinecore.random
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.renderer.BufferBuilder
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.util.math.MathHelper
|
||||
import net.minecraft.world.World
|
||||
import net.minecraftforge.common.MinecraftForge
|
||||
import net.minecraftforge.event.TickEvent
|
||||
import net.minecraftforge.event.world.WorldEvent
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent
|
||||
import org.lwjgl.opengl.GL11
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
const val rotationFactor = PI2.toFloat() / 64.0f
|
||||
|
||||
class EntityFallingLeavesFX(
|
||||
world: World, pos: BlockPos
|
||||
) : AbstractEntityFX(
|
||||
world, pos.x.toDouble() + 0.5, pos.y.toDouble(), pos.z.toDouble() + 0.5
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic val biomeBrightnessMultiplier = 0.5f
|
||||
}
|
||||
|
||||
var particleRot = rand.nextInt(64)
|
||||
var rotPositive = true
|
||||
val isMirrored = (rand.nextInt() and 1) == 1
|
||||
var wasCollided = false
|
||||
|
||||
init {
|
||||
maxAge = MathHelper.floor(random(0.6, 1.0) * Config.fallingLeaves.lifetime * 20.0)
|
||||
motionY = -Config.fallingLeaves.speed
|
||||
|
||||
particleScale = Config.fallingLeaves.size.toFloat() * 0.1f
|
||||
|
||||
val state = world.getBlockState(pos)
|
||||
val leafInfo = LeafRegistry[state, world, pos]
|
||||
val blockColor = Minecraft.getInstance().blockColors.getColor(state, world, pos, 0)
|
||||
if (leafInfo != null) {
|
||||
sprite = leafInfo.particleTextures[rand.nextInt(1024)]
|
||||
calculateParticleColor(leafInfo.averageColor, blockColor)
|
||||
} else {
|
||||
sprite = LeafParticleRegistry["default"][rand.nextInt(1024)]
|
||||
setColor(blockColor)
|
||||
}
|
||||
}
|
||||
|
||||
override val isValid: Boolean get() = (sprite != null)
|
||||
|
||||
override fun update() {
|
||||
if (rand.nextFloat() > 0.95f) rotPositive = !rotPositive
|
||||
if (age > maxAge - 20) particleAlpha = 0.05f * (maxAge - age)
|
||||
|
||||
if (onGround || wasCollided) {
|
||||
velocity.setTo(0.0, 0.0, 0.0)
|
||||
if (!wasCollided) {
|
||||
age = Math.max(age, maxAge - 20)
|
||||
wasCollided = true
|
||||
}
|
||||
} else {
|
||||
velocity.setTo(cos[particleRot], 0.0, sin[particleRot]).mul(Config.fallingLeaves.perturb)
|
||||
.add(LeafWindTracker.current).add(0.0, -1.0, 0.0).mul(Config.fallingLeaves.speed)
|
||||
particleRot = (particleRot + (if (rotPositive) 1 else -1)) and 63
|
||||
particleAngle = rotationFactor * particleRot.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
override fun render(worldRenderer: BufferBuilder, partialTickTime: Float) {
|
||||
if (Config.fallingLeaves.opacityHack) GL11.glDepthMask(true)
|
||||
renderParticleQuad(worldRenderer, partialTickTime, rotation = particleRot, isMirrored = isMirrored)
|
||||
}
|
||||
|
||||
fun calculateParticleColor(textureAvgColor: Int, blockColor: Int) {
|
||||
val texture = HSB.fromColor(textureAvgColor)
|
||||
val block = HSB.fromColor(blockColor)
|
||||
|
||||
val weightTex = texture.saturation / (texture.saturation + block.saturation)
|
||||
val weightBlock = 1.0f - weightTex
|
||||
|
||||
// avoid circular average for hue for performance reasons
|
||||
// one of the color components should dominate anyway
|
||||
val particle = HSB(
|
||||
weightTex * texture.hue + weightBlock * block.hue,
|
||||
weightTex * texture.saturation + weightBlock * block.saturation,
|
||||
weightTex * texture.brightness + weightBlock * block.brightness * biomeBrightnessMultiplier
|
||||
)
|
||||
setColor(particle.asColor)
|
||||
}
|
||||
}
|
||||
|
||||
object LeafWindTracker {
|
||||
var random = Random()
|
||||
val target = Double3.zero
|
||||
val current = Double3.zero
|
||||
var nextChange: Long = 0
|
||||
|
||||
init {
|
||||
MinecraftForge.EVENT_BUS.register(this)
|
||||
}
|
||||
|
||||
fun changeWind(world: World) {
|
||||
nextChange = world.worldInfo.gameTime + 120 + random.nextInt(80)
|
||||
val direction = PI2 * random.nextDouble()
|
||||
val speed = abs(random.nextGaussian()) * Config.fallingLeaves.windStrength +
|
||||
(if (!world.isRaining) 0.0 else abs(random.nextGaussian()) * Config.fallingLeaves.stormStrength)
|
||||
target.setTo(cos(direction) * speed, 0.0, sin(direction) * speed)
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
fun handleWorldTick(event: TickEvent.ClientTickEvent) {
|
||||
if (event.phase == TickEvent.Phase.START) Minecraft.getInstance().world?.let { world ->
|
||||
// change target wind speed
|
||||
if (world.worldInfo.dayTime >= nextChange) changeWind(world)
|
||||
|
||||
// change current wind speed
|
||||
val changeRate = if (world.isRaining) 0.015 else 0.005
|
||||
current.add(
|
||||
(target.x - current.x).minmax(-changeRate, changeRate),
|
||||
0.0,
|
||||
(target.z - current.z).minmax(-changeRate, changeRate)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
fun handleWorldLoad(event: WorldEvent.Load) { if (event.world.isRemote) changeWind(event.world.world) }
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.octarinecore.client.render.AbstractEntityFX
|
||||
import mods.octarinecore.client.resource.Atlas
|
||||
import mods.octarinecore.client.resource.ResourceHandler
|
||||
import mods.octarinecore.common.Double3
|
||||
import mods.octarinecore.forEachPairIndexed
|
||||
import net.minecraft.client.renderer.BufferBuilder
|
||||
import net.minecraft.util.ResourceLocation
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.util.math.MathHelper
|
||||
import net.minecraft.world.World
|
||||
import org.apache.logging.log4j.Level.DEBUG
|
||||
import java.util.*
|
||||
|
||||
class EntityRisingSoulFX(world: World, pos: BlockPos) :
|
||||
AbstractEntityFX(world, pos.x.toDouble() + 0.5, pos.y.toDouble() + 1.0, pos.z.toDouble() + 0.5) {
|
||||
|
||||
val particleTrail: Deque<Double3> = LinkedList<Double3>()
|
||||
val initialPhase = rand.nextInt(64)
|
||||
|
||||
init {
|
||||
motionY = 0.1
|
||||
particleGravity = 0.0f
|
||||
sprite = RisingSoulTextures.headIcons[rand.nextInt(256)]
|
||||
maxAge = MathHelper.floor((0.6 + 0.4 * rand.nextDouble()) * Config.risingSoul.lifetime * 20.0)
|
||||
}
|
||||
|
||||
override val isValid: Boolean get() = true
|
||||
|
||||
override fun update() {
|
||||
val phase = (initialPhase + age) % 64
|
||||
velocity.setTo(cos[phase] * Config.risingSoul.perturb, 0.1, sin[phase] * Config.risingSoul.perturb)
|
||||
|
||||
particleTrail.addFirst(currentPos.copy())
|
||||
while (particleTrail.size > Config.risingSoul.trailLength) particleTrail.removeLast()
|
||||
|
||||
if (!Config.enabled) setExpired()
|
||||
}
|
||||
|
||||
override fun render(worldRenderer: BufferBuilder, partialTickTime: Float) {
|
||||
var alpha = Config.risingSoul.opacity.toFloat()
|
||||
if (age > maxAge - 40) alpha *= (maxAge - age) / 40.0f
|
||||
|
||||
renderParticleQuad(worldRenderer, partialTickTime,
|
||||
size = Config.risingSoul.headSize * 0.25,
|
||||
alpha = alpha
|
||||
)
|
||||
|
||||
var scale = Config.risingSoul.trailSize * 0.25
|
||||
particleTrail.forEachPairIndexed { idx, current, previous ->
|
||||
scale *= Config.risingSoul.sizeDecay
|
||||
alpha *= Config.risingSoul.opacityDecay.toFloat()
|
||||
if (idx % Config.risingSoul.trailDensity == 0) renderParticleQuad(worldRenderer, partialTickTime,
|
||||
currentPos = current,
|
||||
prevPos = previous,
|
||||
size = scale,
|
||||
alpha = alpha,
|
||||
icon = RisingSoulTextures.trackIcon
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object RisingSoulTextures : ResourceHandler(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus, targetAtlas = Atlas.PARTICLES) {
|
||||
val headIcons = spriteSet { idx -> ResourceLocation(BetterFoliageMod.MOD_ID, "rising_soul_$idx") }
|
||||
val trackIcon by sprite(Identifier(BetterFoliageMod.MOD_ID, "soul_track"))
|
||||
}
|
||||
146
src/main/kotlin/mods/betterfoliage/client/render/ModelColumn.kt
Normal file
146
src/main/kotlin/mods/betterfoliage/client/render/ModelColumn.kt
Normal file
@@ -0,0 +1,146 @@
|
||||
@file:JvmName("ModelColumn")
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.octarinecore.client.render.*
|
||||
import mods.octarinecore.client.render.lighting.*
|
||||
import mods.octarinecore.common.Double3
|
||||
import mods.octarinecore.exchange
|
||||
import net.minecraft.util.Direction.*
|
||||
|
||||
/** Weight of the same-side AO values on the outer edges of the 45deg chamfered column faces. */
|
||||
const val chamferAffinity = 0.9f
|
||||
|
||||
/** Amount to shrink column extension bits to stop Z-fighting. */
|
||||
val zProtectionScale: Double3 get() = Double3(Config.roundLogs.zProtection, 1.0, Config.roundLogs.zProtection)
|
||||
|
||||
fun Model.columnSide(radius: Double, yBottom: Double, yTop: Double, transform: (Quad) -> Quad = { it }) {
|
||||
val halfRadius = radius * 0.5
|
||||
listOf(
|
||||
verticalRectangle(x1 = 0.0, z1 = 0.5, x2 = 0.5 - radius, z2 = 0.5, yBottom = yBottom, yTop = yTop)
|
||||
.clampUV(minU = 0.0, maxU = 0.5 - radius)
|
||||
.setAoShader(faceOrientedInterpolate(overrideFace = SOUTH))
|
||||
.setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y)), predicate = { v, vi -> vi == 1 || vi == 2}),
|
||||
|
||||
verticalRectangle(x1 = 0.5 - radius, z1 = 0.5, x2 = 0.5 - halfRadius, z2 = 0.5 - halfRadius, yBottom = yBottom, yTop = yTop)
|
||||
.clampUV(minU = 0.5 - radius)
|
||||
.setAoShader(
|
||||
faceOrientedAuto(overrideFace = SOUTH, corner = cornerInterpolate(Axis.Y, chamferAffinity, Config.roundLogs.dimming.toFloat()))
|
||||
)
|
||||
.setAoShader(
|
||||
faceOrientedAuto(overrideFace = SOUTH, corner = cornerInterpolate(Axis.Y, 0.5f, Config.roundLogs.dimming.toFloat())),
|
||||
predicate = { v, vi -> vi == 1 || vi == 2}
|
||||
)
|
||||
).forEach { transform(it.setFlatShader(FaceFlat(SOUTH))).add() }
|
||||
|
||||
listOf(
|
||||
verticalRectangle(x1 = 0.5 - halfRadius, z1 = 0.5 - halfRadius, x2 = 0.5, z2 = 0.5 - radius, yBottom = yBottom, yTop = yTop)
|
||||
.clampUV(maxU = radius - 0.5)
|
||||
.setAoShader(
|
||||
faceOrientedAuto(overrideFace = EAST, corner = cornerInterpolate(Axis.Y, chamferAffinity, Config.roundLogs.dimming.toFloat())))
|
||||
.setAoShader(
|
||||
faceOrientedAuto(overrideFace = EAST, corner = cornerInterpolate(Axis.Y, 0.5f, Config.roundLogs.dimming.toFloat())),
|
||||
predicate = { v, vi -> vi == 0 || vi == 3}
|
||||
),
|
||||
|
||||
verticalRectangle(x1 = 0.5, z1 = 0.5 - radius, x2 = 0.5, z2 = 0.0, yBottom = yBottom, yTop = yTop)
|
||||
.clampUV(minU = radius - 0.5, maxU = 0.0)
|
||||
.setAoShader(faceOrientedInterpolate(overrideFace = EAST))
|
||||
.setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y)), predicate = { v, vi -> vi == 0 || vi == 3})
|
||||
).forEach { transform(it.setFlatShader(FaceFlat(EAST))).add() }
|
||||
|
||||
quads.exchange(1, 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a model of the side of a square column quadrant.
|
||||
*
|
||||
* @param[transform] transformation to apply to the model
|
||||
*/
|
||||
fun Model.columnSideSquare(yBottom: Double, yTop: Double, transform: (Quad) -> Quad = { it }) {
|
||||
listOf(
|
||||
verticalRectangle(x1 = 0.0, z1 = 0.5, x2 = 0.5, z2 = 0.5, yBottom = yBottom, yTop = yTop)
|
||||
.clampUV(minU = 0.0)
|
||||
.setAoShader(faceOrientedInterpolate(overrideFace = SOUTH))
|
||||
.setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y)), predicate = { v, vi -> vi == 1 || vi == 2}),
|
||||
|
||||
verticalRectangle(x1 = 0.5, z1 = 0.5, x2 = 0.5, z2 = 0.0, yBottom = yBottom, yTop = yTop)
|
||||
.clampUV(maxU = 0.0)
|
||||
.setAoShader(faceOrientedInterpolate(overrideFace = EAST))
|
||||
.setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y)), predicate = { v, vi -> vi == 0 || vi == 3})
|
||||
).forEach {
|
||||
transform(it.setFlatShader(faceOrientedAuto(corner = cornerFlat))).add()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a model of the top lid of a chamfered column quadrant.
|
||||
*
|
||||
* @param[radius] the chamfer radius
|
||||
* @param[transform] transformation to apply to the model
|
||||
*/
|
||||
fun Model.columnLid(radius: Double, transform: (Quad)->Quad = { it }) {
|
||||
val v1 = Vertex(Double3(0.0, 0.5, 0.0), UV(0.0, 0.0))
|
||||
val v2 = Vertex(Double3(0.0, 0.5, 0.5), UV(0.0, 0.5))
|
||||
val v3 = Vertex(Double3(0.5 - radius, 0.5, 0.5), UV(0.5 - radius, 0.5))
|
||||
val v4 = Vertex(Double3(0.5 - radius * 0.5, 0.5, 0.5 - radius * 0.5), UV(0.5, 0.5))
|
||||
val v5 = Vertex(Double3(0.5, 0.5, 0.5 - radius), UV(0.5, 0.5 - radius))
|
||||
val v6 = Vertex(Double3(0.5, 0.5, 0.0), UV(0.5, 0.0))
|
||||
|
||||
val q1 = Quad(v1, v2, v3, v4).setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y)))
|
||||
.transformVI { vertex, idx -> vertex.copy(aoShader = when(idx) {
|
||||
0 -> FaceCenter(UP)
|
||||
1 -> EdgeInterpolateFallback(UP, SOUTH, 0.0)
|
||||
else -> vertex.aoShader
|
||||
})}
|
||||
.cycleVertices(if (Config.nVidia) 0 else 1)
|
||||
val q2 = Quad(v1, v4, v5, v6).setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y)))
|
||||
.transformVI { vertex, idx -> vertex.copy(aoShader = when(idx) {
|
||||
0 -> FaceCenter(UP)
|
||||
3 -> EdgeInterpolateFallback(UP, EAST, 0.0)
|
||||
else -> vertex.aoShader
|
||||
})}
|
||||
.cycleVertices(if (Config.nVidia) 0 else 1)
|
||||
listOf(q1, q2).forEach { transform(it.setFlatShader(FaceFlat(UP))).add() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a model of the top lid of a square column quadrant.
|
||||
*
|
||||
* @param[transform] transformation to apply to the model
|
||||
*/
|
||||
fun Model.columnLidSquare(transform: (Quad)-> Quad = { it }) {
|
||||
transform(
|
||||
horizontalRectangle(x1 = 0.0, x2 = 0.5, z1 = 0.0, z2 = 0.5, y = 0.5)
|
||||
.transformVI { vertex, idx -> vertex.copy(uv = UV(vertex.xyz.x, vertex.xyz.z), aoShader = when(idx) {
|
||||
0 -> FaceCenter(UP)
|
||||
1 -> EdgeInterpolateFallback(UP, SOUTH, 0.0)
|
||||
2 -> CornerSingleFallback(UP, SOUTH, EAST, UP)
|
||||
else -> EdgeInterpolateFallback(UP, EAST, 0.0)
|
||||
}) }
|
||||
.setFlatShader(FaceFlat(UP))
|
||||
).add()
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a chamfered side quadrant model of a column that extends from the top of the block.
|
||||
* (clamp UV coordinates, apply some scaling to avoid Z-fighting).
|
||||
*
|
||||
* @param[size] amount that the model extends from the top
|
||||
*/
|
||||
fun topExtension(size: Double) = { q: Quad ->
|
||||
q.clampUV(minV = 0.5 - size).transformVI { vertex, idx ->
|
||||
if (idx < 2) vertex else vertex.copy(xyz = vertex.xyz * zProtectionScale)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Transform a chamfered side quadrant model of a column that extends from the bottom of the block.
|
||||
* (clamp UV coordinates, apply some scaling to avoid Z-fighting).
|
||||
*
|
||||
* @param[size] amount that the model extends from the bottom
|
||||
*/
|
||||
fun bottomExtension(size: Double) = { q: Quad ->
|
||||
q.clampUV(maxV = -0.5 + size).transformVI { vertex, idx ->
|
||||
if (idx > 1) vertex else vertex.copy(xyz = vertex.xyz * zProtectionScale)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.integration.ShadersModIntegration
|
||||
import mods.octarinecore.client.render.CombinedContext
|
||||
import mods.octarinecore.client.render.RenderDecorator
|
||||
import net.minecraft.block.material.Material
|
||||
import net.minecraft.tags.BlockTags
|
||||
import net.minecraft.util.ResourceLocation
|
||||
import net.minecraft.world.biome.Biome
|
||||
import org.apache.logging.log4j.Level.DEBUG
|
||||
|
||||
class RenderAlgae : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) {
|
||||
|
||||
val noise = simplexNoise()
|
||||
|
||||
val algaeIcons = spriteSet { idx -> ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_algae_$idx") }
|
||||
val algaeModels = modelSet(64) { idx -> RenderGrass.grassTopQuads(Config.algae.heightMin, Config.algae.heightMax)(idx) }
|
||||
|
||||
override fun isEligible(ctx: CombinedContext) =
|
||||
Config.enabled && Config.algae.enabled &&
|
||||
ctx.state(up2).material == Material.WATER &&
|
||||
ctx.state(up1).material == Material.WATER &&
|
||||
BlockTags.DIRT_LIKE.contains(ctx.state.block) &&
|
||||
ctx.biome.category.let { it == Biome.Category.OCEAN || it == Biome.Category.BEACH || it == Biome.Category.RIVER } &&
|
||||
noise[ctx.pos] < Config.algae.population
|
||||
|
||||
override fun render(ctx: CombinedContext) {
|
||||
ctx.render()
|
||||
if (!ctx.isCutout) return
|
||||
val rand = ctx.semiRandomArray(3)
|
||||
ShadersModIntegration.grass(ctx, Config.algae.shaderWind) {
|
||||
ctx.render(
|
||||
algaeModels[rand[2]],
|
||||
icon = { _, qi, _ -> algaeIcons[rand[qi and 1]] }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/main/kotlin/mods/betterfoliage/client/render/RenderCactus.kt
Normal file
104
src/main/kotlin/mods/betterfoliage/client/render/RenderCactus.kt
Normal file
@@ -0,0 +1,104 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.render.column.ColumnTextureInfo
|
||||
import mods.betterfoliage.client.render.column.SimpleColumnInfo
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.octarinecore.client.render.*
|
||||
import mods.octarinecore.client.render.lighting.*
|
||||
import mods.octarinecore.client.resource.*
|
||||
import mods.octarinecore.common.Rotation
|
||||
import mods.octarinecore.common.config.ModelTextureList
|
||||
import mods.octarinecore.common.config.SimpleBlockMatcher
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.block.CactusBlock
|
||||
import net.minecraft.util.Direction.*
|
||||
import org.apache.logging.log4j.Level.DEBUG
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
object AsyncCactusDiscovery : ConfigurableModelDiscovery<ColumnTextureInfo>() {
|
||||
override val logger = BetterFoliage.logDetail
|
||||
override val matchClasses = SimpleBlockMatcher(CactusBlock::class.java)
|
||||
override val modelTextures = listOf(ModelTextureList("block/cactus", "top", "bottom", "side"))
|
||||
override fun processModel(state: BlockState, textures: List<String>, atlas: AtlasFuture): CompletableFuture<ColumnTextureInfo>? {
|
||||
val sprites = textures.map { atlas.sprite(Identifier(it)) }
|
||||
return atlas.mapAfter {
|
||||
SimpleColumnInfo(
|
||||
Axis.Y,
|
||||
sprites[0].get(),
|
||||
sprites[1].get(),
|
||||
sprites.drop(2).map { it.get() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun init() {
|
||||
BetterFoliage.blockSprites.providers.add(this)
|
||||
}
|
||||
}
|
||||
|
||||
class RenderCactus : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) {
|
||||
|
||||
val cactusStemRadius = 0.4375
|
||||
val cactusArmRotation = listOf(NORTH, SOUTH, EAST, WEST).map { Rotation.rot90[it.ordinal] }
|
||||
|
||||
val iconCross by sprite(Identifier(BetterFoliageMod.MOD_ID, "blocks/better_cactus"))
|
||||
val iconArm = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_cactus_arm_$idx") }
|
||||
|
||||
val modelStem = model {
|
||||
horizontalRectangle(x1 = -cactusStemRadius, x2 = cactusStemRadius, z1 = -cactusStemRadius, z2 = cactusStemRadius, y = 0.5)
|
||||
.scaleUV(cactusStemRadius * 2.0)
|
||||
.let { listOf(it.flipped.move(1.0 to DOWN), it) }
|
||||
.forEach { it.setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y), edge = null)).add() }
|
||||
|
||||
verticalRectangle(x1 = -0.5, z1 = cactusStemRadius, x2 = 0.5, z2 = cactusStemRadius, yBottom = -0.5, yTop = 0.5)
|
||||
.setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y), edge = null))
|
||||
.toCross(UP).addAll()
|
||||
}
|
||||
val modelCross = modelSet(64) { modelIdx ->
|
||||
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -0.5 * 1.41, yTop = 0.5 * 1.41)
|
||||
.setAoShader(edgeOrientedAuto(corner = cornerAoMaxGreen))
|
||||
.scale(1.4)
|
||||
.transformV { v ->
|
||||
val perturb = xzDisk(modelIdx) * Config.cactus.sizeVariation
|
||||
Vertex(v.xyz + (if (v.uv.u < 0.0) perturb else -perturb), v.uv, v.aoShader)
|
||||
}
|
||||
.toCross(UP).addAll()
|
||||
}
|
||||
val modelArm = modelSet(64) { modelIdx ->
|
||||
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.0, yTop = 1.0)
|
||||
.scale(Config.cactus.size).move(0.5 to UP)
|
||||
|
||||
.setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y), edge = null))
|
||||
.toCross(UP) { it.move(xzDisk(modelIdx) * Config.cactus.hOffset) }.addAll()
|
||||
}
|
||||
|
||||
override fun isEligible(ctx: CombinedContext): Boolean =
|
||||
Config.enabled && Config.cactus.enabled &&
|
||||
AsyncCactusDiscovery[ctx] != null
|
||||
|
||||
override val onlyOnCutout get() = true
|
||||
|
||||
override fun render(ctx: CombinedContext) {
|
||||
val icons = AsyncCactusDiscovery[ctx]!!
|
||||
|
||||
ctx.render(
|
||||
modelStem.model,
|
||||
icon = { ctx, qi, q -> when(qi) {
|
||||
0 -> icons.bottom(ctx, qi, q); 1 -> icons.top(ctx, qi, q); else -> icons.side(ctx, qi, q)
|
||||
} }
|
||||
)
|
||||
ctx.render(
|
||||
modelCross[ctx.semiRandom(0)],
|
||||
icon = { _, _, _ -> iconCross }
|
||||
)
|
||||
|
||||
ctx.render(
|
||||
modelArm[ctx.semiRandom(1)],
|
||||
cactusArmRotation[ctx.semiRandom(2) % 4],
|
||||
icon = { _, _, _ -> iconArm[ctx.semiRandom(3)] }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.texture.GrassRegistry
|
||||
import mods.octarinecore.client.render.CombinedContext
|
||||
import mods.octarinecore.client.render.RenderDecorator
|
||||
import mods.octarinecore.common.Int3
|
||||
import mods.octarinecore.common.horizontalDirections
|
||||
import mods.octarinecore.common.offset
|
||||
import net.minecraft.tags.BlockTags
|
||||
|
||||
class RenderConnectedGrass : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) {
|
||||
override fun isEligible(ctx: CombinedContext) =
|
||||
Config.enabled && Config.connectedGrass.enabled &&
|
||||
BlockTags.DIRT_LIKE.contains(ctx.state.block) &&
|
||||
GrassRegistry[ctx, up1] != null &&
|
||||
(Config.connectedGrass.snowEnabled || !ctx.state(up2).isSnow)
|
||||
|
||||
override fun render(ctx: CombinedContext) {
|
||||
// if the block sides are not visible anyway, render normally
|
||||
if (horizontalDirections.none { ctx.shouldSideBeRendered(it) }) {
|
||||
ctx.render()
|
||||
} else {
|
||||
ctx.exchange(Int3.zero, up1).exchange(up1, up2).render()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RenderConnectedGrassLog : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) {
|
||||
|
||||
override fun isEligible(ctx: CombinedContext) =
|
||||
Config.enabled && Config.roundLogs.enabled && Config.roundLogs.connectGrass &&
|
||||
BlockTags.DIRT_LIKE.contains(ctx.state.block) &&
|
||||
LogRegistry[ctx, up1] != null
|
||||
|
||||
override fun render(ctx: CombinedContext) {
|
||||
val grassDir = horizontalDirections.find { GrassRegistry[ctx, it.offset] != null }
|
||||
if (grassDir == null) {
|
||||
ctx.render()
|
||||
} else {
|
||||
ctx.exchange(Int3.zero, grassDir.offset).render()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.client.config.BlockConfig
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.octarinecore.client.render.*
|
||||
import mods.octarinecore.client.render.lighting.*
|
||||
import mods.octarinecore.common.allDirections
|
||||
import mods.octarinecore.random
|
||||
import net.minecraft.block.material.Material
|
||||
import net.minecraft.tags.BlockTags
|
||||
import net.minecraft.util.Direction.Axis
|
||||
import net.minecraft.util.Direction.UP
|
||||
import net.minecraft.util.ResourceLocation
|
||||
import net.minecraft.world.biome.Biome
|
||||
import org.apache.logging.log4j.Level.DEBUG
|
||||
|
||||
class RenderCoral : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) {
|
||||
|
||||
val noise = simplexNoise()
|
||||
|
||||
val coralIcons = spriteSet { idx -> ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_coral_$idx") }
|
||||
val crustIcons = spriteSet { idx -> ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_crust_$idx") }
|
||||
val coralModels = modelSet(64) { modelIdx ->
|
||||
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.0, yTop = 1.0)
|
||||
.scale(Config.coral.size).move(0.5 to UP)
|
||||
.toCross(UP) { it.move(xzDisk(modelIdx) * Config.coral.hOffset) }.addAll()
|
||||
|
||||
val separation = random(0.01, Config.coral.vOffset)
|
||||
horizontalRectangle(x1 = -0.5, x2 = 0.5, z1 = -0.5, z2 = 0.5, y = 0.0)
|
||||
.scale(Config.coral.crustSize).move(0.5 + separation to UP).add()
|
||||
|
||||
transformQ {
|
||||
it.setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y)))
|
||||
.setFlatShader(faceOrientedAuto(overrideFace = UP, corner = cornerFlat))
|
||||
}
|
||||
}
|
||||
|
||||
override fun isEligible(ctx: CombinedContext) =
|
||||
Config.enabled && Config.coral.enabled &&
|
||||
(ctx.state(up2).material == Material.WATER || Config.coral.shallowWater) &&
|
||||
ctx.state(up1).material == Material.WATER &&
|
||||
BlockTags.SAND.contains(ctx.state.block) &&
|
||||
ctx.biome.category.let { it == Biome.Category.OCEAN || it == Biome.Category.BEACH } &&
|
||||
noise[ctx.pos] < Config.coral.population
|
||||
|
||||
override fun render(ctx: CombinedContext) {
|
||||
val baseRender = ctx.render()
|
||||
if (!ctx.isCutout) return
|
||||
|
||||
allDirections.forEachIndexed { idx, face ->
|
||||
if (ctx.state(face).material == Material.WATER && ctx.semiRandom(idx) < Config.coral.chance) {
|
||||
var variation = ctx.semiRandom(6)
|
||||
ctx.render(
|
||||
coralModels[variation++],
|
||||
rotationFromUp[idx],
|
||||
icon = { _, qi, _ -> if (qi == 4) crustIcons[variation] else coralIcons[variation + (qi and 1)] }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
src/main/kotlin/mods/betterfoliage/client/render/RenderGrass.kt
Normal file
102
src/main/kotlin/mods/betterfoliage/client/render/RenderGrass.kt
Normal file
@@ -0,0 +1,102 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.integration.OptifineCustomColors
|
||||
import mods.betterfoliage.client.integration.ShadersModIntegration
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.betterfoliage.client.texture.GeneratedGrass
|
||||
import mods.betterfoliage.client.texture.GrassRegistry
|
||||
import mods.octarinecore.client.render.CombinedContext
|
||||
import mods.octarinecore.client.render.Model
|
||||
import mods.octarinecore.client.render.RenderDecorator
|
||||
import mods.octarinecore.client.render.fullCube
|
||||
import mods.octarinecore.client.render.lighting.cornerAo
|
||||
import mods.octarinecore.client.render.lighting.cornerFlat
|
||||
import mods.octarinecore.client.render.lighting.faceOrientedAuto
|
||||
import mods.octarinecore.common.Double3
|
||||
import mods.octarinecore.common.allDirections
|
||||
import mods.octarinecore.random
|
||||
import net.minecraft.tags.BlockTags
|
||||
import net.minecraft.util.Direction.*
|
||||
import org.apache.logging.log4j.Level.DEBUG
|
||||
|
||||
class RenderGrass : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun grassTopQuads(heightMin: Double, heightMax: Double): Model.(Int)->Unit = { modelIdx ->
|
||||
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.5,
|
||||
yTop = 0.5 + random(heightMin, heightMax)
|
||||
)
|
||||
.setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y)))
|
||||
.setFlatShader(faceOrientedAuto(overrideFace = UP, corner = cornerFlat))
|
||||
.toCross(UP) { it.move(xzDisk(modelIdx) * Config.shortGrass.hOffset) }.addAll()
|
||||
}
|
||||
}
|
||||
|
||||
val noise = simplexNoise()
|
||||
|
||||
val normalIcons = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_grass_long_$idx") }
|
||||
val snowedIcons = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_grass_snowed_$idx") }
|
||||
val normalGenIcon by sprite { GeneratedGrass(sprite = "minecraft:blocks/tall_grass_top", isSnowed = false).register(BetterFoliage.asyncPack) }
|
||||
val snowedGenIcon by sprite { GeneratedGrass(sprite = "minecraft:blocks/tall_grass_top", isSnowed = true).register(BetterFoliage.asyncPack) }
|
||||
|
||||
val grassModels = modelSet(64) { idx -> grassTopQuads(Config.shortGrass.heightMin, Config.shortGrass.heightMax)(idx) }
|
||||
|
||||
override fun isEligible(ctx: CombinedContext) =
|
||||
Config.enabled &&
|
||||
(Config.shortGrass.grassEnabled || Config.connectedGrass.enabled) &&
|
||||
GrassRegistry[ctx] != null
|
||||
|
||||
override val onlyOnCutout get() = true
|
||||
|
||||
override fun render(ctx: CombinedContext) {
|
||||
val isConnected = BlockTags.DIRT_LIKE.contains(ctx.state(DOWN).block) || GrassRegistry[ctx, down1] != null
|
||||
val isSnowed = ctx.state(UP).isSnow
|
||||
val connectedGrass = isConnected && Config.connectedGrass.enabled && (!isSnowed || Config.connectedGrass.snowEnabled)
|
||||
|
||||
val grass = GrassRegistry[ctx]!!
|
||||
val blockColor = OptifineCustomColors.getBlockColor(ctx)
|
||||
|
||||
if (connectedGrass) {
|
||||
// check occlusion
|
||||
val isVisible = allDirections.map { ctx.shouldSideBeRendered(it) }
|
||||
|
||||
// render full grass block
|
||||
ctx.render(
|
||||
fullCube,
|
||||
quadFilter = { qi, _ -> isVisible[qi] },
|
||||
icon = { _, _, _ -> grass.grassTopTexture },
|
||||
postProcess = { ctx, _, _, _, _ ->
|
||||
rotateUV(2)
|
||||
if (isSnowed) {
|
||||
if (!ctx.aoEnabled) setGrey(1.4f)
|
||||
} else if (ctx.aoEnabled && grass.overrideColor == null) multiplyColor(blockColor)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
ctx.render()
|
||||
}
|
||||
|
||||
if (!Config.shortGrass.grassEnabled) return
|
||||
if (isSnowed && !Config.shortGrass.snowEnabled) return
|
||||
if (ctx.offset(UP).isNormalCube) return
|
||||
if (Config.shortGrass.population < 64 && noise[ctx.pos] >= Config.shortGrass.population) return
|
||||
|
||||
// render grass quads
|
||||
val iconset = if (isSnowed) snowedIcons else normalIcons
|
||||
val iconGen = if (isSnowed) snowedGenIcon else normalGenIcon
|
||||
val rand = ctx.semiRandomArray(2)
|
||||
|
||||
ShadersModIntegration.grass(ctx, Config.shortGrass.shaderWind) {
|
||||
ctx.render(
|
||||
grassModels[rand[0]],
|
||||
translation = ctx.blockCenter + (if (isSnowed) snowOffset else Double3.zero),
|
||||
icon = { _, qi, _ -> if (Config.shortGrass.useGenerated) iconGen else iconset[rand[qi and 1]] },
|
||||
postProcess = { _, _, _, _, _ -> if (isSnowed) setGrey(1.0f) else multiplyColor(grass.overrideColor ?: blockColor) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.integration.OptifineCustomColors
|
||||
import mods.betterfoliage.client.integration.ShadersModIntegration
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.betterfoliage.client.texture.LeafRegistry
|
||||
import mods.octarinecore.PI2
|
||||
import mods.octarinecore.client.render.CombinedContext
|
||||
import mods.octarinecore.client.render.RenderDecorator
|
||||
import mods.octarinecore.client.render.lighting.FlatOffset
|
||||
import mods.octarinecore.client.render.lighting.cornerAoMaxGreen
|
||||
import mods.octarinecore.client.render.lighting.edgeOrientedAuto
|
||||
import mods.octarinecore.common.Double3
|
||||
import mods.octarinecore.common.Int3
|
||||
import mods.octarinecore.common.allDirections
|
||||
import mods.octarinecore.common.vec
|
||||
import mods.octarinecore.random
|
||||
import net.minecraft.util.Direction.UP
|
||||
import java.lang.Math.cos
|
||||
import java.lang.Math.sin
|
||||
|
||||
class RenderLeaves : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) {
|
||||
|
||||
val leavesModel = model {
|
||||
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -0.5 * 1.41, yTop = 0.5 * 1.41)
|
||||
.setAoShader(edgeOrientedAuto(corner = cornerAoMaxGreen))
|
||||
.setFlatShader(FlatOffset(Int3.zero))
|
||||
.scale(Config.leaves.size)
|
||||
.toCross(UP).addAll()
|
||||
}
|
||||
val snowedIcon = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_leaves_snowed_$idx") }
|
||||
|
||||
val perturbs = vectorSet(64) { idx ->
|
||||
val angle = PI2 * idx / 64.0
|
||||
Double3(cos(angle), 0.0, sin(angle)) * Config.leaves.hOffset +
|
||||
UP.vec * random(-1.0, 1.0) * Config.leaves.vOffset
|
||||
}
|
||||
|
||||
override fun isEligible(ctx: CombinedContext) =
|
||||
Config.enabled &&
|
||||
Config.leaves.enabled &&
|
||||
LeafRegistry[ctx] != null &&
|
||||
!(Config.leaves.hideInternal && allDirections.all { ctx.offset(it).isNormalCube } )
|
||||
|
||||
override val onlyOnCutout get() = true
|
||||
|
||||
override fun render(ctx: CombinedContext) {
|
||||
val isSnowed = ctx.state(UP).isSnow
|
||||
val leafInfo = LeafRegistry[ctx]!!
|
||||
val blockColor = OptifineCustomColors.getBlockColor(ctx)
|
||||
|
||||
ctx.render(force = true)
|
||||
|
||||
ShadersModIntegration.leaves(ctx) {
|
||||
val rand = ctx.semiRandomArray(2)
|
||||
(if (Config.leaves.dense) denseLeavesRot else normalLeavesRot).forEach { rotation ->
|
||||
ctx.render(
|
||||
leavesModel.model,
|
||||
rotation,
|
||||
translation = ctx.blockCenter + perturbs[rand[0]],
|
||||
icon = { _, _, _ -> leafInfo.roundLeafTexture },
|
||||
postProcess = { _, _, _, _, _ ->
|
||||
rotateUV(rand[1])
|
||||
multiplyColor(blockColor)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isSnowed && Config.leaves.snowEnabled) ctx.render(
|
||||
leavesModel.model,
|
||||
translation = ctx.blockCenter + perturbs[rand[0]],
|
||||
icon = { _, _, _ -> snowedIcon[rand[1]] },
|
||||
postProcess = whitewash
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.client.config.BlockConfig
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.integration.ShadersModIntegration
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.octarinecore.client.render.CombinedContext
|
||||
import mods.octarinecore.client.render.RenderDecorator
|
||||
import mods.octarinecore.client.render.lighting.FlatOffsetNoColor
|
||||
import mods.octarinecore.common.Int3
|
||||
import net.minecraft.util.Direction.DOWN
|
||||
import net.minecraft.util.Direction.UP
|
||||
import org.apache.logging.log4j.Level.DEBUG
|
||||
|
||||
class RenderLilypad : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) {
|
||||
|
||||
val rootModel = model {
|
||||
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -1.5, yTop = -0.5)
|
||||
.setFlatShader(FlatOffsetNoColor(Int3.zero))
|
||||
.toCross(UP).addAll()
|
||||
}
|
||||
val flowerModel = model {
|
||||
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.0, yTop = 1.0)
|
||||
.scale(0.5).move(0.5 to DOWN)
|
||||
.setFlatShader(FlatOffsetNoColor(Int3.zero))
|
||||
.toCross(UP).addAll()
|
||||
}
|
||||
val rootIcon = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_lilypad_roots_$idx") }
|
||||
val flowerIcon = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_lilypad_flower_$idx") }
|
||||
val perturbs = vectorSet(64) { modelIdx -> xzDisk(modelIdx) * Config.lilypad.hOffset }
|
||||
|
||||
override fun isEligible(ctx: CombinedContext): Boolean =
|
||||
Config.enabled && Config.lilypad.enabled &&
|
||||
BlockConfig.lilypad.matchesClass(ctx.state.block)
|
||||
|
||||
override fun render(ctx: CombinedContext) {
|
||||
ctx.render()
|
||||
|
||||
val rand = ctx.semiRandomArray(5)
|
||||
ShadersModIntegration.grass(ctx) {
|
||||
ctx.render(
|
||||
rootModel.model,
|
||||
translation = ctx.blockCenter.add(perturbs[rand[2]]),
|
||||
forceFlat = true,
|
||||
icon = { ctx, qi, q -> rootIcon[rand[qi and 1]] }
|
||||
)
|
||||
}
|
||||
|
||||
if (rand[3] < Config.lilypad.flowerChance) ctx.render(
|
||||
flowerModel.model,
|
||||
translation = ctx.blockCenter.add(perturbs[rand[4]]),
|
||||
forceFlat = true,
|
||||
icon = { _, _, _ -> flowerIcon[rand[0]] }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.chunk.ChunkOverlayManager
|
||||
import mods.betterfoliage.client.config.BlockConfig
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.render.column.AbstractRenderColumn
|
||||
import mods.betterfoliage.client.render.column.ColumnRenderLayer
|
||||
import mods.betterfoliage.client.render.column.ColumnTextureInfo
|
||||
import mods.betterfoliage.client.render.column.SimpleColumnInfo
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.octarinecore.client.render.CombinedContext
|
||||
import mods.octarinecore.client.resource.*
|
||||
import mods.octarinecore.common.config.ConfigurableBlockMatcher
|
||||
import mods.octarinecore.common.config.ModelTextureList
|
||||
import mods.octarinecore.tryDefault
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.block.LogBlock
|
||||
import net.minecraft.util.Direction.Axis
|
||||
import org.apache.logging.log4j.Level
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class RenderLog : AbstractRenderColumn(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) {
|
||||
|
||||
override val renderOnCutout: Boolean get() = false
|
||||
|
||||
override fun isEligible(ctx: CombinedContext) =
|
||||
Config.enabled && Config.roundLogs.enabled &&
|
||||
LogRegistry[ctx] != null
|
||||
|
||||
override val overlayLayer = RoundLogOverlayLayer()
|
||||
override val connectPerpendicular: Boolean get() = Config.roundLogs.connectPerpendicular
|
||||
override val radiusSmall: Double get() = Config.roundLogs.radiusSmall
|
||||
override val radiusLarge: Double get() = Config.roundLogs.radiusLarge
|
||||
init {
|
||||
ChunkOverlayManager.layers.add(overlayLayer)
|
||||
}
|
||||
}
|
||||
|
||||
class RoundLogOverlayLayer : ColumnRenderLayer() {
|
||||
override val registry: ModelRenderRegistry<ColumnTextureInfo> get() = LogRegistry
|
||||
override val blockPredicate = { state: BlockState -> BlockConfig.logBlocks.matchesClass(state.block) }
|
||||
|
||||
override val connectSolids: Boolean get() = Config.roundLogs.connectSolids
|
||||
override val lenientConnect: Boolean get() = Config.roundLogs.lenientConnect
|
||||
override val defaultToY: Boolean get() = Config.roundLogs.defaultY
|
||||
}
|
||||
|
||||
object LogRegistry : ModelRenderRegistryRoot<ColumnTextureInfo>()
|
||||
|
||||
object AsyncLogDiscovery : ConfigurableModelDiscovery<ColumnTextureInfo>() {
|
||||
override val logger = BetterFoliage.logDetail
|
||||
override val matchClasses: ConfigurableBlockMatcher get() = BlockConfig.logBlocks
|
||||
override val modelTextures: List<ModelTextureList> get() = BlockConfig.logModels.modelList
|
||||
|
||||
override fun processModel(state: BlockState, textures: List<String>, atlas: AtlasFuture): CompletableFuture<ColumnTextureInfo> {
|
||||
val axis = getAxis(state)
|
||||
logger.log(Level.DEBUG, "$logName: axis $axis")
|
||||
val spriteList = textures.map { atlas.sprite(Identifier(it)) }
|
||||
return atlas.mapAfter {
|
||||
SimpleColumnInfo(
|
||||
axis,
|
||||
spriteList[0].get(),
|
||||
spriteList[1].get(),
|
||||
spriteList.drop(2).map { it.get() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAxis(state: BlockState): Axis? {
|
||||
val axis = tryDefault(null) { state.get(LogBlock.AXIS).toString() } ?:
|
||||
state.values.entries.find { it.key.getName().toLowerCase() == "axis" }?.value?.toString()
|
||||
return when (axis) {
|
||||
"x" -> Axis.X
|
||||
"y" -> Axis.Y
|
||||
"z" -> Axis.Z
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun init() {
|
||||
LogRegistry.registries.add(this)
|
||||
BetterFoliage.blockSprites.providers.add(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.client.config.BlockConfig
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.octarinecore.client.render.CombinedContext
|
||||
import mods.octarinecore.client.render.RenderDecorator
|
||||
import mods.octarinecore.client.render.noPost
|
||||
import mods.octarinecore.common.Double3
|
||||
import net.minecraft.util.Direction.UP
|
||||
import org.apache.logging.log4j.Level.DEBUG
|
||||
|
||||
class RenderMycelium : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) {
|
||||
|
||||
val myceliumIcon = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_mycel_$idx") }
|
||||
val myceliumModel = modelSet(64) { idx -> RenderGrass.grassTopQuads(Config.shortGrass.heightMin, Config.shortGrass.heightMax)(idx) }
|
||||
|
||||
override fun isEligible(ctx: CombinedContext): Boolean {
|
||||
if (!Config.enabled || !Config.shortGrass.myceliumEnabled) return false
|
||||
return BlockConfig.mycelium.matchesClass(ctx.state.block)
|
||||
}
|
||||
|
||||
override fun render(ctx: CombinedContext) {
|
||||
ctx.render()
|
||||
if (!ctx.isCutout) return
|
||||
|
||||
val isSnowed = ctx.state(UP).isSnow
|
||||
if (isSnowed && !Config.shortGrass.snowEnabled) return
|
||||
if (ctx.offset(UP).isNormalCube) return
|
||||
val rand = ctx.semiRandomArray(2)
|
||||
|
||||
ctx.render(
|
||||
myceliumModel[rand[0]],
|
||||
translation = ctx.blockCenter + (if (isSnowed) snowOffset else Double3.zero),
|
||||
icon = { _, qi, _ -> myceliumIcon[rand[qi and 1]] },
|
||||
postProcess = if (isSnowed) whitewash else noPost
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.client.config.BlockConfig
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.octarinecore.client.render.*
|
||||
import mods.octarinecore.client.render.lighting.*
|
||||
import mods.octarinecore.random
|
||||
import net.minecraft.block.Blocks
|
||||
import net.minecraft.util.Direction.Axis
|
||||
import net.minecraft.util.Direction.*
|
||||
import org.apache.logging.log4j.Level.DEBUG
|
||||
|
||||
class RenderNetherrack : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) {
|
||||
|
||||
val netherrackIcon = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_netherrack_$idx") }
|
||||
val netherrackModel = modelSet(64) { modelIdx ->
|
||||
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yTop = -0.5,
|
||||
yBottom = -0.5 - random(Config.netherrack.heightMin, Config.netherrack.heightMax))
|
||||
.setAoShader(faceOrientedAuto(overrideFace = DOWN, corner = cornerAo(Axis.Y)))
|
||||
.setFlatShader(faceOrientedAuto(overrideFace = DOWN, corner = cornerFlat))
|
||||
.toCross(UP) { it.move(xzDisk(modelIdx) * Config.shortGrass.hOffset) }.addAll()
|
||||
|
||||
}
|
||||
|
||||
override fun isEligible(ctx: CombinedContext) =
|
||||
Config.enabled && Config.netherrack.enabled && ctx.state.block == Blocks.NETHERRACK
|
||||
|
||||
override fun render(ctx: CombinedContext) {
|
||||
ctx.render()
|
||||
if (!ctx.isCutout) return
|
||||
if (ctx.offset(DOWN).isNormalCube) return
|
||||
|
||||
val rand = ctx.semiRandomArray(2)
|
||||
ctx.render(
|
||||
netherrackModel[rand[0]],
|
||||
icon = { _, qi, _ -> netherrackIcon[rand[qi and 1]] }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.integration.ShadersModIntegration
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.octarinecore.client.render.CombinedContext
|
||||
import mods.octarinecore.client.render.RenderDecorator
|
||||
import mods.octarinecore.client.render.lighting.FlatOffsetNoColor
|
||||
import mods.octarinecore.client.resource.CenteredSprite
|
||||
import mods.octarinecore.random
|
||||
import net.minecraft.block.material.Material
|
||||
import net.minecraft.tags.BlockTags
|
||||
import net.minecraft.util.Direction.UP
|
||||
import org.apache.logging.log4j.Level.DEBUG
|
||||
|
||||
class RenderReeds : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) {
|
||||
|
||||
val noise = simplexNoise()
|
||||
val reedIcons = spriteSetTransformed(
|
||||
check = { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_reed_$idx")},
|
||||
register = { CenteredSprite(it).register(BetterFoliage.asyncPack) }
|
||||
)
|
||||
val reedModels = modelSet(64) { modelIdx ->
|
||||
val height = random(Config.reed.heightMin, Config.reed.heightMax)
|
||||
val waterline = 0.875f
|
||||
val vCutLine = 0.5 - waterline / height
|
||||
listOf(
|
||||
// below waterline
|
||||
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.5, yTop = 0.5 + waterline)
|
||||
.setFlatShader(FlatOffsetNoColor(up1)).clampUV(minV = vCutLine),
|
||||
|
||||
// above waterline
|
||||
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.5 + waterline, yTop = 0.5 + height)
|
||||
.setFlatShader(FlatOffsetNoColor(up2)).clampUV(maxV = vCutLine)
|
||||
).forEach {
|
||||
it.clampUV(minU = -0.25, maxU = 0.25)
|
||||
.toCross(UP) { it.move(xzDisk(modelIdx) * Config.reed.hOffset) }.addAll()
|
||||
}
|
||||
}
|
||||
|
||||
override fun isEligible(ctx: CombinedContext) =
|
||||
Config.enabled && Config.reed.enabled &&
|
||||
ctx.state(up2).material == Material.AIR &&
|
||||
ctx.state(UP).material == Material.WATER &&
|
||||
BlockTags.DIRT_LIKE.contains(ctx.state.block) &&
|
||||
ctx.biome.let { it.downfall > Config.reed.minBiomeRainfall && it.defaultTemperature >= Config.reed.minBiomeTemp } &&
|
||||
noise[ctx.pos] < Config.reed.population
|
||||
|
||||
override val onlyOnCutout get() = false
|
||||
|
||||
override fun render(ctx: CombinedContext) {
|
||||
ctx.render()
|
||||
if (!ctx.isCutout) return
|
||||
|
||||
val iconVar = ctx.semiRandom(1)
|
||||
ShadersModIntegration.grass(ctx, Config.reed.shaderWind) {
|
||||
ctx.render(
|
||||
reedModels[ctx.semiRandom(0)],
|
||||
forceFlat = true,
|
||||
icon = { _, _, _ -> reedIcons[iconVar] }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/main/kotlin/mods/betterfoliage/client/render/Utils.kt
Normal file
65
src/main/kotlin/mods/betterfoliage/client/render/Utils.kt
Normal file
@@ -0,0 +1,65 @@
|
||||
@file:JvmName("Utils")
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.octarinecore.PI2
|
||||
import mods.octarinecore.client.render.Model
|
||||
import mods.octarinecore.client.render.lighting.PostProcessLambda
|
||||
import mods.octarinecore.client.render.Quad
|
||||
import mods.octarinecore.common.Double3
|
||||
import mods.octarinecore.common.Int3
|
||||
import mods.octarinecore.common.Rotation
|
||||
import mods.octarinecore.common.times
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.block.material.Material
|
||||
import net.minecraft.util.BlockRenderLayer
|
||||
import net.minecraft.util.Direction
|
||||
import net.minecraft.util.Direction.*
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
val up1 = Int3(1 to UP)
|
||||
val up2 = Int3(2 to UP)
|
||||
val down1 = Int3(1 to DOWN)
|
||||
val snowOffset = UP * 0.0625
|
||||
|
||||
val normalLeavesRot = arrayOf(Rotation.identity)
|
||||
val denseLeavesRot = arrayOf(Rotation.identity, Rotation.rot90[EAST.ordinal], Rotation.rot90[SOUTH.ordinal])
|
||||
|
||||
val whitewash: PostProcessLambda = { _, _, _, _, _ -> setGrey(1.4f) }
|
||||
val greywash: PostProcessLambda = { _, _, _, _, _ -> setGrey(1.0f) }
|
||||
|
||||
val BlockState.isSnow: Boolean get() = material.let { it == Material.SNOW }
|
||||
|
||||
fun Quad.toCross(rotAxis: Direction, trans: (Quad)->Quad) =
|
||||
(0..3).map { rotIdx ->
|
||||
trans(rotate(Rotation.rot90[rotAxis.ordinal] * rotIdx).mirrorUV(rotIdx > 1, false))
|
||||
}
|
||||
fun Quad.toCross(rotAxis: Direction) = toCross(rotAxis) { it }
|
||||
|
||||
fun xzDisk(modelIdx: Int) = (PI2 * modelIdx / 64.0).let { Double3(cos(it), 0.0, sin(it)) }
|
||||
|
||||
val rotationFromUp = arrayOf(
|
||||
Rotation.rot90[EAST.ordinal] * 2,
|
||||
Rotation.identity,
|
||||
Rotation.rot90[WEST.ordinal],
|
||||
Rotation.rot90[EAST.ordinal],
|
||||
Rotation.rot90[SOUTH.ordinal],
|
||||
Rotation.rot90[NORTH.ordinal]
|
||||
)
|
||||
|
||||
fun Model.mix(first: Model, second: Model, predicate: (Int)->Boolean) {
|
||||
first.quads.forEachIndexed { qi, quad ->
|
||||
val otherQuad = second.quads[qi]
|
||||
Quad(
|
||||
if (predicate(0)) otherQuad.v1.copy() else quad.v1.copy(),
|
||||
if (predicate(1)) otherQuad.v2.copy() else quad.v2.copy(),
|
||||
if (predicate(2)) otherQuad.v3.copy() else quad.v3.copy(),
|
||||
if (predicate(3)) otherQuad.v4.copy() else quad.v4.copy()
|
||||
).add()
|
||||
}
|
||||
}
|
||||
|
||||
val BlockRenderLayer.isCutout: Boolean get() = (this == BlockRenderLayer.CUTOUT) || (this == BlockRenderLayer.CUTOUT_MIPPED)
|
||||
|
||||
fun BlockState.canRenderInLayer(layer: BlockRenderLayer) = this.block.canRenderInLayer(this, layer)
|
||||
fun BlockState.canRenderInCutout() = this.block.canRenderInLayer(this, BlockRenderLayer.CUTOUT) || this.block.canRenderInLayer(this, BlockRenderLayer.CUTOUT_MIPPED)
|
||||
@@ -0,0 +1,210 @@
|
||||
package mods.betterfoliage.client.render.column
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.client.chunk.ChunkOverlayManager
|
||||
import mods.betterfoliage.client.integration.ShadersModIntegration.renderAs
|
||||
import mods.betterfoliage.client.render.*
|
||||
import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.BlockType.*
|
||||
import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.QuadrantType
|
||||
import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.QuadrantType.*
|
||||
import mods.octarinecore.client.render.CombinedContext
|
||||
import mods.octarinecore.client.render.Model
|
||||
import mods.octarinecore.client.render.RenderDecorator
|
||||
import mods.octarinecore.client.render.noPost
|
||||
import mods.octarinecore.common.Rotation
|
||||
import mods.octarinecore.common.face
|
||||
import mods.octarinecore.common.rot
|
||||
import net.minecraft.block.BlockRenderType.MODEL
|
||||
import net.minecraft.util.Direction.*
|
||||
import net.minecraftforge.eventbus.api.IEventBus
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
abstract class AbstractRenderColumn(modId: String, modBus: IEventBus) : RenderDecorator(modId, modBus) {
|
||||
|
||||
/** The rotations necessary to bring the models in position for the 4 quadrants */
|
||||
val quadrantRotations = Array(4) { Rotation.rot90[UP.ordinal] * it }
|
||||
|
||||
// ============================
|
||||
// Configuration
|
||||
// ============================
|
||||
abstract val overlayLayer: ColumnRenderLayer
|
||||
abstract val connectPerpendicular: Boolean
|
||||
abstract val radiusSmall: Double
|
||||
abstract val radiusLarge: Double
|
||||
|
||||
// ============================
|
||||
// Models
|
||||
// ============================
|
||||
val sideSquare = model { columnSideSquare(-0.5, 0.5) }
|
||||
val sideRoundSmall = model { columnSide(radiusSmall, -0.5, 0.5) }
|
||||
val sideRoundLarge = model { columnSide(radiusLarge, -0.5, 0.5) }
|
||||
|
||||
val extendTopSquare = model { columnSideSquare(0.5, 0.5 + radiusLarge, topExtension(radiusLarge)) }
|
||||
val extendTopRoundSmall = model { columnSide(radiusSmall, 0.5, 0.5 + radiusLarge, topExtension(radiusLarge)) }
|
||||
val extendTopRoundLarge = model { columnSide(radiusLarge, 0.5, 0.5 + radiusLarge, topExtension(radiusLarge)) }
|
||||
inline fun extendTop(type: QuadrantType) = when(type) {
|
||||
SMALL_RADIUS -> extendTopRoundSmall.model
|
||||
LARGE_RADIUS -> extendTopRoundLarge.model
|
||||
SQUARE -> extendTopSquare.model
|
||||
INVISIBLE -> extendTopSquare.model
|
||||
else -> null
|
||||
}
|
||||
|
||||
val extendBottomSquare = model { columnSideSquare(-0.5 - radiusLarge, -0.5, bottomExtension(radiusLarge)) }
|
||||
val extendBottomRoundSmall = model { columnSide(radiusSmall, -0.5 - radiusLarge, -0.5, bottomExtension(radiusLarge)) }
|
||||
val extendBottomRoundLarge = model { columnSide(radiusLarge, -0.5 - radiusLarge, -0.5, bottomExtension(radiusLarge)) }
|
||||
inline fun extendBottom(type: QuadrantType) = when (type) {
|
||||
SMALL_RADIUS -> extendBottomRoundSmall.model
|
||||
LARGE_RADIUS -> extendBottomRoundLarge.model
|
||||
SQUARE -> extendBottomSquare.model
|
||||
INVISIBLE -> extendBottomSquare.model
|
||||
else -> null
|
||||
}
|
||||
|
||||
val topSquare = model { columnLidSquare() }
|
||||
val topRoundSmall = model { columnLid(radiusSmall) }
|
||||
val topRoundLarge = model { columnLid(radiusLarge) }
|
||||
inline fun flatTop(type: QuadrantType) = when(type) {
|
||||
SMALL_RADIUS -> topRoundSmall.model
|
||||
LARGE_RADIUS -> topRoundLarge.model
|
||||
SQUARE -> topSquare.model
|
||||
INVISIBLE -> topSquare.model
|
||||
else -> null
|
||||
}
|
||||
|
||||
val bottomSquare = model { columnLidSquare() { it.rotate(rot(EAST) * 2 + rot(UP)).mirrorUV(true, true) } }
|
||||
val bottomRoundSmall = model { columnLid(radiusSmall) { it.rotate(rot(EAST) * 2 + rot(UP)).mirrorUV(true, true) } }
|
||||
val bottomRoundLarge = model { columnLid(radiusLarge) { it.rotate(rot(EAST) * 2 + rot(UP)).mirrorUV(true, true) } }
|
||||
inline fun flatBottom(type: QuadrantType) = when(type) {
|
||||
SMALL_RADIUS -> bottomRoundSmall.model
|
||||
LARGE_RADIUS -> bottomRoundLarge.model
|
||||
SQUARE -> bottomSquare.model
|
||||
INVISIBLE -> bottomSquare.model
|
||||
else -> null
|
||||
}
|
||||
|
||||
val transitionTop = model { mix(sideRoundLarge.model, sideRoundSmall.model) { it > 1 } }
|
||||
val transitionBottom = model { mix(sideRoundSmall.model, sideRoundLarge.model) { it > 1 } }
|
||||
|
||||
inline fun continuous(q1: QuadrantType, q2: QuadrantType) =
|
||||
q1 == q2 || ((q1 == SQUARE || q1 == INVISIBLE) && (q2 == SQUARE || q2 == INVISIBLE))
|
||||
|
||||
@Suppress("NON_EXHAUSTIVE_WHEN")
|
||||
override fun render(ctx: CombinedContext) {
|
||||
|
||||
val roundLog = ChunkOverlayManager.get(overlayLayer, ctx)
|
||||
when(roundLog) {
|
||||
ColumnLayerData.SkipRender -> return
|
||||
ColumnLayerData.NormalRender -> return ctx.render()
|
||||
ColumnLayerData.ResolveError, null -> {
|
||||
BetterFoliage.logRenderError(ctx.state, ctx.pos)
|
||||
return ctx.render()
|
||||
}
|
||||
}
|
||||
|
||||
// if log axis is not defined and "Default to vertical" config option is not set, render normally
|
||||
if ((roundLog as ColumnLayerData.SpecialRender).column.axis == null && !overlayLayer.defaultToY) {
|
||||
return ctx.render()
|
||||
}
|
||||
|
||||
val baseRotation = rotationFromUp[((roundLog.column.axis ?: Axis.Y) to AxisDirection.POSITIVE).face.ordinal]
|
||||
renderAs(ctx, MODEL) {
|
||||
quadrantRotations.forEachIndexed { idx, quadrantRotation ->
|
||||
// set rotation for the current quadrant
|
||||
val rotation = baseRotation + quadrantRotation
|
||||
|
||||
// disallow sharp discontinuities in the chamfer radius, or tapering-in where inappropriate
|
||||
if (roundLog.quadrants[idx] == LARGE_RADIUS &&
|
||||
roundLog.upType == PARALLEL && roundLog.quadrantsTop[idx] != LARGE_RADIUS &&
|
||||
roundLog.downType == PARALLEL && roundLog.quadrantsBottom[idx] != LARGE_RADIUS) {
|
||||
roundLog.quadrants[idx] = SMALL_RADIUS
|
||||
}
|
||||
|
||||
// render side of current quadrant
|
||||
val sideModel = when (roundLog.quadrants[idx]) {
|
||||
SMALL_RADIUS -> sideRoundSmall.model
|
||||
LARGE_RADIUS -> if (roundLog.upType == PARALLEL && roundLog.quadrantsTop[idx] == SMALL_RADIUS) transitionTop.model
|
||||
else if (roundLog.downType == PARALLEL && roundLog.quadrantsBottom[idx] == SMALL_RADIUS) transitionBottom.model
|
||||
else sideRoundLarge.model
|
||||
SQUARE -> sideSquare.model
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (sideModel != null) ctx.render(
|
||||
sideModel,
|
||||
rotation,
|
||||
icon = roundLog.column.side,
|
||||
postProcess = noPost
|
||||
)
|
||||
|
||||
// render top and bottom end of current quadrant
|
||||
var upModel: Model? = null
|
||||
var downModel: Model? = null
|
||||
var upIcon = roundLog.column.top
|
||||
var downIcon = roundLog.column.bottom
|
||||
var isLidUp = true
|
||||
var isLidDown = true
|
||||
|
||||
when (roundLog.upType) {
|
||||
NONSOLID -> upModel = flatTop(roundLog.quadrants[idx])
|
||||
PERPENDICULAR -> {
|
||||
if (!connectPerpendicular) {
|
||||
upModel = flatTop(roundLog.quadrants[idx])
|
||||
} else {
|
||||
upIcon = roundLog.column.side
|
||||
upModel = extendTop(roundLog.quadrants[idx])
|
||||
isLidUp = false
|
||||
}
|
||||
}
|
||||
PARALLEL -> {
|
||||
if (!continuous(roundLog.quadrants[idx], roundLog.quadrantsTop[idx])) {
|
||||
if (roundLog.quadrants[idx] == SQUARE || roundLog.quadrants[idx] == INVISIBLE) {
|
||||
upModel = topSquare.model
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
when (roundLog.downType) {
|
||||
NONSOLID -> downModel = flatBottom(roundLog.quadrants[idx])
|
||||
PERPENDICULAR -> {
|
||||
if (!connectPerpendicular) {
|
||||
downModel = flatBottom(roundLog.quadrants[idx])
|
||||
} else {
|
||||
downIcon = roundLog.column.side
|
||||
downModel = extendBottom(roundLog.quadrants[idx])
|
||||
isLidDown = false
|
||||
}
|
||||
}
|
||||
PARALLEL -> {
|
||||
if (!continuous(roundLog.quadrants[idx], roundLog.quadrantsBottom[idx]) &&
|
||||
(roundLog.quadrants[idx] == SQUARE || roundLog.quadrants[idx] == INVISIBLE)) {
|
||||
downModel = bottomSquare.model
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (upModel != null) ctx.render(
|
||||
upModel,
|
||||
rotation,
|
||||
icon = upIcon,
|
||||
postProcess = { _, _, _, _, _ ->
|
||||
if (isLidUp) {
|
||||
rotateUV(idx + if (roundLog.column.axis == Axis.X) 1 else 0)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (downModel != null) ctx.render(
|
||||
downModel,
|
||||
rotation,
|
||||
icon = downIcon,
|
||||
postProcess = { _, _, _, _, _ ->
|
||||
if (isLidDown) {
|
||||
rotateUV((if (roundLog.column.axis == Axis.X) 0 else 3) - idx)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package mods.betterfoliage.client.render.column
|
||||
|
||||
import mods.betterfoliage.client.chunk.ChunkOverlayLayer
|
||||
import mods.betterfoliage.client.chunk.ChunkOverlayManager
|
||||
import mods.betterfoliage.client.chunk.dimType
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.BlockType.*
|
||||
import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.QuadrantType
|
||||
import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.QuadrantType.*
|
||||
import mods.betterfoliage.client.render.rotationFromUp
|
||||
import mods.octarinecore.client.render.BlockCtx
|
||||
import mods.octarinecore.client.resource.ModelRenderRegistry
|
||||
import mods.octarinecore.common.*
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.util.Direction.Axis
|
||||
import net.minecraft.util.Direction.AxisDirection
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.world.IEnviromentBlockReader
|
||||
|
||||
/** Index of SOUTH-EAST quadrant. */
|
||||
const val SE = 0
|
||||
/** Index of NORTH-EAST quadrant. */
|
||||
const val NE = 1
|
||||
/** Index of NORTH-WEST quadrant. */
|
||||
const val NW = 2
|
||||
/** Index of SOUTH-WEST quadrant. */
|
||||
const val SW = 3
|
||||
|
||||
/**
|
||||
* Sealed class hierarchy for all possible render outcomes
|
||||
*/
|
||||
sealed class ColumnLayerData {
|
||||
/**
|
||||
* Data structure to cache texture and world neighborhood data relevant to column rendering
|
||||
*/
|
||||
@Suppress("ArrayInDataClass") // not used in comparisons anywhere
|
||||
data class SpecialRender(
|
||||
val column: ColumnTextureInfo,
|
||||
val upType: BlockType,
|
||||
val downType: BlockType,
|
||||
val quadrants: Array<QuadrantType>,
|
||||
val quadrantsTop: Array<QuadrantType>,
|
||||
val quadrantsBottom: Array<QuadrantType>
|
||||
) : ColumnLayerData() {
|
||||
enum class BlockType { SOLID, NONSOLID, PARALLEL, PERPENDICULAR }
|
||||
enum class QuadrantType { SMALL_RADIUS, LARGE_RADIUS, SQUARE, INVISIBLE }
|
||||
}
|
||||
|
||||
/** Column block should not be rendered at all */
|
||||
object SkipRender : ColumnLayerData()
|
||||
|
||||
/** Column block must be rendered normally */
|
||||
object NormalRender : ColumnLayerData()
|
||||
|
||||
/** Error while resolving render data, column block must be rendered normally */
|
||||
object ResolveError : ColumnLayerData()
|
||||
}
|
||||
|
||||
|
||||
abstract class ColumnRenderLayer : ChunkOverlayLayer<ColumnLayerData> {
|
||||
|
||||
abstract val registry: ModelRenderRegistry<ColumnTextureInfo>
|
||||
abstract val blockPredicate: (BlockState)->Boolean
|
||||
abstract val connectSolids: Boolean
|
||||
abstract val lenientConnect: Boolean
|
||||
abstract val defaultToY: Boolean
|
||||
|
||||
val allNeighborOffsets = (-1..1).flatMap { offsetX -> (-1..1).flatMap { offsetY -> (-1..1).map { offsetZ -> Int3(offsetX, offsetY, offsetZ) }}}
|
||||
|
||||
override fun onBlockUpdate(world: IEnviromentBlockReader, pos: BlockPos) {
|
||||
allNeighborOffsets.forEach { offset -> ChunkOverlayManager.clear(world.dimType, this, pos + offset) }
|
||||
}
|
||||
|
||||
override fun calculate(ctx: BlockCtx): ColumnLayerData {
|
||||
if (allDirections.all { ctx.offset(it).isNormalCube }) return ColumnLayerData.SkipRender
|
||||
val columnTextures = registry[ctx] ?: return ColumnLayerData.ResolveError
|
||||
|
||||
// if log axis is not defined and "Default to vertical" config option is not set, render normally
|
||||
val logAxis = columnTextures.axis ?: if (defaultToY) Axis.Y else return ColumnLayerData.NormalRender
|
||||
|
||||
// check log neighborhood
|
||||
val baseRotation = rotationFromUp[(logAxis to AxisDirection.POSITIVE).face.ordinal]
|
||||
|
||||
val upType = ctx.blockType(baseRotation, logAxis, Int3(0, 1, 0))
|
||||
val downType = ctx.blockType(baseRotation, logAxis, Int3(0, -1, 0))
|
||||
|
||||
val quadrants = Array(4) { SMALL_RADIUS }.checkNeighbors(ctx, baseRotation, logAxis, 0)
|
||||
val quadrantsTop = Array(4) { SMALL_RADIUS }
|
||||
if (upType == PARALLEL) quadrantsTop.checkNeighbors(ctx, baseRotation, logAxis, 1)
|
||||
val quadrantsBottom = Array(4) { SMALL_RADIUS }
|
||||
if (downType == PARALLEL) quadrantsBottom.checkNeighbors(ctx, baseRotation, logAxis, -1)
|
||||
return ColumnLayerData.SpecialRender(columnTextures, upType, downType, quadrants, quadrantsTop, quadrantsBottom)
|
||||
}
|
||||
|
||||
/** Sets the type of the given quadrant only if the new value is "stronger" (larger ordinal). */
|
||||
inline fun Array<QuadrantType>.upgrade(idx: Int, value: QuadrantType) {
|
||||
if (this[idx].ordinal < value.ordinal) this[idx] = value
|
||||
}
|
||||
|
||||
/** Fill the array of [QuadrantType]s based on the blocks to the sides of this one. */
|
||||
fun Array<QuadrantType>.checkNeighbors(ctx: BlockCtx, rotation: Rotation, logAxis: Axis, yOff: Int): Array<QuadrantType> {
|
||||
val blkS = ctx.blockType(rotation, logAxis, Int3(0, yOff, 1))
|
||||
val blkE = ctx.blockType(rotation, logAxis, Int3(1, yOff, 0))
|
||||
val blkN = ctx.blockType(rotation, logAxis, Int3(0, yOff, -1))
|
||||
val blkW = ctx.blockType(rotation, logAxis, Int3(-1, yOff, 0))
|
||||
|
||||
// a solid block on one side will make the 2 neighboring quadrants SQUARE
|
||||
// if there are solid blocks to both sides of a quadrant, it is INVISIBLE
|
||||
if (connectSolids) {
|
||||
if (blkS == SOLID) {
|
||||
upgrade(SW, SQUARE); upgrade(SE, SQUARE)
|
||||
}
|
||||
if (blkE == SOLID) {
|
||||
upgrade(SE, SQUARE); upgrade(NE, SQUARE)
|
||||
}
|
||||
if (blkN == SOLID) {
|
||||
upgrade(NE, SQUARE); upgrade(NW, SQUARE)
|
||||
}
|
||||
if (blkW == SOLID) {
|
||||
upgrade(NW, SQUARE); upgrade(SW, SQUARE)
|
||||
}
|
||||
if (blkS == SOLID && blkE == SOLID) upgrade(SE, INVISIBLE)
|
||||
if (blkN == SOLID && blkE == SOLID) upgrade(NE, INVISIBLE)
|
||||
if (blkN == SOLID && blkW == SOLID) upgrade(NW, INVISIBLE)
|
||||
if (blkS == SOLID && blkW == SOLID) upgrade(SW, INVISIBLE)
|
||||
}
|
||||
val blkSE = ctx.blockType(rotation, logAxis, Int3(1, yOff, 1))
|
||||
val blkNE = ctx.blockType(rotation, logAxis, Int3(1, yOff, -1))
|
||||
val blkNW = ctx.blockType(rotation, logAxis, Int3(-1, yOff, -1))
|
||||
val blkSW = ctx.blockType(rotation, logAxis, Int3(-1, yOff, 1))
|
||||
|
||||
if (lenientConnect) {
|
||||
// if the block forms the tip of an L-shape, connect to its neighbor with SQUARE quadrants
|
||||
if (blkE == PARALLEL && (blkSE == PARALLEL || blkNE == PARALLEL)) {
|
||||
upgrade(SE, SQUARE); upgrade(NE, SQUARE)
|
||||
}
|
||||
if (blkN == PARALLEL && (blkNE == PARALLEL || blkNW == PARALLEL)) {
|
||||
upgrade(NE, SQUARE); upgrade(NW, SQUARE)
|
||||
}
|
||||
if (blkW == PARALLEL && (blkNW == PARALLEL || blkSW == PARALLEL)) {
|
||||
upgrade(NW, SQUARE); upgrade(SW, SQUARE)
|
||||
}
|
||||
if (blkS == PARALLEL && (blkSE == PARALLEL || blkSW == PARALLEL)) {
|
||||
upgrade(SW, SQUARE); upgrade(SE, SQUARE)
|
||||
}
|
||||
}
|
||||
|
||||
// if the block forms the middle of an L-shape, or is part of a 2x2 configuration,
|
||||
// connect to its neighbors with SQUARE quadrants, INVISIBLE on the inner corner, and LARGE_RADIUS on the outer corner
|
||||
if (blkN == PARALLEL && blkW == PARALLEL && (lenientConnect || blkNW == PARALLEL)) {
|
||||
upgrade(SE, LARGE_RADIUS); upgrade(NE, SQUARE); upgrade(SW, SQUARE); upgrade(NW, INVISIBLE)
|
||||
}
|
||||
if (blkS == PARALLEL && blkW == PARALLEL && (lenientConnect || blkSW == PARALLEL)) {
|
||||
upgrade(NE, LARGE_RADIUS); upgrade(SE, SQUARE); upgrade(NW, SQUARE); upgrade(SW, INVISIBLE)
|
||||
}
|
||||
if (blkS == PARALLEL && blkE == PARALLEL && (lenientConnect || blkSE == PARALLEL)) {
|
||||
upgrade(NW, LARGE_RADIUS); upgrade(NE, SQUARE); upgrade(SW, SQUARE); upgrade(SE, INVISIBLE)
|
||||
}
|
||||
if (blkN == PARALLEL && blkE == PARALLEL && (lenientConnect || blkNE == PARALLEL)) {
|
||||
upgrade(SW, LARGE_RADIUS); upgrade(SE, SQUARE); upgrade(NW, SQUARE); upgrade(NE, INVISIBLE)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of the block at the given offset in a rotated reference frame.
|
||||
*/
|
||||
fun BlockCtx.blockType(rotation: Rotation, axis: Axis, offset: Int3): ColumnLayerData.SpecialRender.BlockType {
|
||||
val offsetRot = offset.rotate(rotation)
|
||||
val state = state(offsetRot)
|
||||
return if (!blockPredicate(state)) {
|
||||
if (offset(offsetRot).isNormalCube) SOLID else NONSOLID
|
||||
} else {
|
||||
(registry[state, world, pos + offsetRot]?.axis ?: if (Config.roundLogs.defaultY) Axis.Y else null)?.let {
|
||||
if (it == axis) PARALLEL else PERPENDICULAR
|
||||
} ?: SOLID
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package mods.betterfoliage.client.render.column
|
||||
|
||||
import mods.octarinecore.client.render.lighting.QuadIconResolver
|
||||
import mods.octarinecore.common.rotate
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite
|
||||
import net.minecraft.util.Direction.*
|
||||
|
||||
interface ColumnTextureInfo {
|
||||
val axis: Axis?
|
||||
val top: QuadIconResolver
|
||||
val bottom: QuadIconResolver
|
||||
val side: QuadIconResolver
|
||||
}
|
||||
|
||||
open class SimpleColumnInfo(
|
||||
override val axis: Axis?,
|
||||
val topTexture: TextureAtlasSprite,
|
||||
val bottomTexture: TextureAtlasSprite,
|
||||
val sideTextures: List<TextureAtlasSprite>
|
||||
) : ColumnTextureInfo {
|
||||
|
||||
// index offsets for EnumFacings, to make it less likely for neighboring faces to get the same bark texture
|
||||
val dirToIdx = arrayOf(0, 1, 2, 4, 3, 5)
|
||||
|
||||
override val top: QuadIconResolver = { _, _, _ -> topTexture }
|
||||
override val bottom: QuadIconResolver = { _, _, _ -> bottomTexture }
|
||||
override val side: QuadIconResolver = { ctx, idx, _ ->
|
||||
val worldFace = (if ((idx and 1) == 0) SOUTH else EAST).rotate(ctx.modelRotation)
|
||||
val sideIdx = if (sideTextures.size > 1) (ctx.semiRandom(1) + dirToIdx[worldFace.ordinal]) % sideTextures.size else 0
|
||||
sideTextures[sideIdx]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package mods.betterfoliage.client.resource
|
||||
|
||||
import net.minecraft.client.renderer.model.ModelResourceLocation
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite
|
||||
import net.minecraft.util.ResourceLocation
|
||||
|
||||
typealias Identifier = ResourceLocation
|
||||
typealias ModelIdentifier = ModelResourceLocation
|
||||
|
||||
typealias Sprite = TextureAtlasSprite
|
||||
@@ -0,0 +1,50 @@
|
||||
package mods.betterfoliage.client.texture
|
||||
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.octarinecore.client.resource.*
|
||||
import net.minecraft.resources.IResourceManager
|
||||
import java.awt.image.BufferedImage
|
||||
|
||||
/**
|
||||
* Generate Short Grass textures from [Blocks.tallgrass] block textures.
|
||||
* The bottom 3/8 of the base texture is chopped off.
|
||||
*
|
||||
* @param[domain] Resource domain of generator
|
||||
*/
|
||||
data class GeneratedGrass(val sprite: Identifier, val isSnowed: Boolean, val atlas: Atlas = Atlas.BLOCKS) {
|
||||
constructor(sprite: String, isSnowed: Boolean) : this(Identifier(sprite), isSnowed)
|
||||
|
||||
fun register(pack: GeneratedBlockTexturePack) = pack.register(this, this::draw)
|
||||
|
||||
fun draw(resourceManager: IResourceManager): ByteArray {
|
||||
val baseTexture = resourceManager.loadSprite(atlas.wrap(sprite))
|
||||
|
||||
val result = BufferedImage(baseTexture.width, baseTexture.height, BufferedImage.TYPE_4BYTE_ABGR)
|
||||
val graphics = result.createGraphics()
|
||||
|
||||
val size = baseTexture.width
|
||||
val frames = baseTexture.height / size
|
||||
|
||||
// iterate all frames
|
||||
for (frame in 0 until frames) {
|
||||
val baseFrame = baseTexture.getSubimage(0, size * frame, size, size)
|
||||
val grassFrame = BufferedImage(size, size, BufferedImage.TYPE_4BYTE_ABGR)
|
||||
|
||||
// draw bottom half of texture
|
||||
grassFrame.createGraphics().apply {
|
||||
drawImage(baseFrame, 0, 3 * size / 8, null)
|
||||
}
|
||||
|
||||
// add to animated png
|
||||
graphics.drawImage(grassFrame, 0, size * frame, null)
|
||||
}
|
||||
|
||||
// blend with white if snowed
|
||||
if (isSnowed) {
|
||||
for (x in 0..result.width - 1) for (y in 0..result.height - 1) {
|
||||
result[x, y] = blendRGB(result[x, y], 16777215, 2, 3)
|
||||
}
|
||||
}
|
||||
return result.bytes
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package mods.betterfoliage.client.texture
|
||||
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.octarinecore.client.resource.*
|
||||
import net.minecraft.resources.IResource
|
||||
import net.minecraft.resources.IResourceManager
|
||||
import net.minecraft.util.ResourceLocation
|
||||
import java.awt.image.BufferedImage
|
||||
|
||||
/**
|
||||
* Generate round leaf textures from leaf block textures.
|
||||
* The base texture is tiled 2x2, then parts of it are made transparent by applying a mask to the alpha channel.
|
||||
*
|
||||
* Different leaf types may have their own alpha mask.
|
||||
*
|
||||
* @param[domain] Resource domain of generator
|
||||
*/
|
||||
data class GeneratedLeaf(val sprite: ResourceLocation, val leafType: String, val atlas: Atlas = Atlas.BLOCKS) {
|
||||
|
||||
fun register(pack: GeneratedBlockTexturePack) = pack.register(this, this::draw)
|
||||
|
||||
fun draw(resourceManager: IResourceManager): ByteArray {
|
||||
val baseTexture = resourceManager.loadSprite(atlas.wrap(sprite))
|
||||
|
||||
val size = baseTexture.width
|
||||
val frames = baseTexture.height / size
|
||||
|
||||
val maskTexture = (getLeafMask(leafType, size * 2) ?: getLeafMask("default", size * 2))?.loadImage()
|
||||
fun scale(i: Int) = i * maskTexture!!.width / (size * 2)
|
||||
|
||||
val leafTexture = BufferedImage(size * 2, size * 2 * frames, BufferedImage.TYPE_4BYTE_ABGR)
|
||||
val graphics = leafTexture.createGraphics()
|
||||
|
||||
// iterate all frames
|
||||
for (frame in 0 until frames) {
|
||||
val baseFrame = baseTexture.getSubimage(0, size * frame, size, size)
|
||||
val leafFrame = BufferedImage(size * 2, size * 2, BufferedImage.TYPE_4BYTE_ABGR)
|
||||
|
||||
// tile leaf texture 2x2
|
||||
leafFrame.createGraphics().apply {
|
||||
drawImage(baseFrame, 0, 0, null)
|
||||
drawImage(baseFrame, 0, size, null)
|
||||
drawImage(baseFrame, size, 0, null)
|
||||
drawImage(baseFrame, size, size, null)
|
||||
}
|
||||
|
||||
// overlay alpha mask
|
||||
if (maskTexture != null) {
|
||||
for (x in 0 until size * 2) for (y in 0 until size * 2) {
|
||||
val basePixel = leafFrame[x, y].toLong() and 0xFFFFFFFFL
|
||||
val maskPixel = maskTexture[scale(x), scale(y)].toLong() and 0xFF000000L or 0xFFFFFFL
|
||||
leafFrame[x, y] = (basePixel and maskPixel).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
// add to animated png
|
||||
graphics.drawImage(leafFrame, 0, size * frame * 2, null)
|
||||
}
|
||||
|
||||
return leafTexture.bytes
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the alpha mask to use
|
||||
*
|
||||
* @param[type] Alpha mask type.
|
||||
* @param[maxSize] Preferred mask size.
|
||||
*/
|
||||
fun getLeafMask(type: String, maxSize: Int) = getMultisizeTexture(maxSize) { size ->
|
||||
ResourceLocation(BetterFoliageMod.MOD_ID, "textures/blocks/leafmask_${size}_${type}.png")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a texture resource when multiple sizes may exist.
|
||||
*
|
||||
* @param[maxSize] Maximum size to consider. This value is progressively halved when searching for smaller versions.
|
||||
* @param[maskPath] Location of the texture of the given size
|
||||
*
|
||||
*/
|
||||
fun getMultisizeTexture(maxSize: Int, maskPath: (Int)->ResourceLocation): IResource? {
|
||||
var size = maxSize
|
||||
val sizes = mutableListOf<Int>()
|
||||
while(size > 2) { sizes.add(size); size /= 2 }
|
||||
return sizes.map { resourceManager[maskPath(it)] }.filterNotNull().firstOrNull()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package mods.betterfoliage.client.texture
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.client.config.BlockConfig
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.octarinecore.client.render.lighting.HSB
|
||||
import mods.octarinecore.client.resource.*
|
||||
import mods.octarinecore.common.config.ConfigurableBlockMatcher
|
||||
import mods.octarinecore.common.config.ModelTextureList
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite
|
||||
import org.apache.logging.log4j.Level
|
||||
import java.lang.Math.min
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
const val defaultGrassColor = 0
|
||||
|
||||
/** Rendering-related information for a grass block. */
|
||||
class GrassInfo(
|
||||
/** Top texture of the grass block. */
|
||||
val grassTopTexture: TextureAtlasSprite,
|
||||
|
||||
/**
|
||||
* Color to use for Short Grass rendering instead of the biome color.
|
||||
*
|
||||
* Value is null if the texture is mostly grey (the saturation of its average color is under a configurable limit),
|
||||
* the average color of the texture otherwise.
|
||||
*/
|
||||
val overrideColor: Int?
|
||||
)
|
||||
|
||||
object GrassRegistry : ModelRenderRegistryRoot<GrassInfo>()
|
||||
|
||||
object AsyncGrassDiscovery : ConfigurableModelDiscovery<GrassInfo>() {
|
||||
override val logger = BetterFoliage.logDetail
|
||||
override val matchClasses: ConfigurableBlockMatcher get() = BlockConfig.grassBlocks
|
||||
override val modelTextures: List<ModelTextureList> get() = BlockConfig.grassModels.modelList
|
||||
|
||||
override fun processModel(state: BlockState, textures: List<String>, atlas: AtlasFuture): CompletableFuture<GrassInfo> {
|
||||
val textureName = textures[0]
|
||||
val spriteF = atlas.sprite(Identifier(textureName))
|
||||
logger.log(Level.DEBUG, "$logName: texture $textureName")
|
||||
return atlas.mapAfter {
|
||||
val sprite = spriteF.get()
|
||||
logger.log(Level.DEBUG, "$logName: block state $state")
|
||||
logger.log(Level.DEBUG, "$logName: texture $textureName")
|
||||
val hsb = HSB.fromColor(sprite.averageColor)
|
||||
val overrideColor = if (hsb.saturation >= Config.shortGrass.saturationThreshold) {
|
||||
logger.log(Level.DEBUG, "$logName: brightness ${hsb.brightness}")
|
||||
logger.log(Level.DEBUG, "$logName: saturation ${hsb.saturation} >= ${Config.shortGrass.saturationThreshold}, using texture color")
|
||||
hsb.copy(brightness = min(0.9f, hsb.brightness * 2.0f)).asColor
|
||||
} else {
|
||||
logger.log(Level.DEBUG, "$logName: saturation ${hsb.saturation} < ${Config.shortGrass.saturationThreshold}, using block color")
|
||||
null
|
||||
}
|
||||
GrassInfo(sprite, overrideColor)
|
||||
}
|
||||
}
|
||||
|
||||
fun init() {
|
||||
GrassRegistry.registries.add(this)
|
||||
BetterFoliage.blockSprites.providers.add(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package mods.betterfoliage.client.texture
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.betterfoliage.client.resource.Sprite
|
||||
import mods.octarinecore.client.resource.*
|
||||
import mods.octarinecore.stripStart
|
||||
import mods.octarinecore.client.resource.Atlas
|
||||
import mods.octarinecore.common.sinkAsync
|
||||
import net.minecraft.client.particle.ParticleManager
|
||||
import net.minecraft.resources.IResourceManager
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class FixedSpriteSet(val sprites: List<Sprite>) : SpriteSet {
|
||||
override val num = sprites.size
|
||||
override fun get(idx: Int) = sprites[idx % num]
|
||||
}
|
||||
|
||||
object LeafParticleRegistry : AsyncSpriteProvider<ParticleManager> {
|
||||
val targetAtlas = Atlas.PARTICLES
|
||||
val typeMappings = TextureMatcher()
|
||||
val particles = hashMapOf<String, SpriteSet>()
|
||||
|
||||
operator fun get(type: String) = particles[type] ?: particles["default"]!!
|
||||
|
||||
override fun setup(manager: IResourceManager, particleF: CompletableFuture<ParticleManager>, atlasFuture: AtlasFuture): StitchPhases {
|
||||
particles.clear()
|
||||
val futures = mutableMapOf<String, List<CompletableFuture<Sprite>>>()
|
||||
|
||||
return StitchPhases(
|
||||
discovery = particleF.sinkAsync {
|
||||
typeMappings.loadMappings(Identifier(BetterFoliageMod.MOD_ID, "leaf_texture_mappings.cfg"))
|
||||
(typeMappings.mappings.map { it.type } + "default").distinct().forEach { leafType ->
|
||||
val ids = (0 until 16).map { idx -> Identifier(BetterFoliageMod.MOD_ID, "falling_leaf_${leafType}_$idx") }
|
||||
val wids = ids.map { Atlas.PARTICLES.wrap(it) }
|
||||
futures[leafType] = (0 until 16).map { idx -> Identifier(BetterFoliageMod.MOD_ID, "falling_leaf_${leafType}_$idx") }
|
||||
.filter { manager.hasResource(Atlas.PARTICLES.wrap(it)) }
|
||||
.map { atlasFuture.sprite(it) }
|
||||
}
|
||||
},
|
||||
cleanup = atlasFuture.runAfter {
|
||||
futures.forEach { leafType, spriteFutures ->
|
||||
val sprites = spriteFutures.filter { !it.isCompletedExceptionally }.map { it.get() }
|
||||
if (sprites.isNotEmpty()) particles[leafType] = FixedSpriteSet(sprites)
|
||||
}
|
||||
if (particles["default"] == null) particles["default"] = FixedSpriteSet(listOf(atlasFuture.missing.get()!!))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun init() {
|
||||
BetterFoliage.particleSprites.providers.add(this)
|
||||
}
|
||||
}
|
||||
|
||||
class TextureMatcher {
|
||||
|
||||
data class Mapping(val domain: String?, val path: String, val type: String) {
|
||||
fun matches(iconLocation: Identifier): Boolean {
|
||||
return (domain == null || domain == iconLocation.namespace) &&
|
||||
iconLocation.path.stripStart("blocks/").contains(path, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
val mappings: MutableList<Mapping> = mutableListOf()
|
||||
|
||||
fun getType(resource: Identifier) = mappings.filter { it.matches(resource) }.map { it.type }.firstOrNull()
|
||||
fun getType(iconName: String) = Identifier(iconName).let { getType(it) }
|
||||
|
||||
fun loadMappings(mappingLocation: Identifier) {
|
||||
mappings.clear()
|
||||
resourceManager[mappingLocation]?.getLines()?.let { lines ->
|
||||
lines.filter { !it.startsWith("//") }.filter { !it.isEmpty() }.forEach { line ->
|
||||
val line2 = line.trim().split('=')
|
||||
if (line2.size == 2) {
|
||||
val mapping = line2[0].trim().split(':')
|
||||
if (mapping.size == 1) mappings.add(Mapping(null, mapping[0].trim(), line2[1].trim()))
|
||||
else if (mapping.size == 2) mappings.add(Mapping(mapping[0].trim(), mapping[1].trim(), line2[1].trim()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package mods.betterfoliage.client.texture
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.client.config.BlockConfig
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.octarinecore.HasLogger
|
||||
import mods.octarinecore.client.resource.*
|
||||
import mods.octarinecore.common.config.ConfigurableBlockMatcher
|
||||
import mods.octarinecore.common.config.ModelTextureList
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
const val defaultLeafColor = 0
|
||||
|
||||
/** Rendering-related information for a leaf block. */
|
||||
class LeafInfo(
|
||||
/** The generated round leaf texture. */
|
||||
val roundLeafTexture: TextureAtlasSprite,
|
||||
|
||||
/** Type of the leaf block (configurable by user). */
|
||||
val leafType: String,
|
||||
|
||||
/** Average color of the round leaf texture. */
|
||||
val averageColor: Int = roundLeafTexture.averageColor
|
||||
) {
|
||||
/** [IconSet] of the textures to use for leaf particles emitted from this block. */
|
||||
val particleTextures: SpriteSet get() = LeafParticleRegistry[leafType]
|
||||
}
|
||||
|
||||
object LeafRegistry : ModelRenderRegistryRoot<LeafInfo>()
|
||||
|
||||
object AsyncLeafDiscovery : ConfigurableModelDiscovery<LeafInfo>() {
|
||||
override val logger = BetterFoliage.logDetail
|
||||
override val matchClasses: ConfigurableBlockMatcher get() = BlockConfig.leafBlocks
|
||||
override val modelTextures: List<ModelTextureList> get() = BlockConfig.leafModels.modelList
|
||||
|
||||
override fun processModel(state: BlockState, textures: List<String>, atlas: AtlasFuture) = defaultRegisterLeaf(Identifier(textures[0]), atlas)
|
||||
|
||||
fun init() {
|
||||
LeafRegistry.registries.add(this)
|
||||
BetterFoliage.blockSprites.providers.add(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun HasLogger.defaultRegisterLeaf(sprite: Identifier, atlas: AtlasFuture): CompletableFuture<LeafInfo> {
|
||||
val leafType = LeafParticleRegistry.typeMappings.getType(sprite) ?: "default"
|
||||
val generated = GeneratedLeaf(sprite, leafType).register(BetterFoliage.asyncPack)
|
||||
val roundLeaf = atlas.sprite(generated)
|
||||
|
||||
log(" leaf texture $sprite")
|
||||
log(" particle $leafType")
|
||||
return atlas.mapAfter {
|
||||
LeafInfo(roundLeaf.get(), leafType)
|
||||
}
|
||||
}
|
||||
20
src/main/kotlin/mods/betterfoliage/client/texture/Utils.kt
Normal file
20
src/main/kotlin/mods/betterfoliage/client/texture/Utils.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
@file:JvmName("Utils")
|
||||
package mods.betterfoliage.client.texture
|
||||
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.octarinecore.client.resource.Atlas
|
||||
import mods.octarinecore.client.resource.get
|
||||
import mods.octarinecore.client.resource.loadImage
|
||||
import net.minecraft.resources.IResourceManager
|
||||
import java.io.IOException
|
||||
|
||||
fun blendRGB(rgb1: Int, rgb2: Int, weight1: Int, weight2: Int): Int {
|
||||
val r = (((rgb1 shr 16) and 255) * weight1 + ((rgb2 shr 16) and 255) * weight2) / (weight1 + weight2)
|
||||
val g = (((rgb1 shr 8) and 255) * weight1 + ((rgb2 shr 8) and 255) * weight2) / (weight1 + weight2)
|
||||
val b = ((rgb1 and 255) * weight1 + (rgb2 and 255) * weight2) / (weight1 + weight2)
|
||||
val a = (rgb1 shr 24) and 255
|
||||
val result = ((a shl 24) or (r shl 16) or (g shl 8) or b).toInt()
|
||||
return result
|
||||
}
|
||||
|
||||
fun IResourceManager.loadSprite(id: Identifier) = this.get(id)?.loadImage() ?: throw IOException("Cannot load resource $id")
|
||||
69
src/main/kotlin/mods/octarinecore/CommonRefs.kt
Normal file
69
src/main/kotlin/mods/octarinecore/CommonRefs.kt
Normal file
@@ -0,0 +1,69 @@
|
||||
package mods.octarinecore
|
||||
|
||||
import mods.octarinecore.metaprog.ClassRef
|
||||
import mods.octarinecore.metaprog.ClassRef.Companion.void
|
||||
import mods.octarinecore.metaprog.FieldRef
|
||||
import mods.octarinecore.metaprog.MethodRef
|
||||
import net.minecraft.block.Block
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.client.renderer.BlockRendererDispatcher
|
||||
import net.minecraft.client.renderer.BufferBuilder
|
||||
import net.minecraft.client.renderer.chunk.ChunkRenderCache
|
||||
import net.minecraft.client.renderer.model.BakedQuad
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite
|
||||
import net.minecraft.util.BlockRenderLayer
|
||||
import net.minecraft.util.ResourceLocation
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.world.IBlockReader
|
||||
import net.minecraft.world.IEnviromentBlockReader
|
||||
import java.util.*
|
||||
|
||||
// Java
|
||||
val String = ClassRef<String>("java.lang.String")
|
||||
val Map = ClassRef<Map<*, *>>("java.util.Map")
|
||||
val List = ClassRef<List<*>>("java.util.List")
|
||||
val Random = ClassRef<Random>("java.util.Random")
|
||||
|
||||
// Minecraft
|
||||
val IBlockReader = ClassRef<IBlockReader>("net.minecraft.world.IBlockReader")
|
||||
val IEnvironmentBlockReader = ClassRef<IEnviromentBlockReader>("net.minecraft.world.IEnviromentBlockReader")
|
||||
val BlockState = ClassRef<BlockState>("net.minecraft.block.BlockState")
|
||||
val BlockPos = ClassRef<BlockPos>("net.minecraft.util.math.BlockPos")
|
||||
val BlockRenderLayer = ClassRef<BlockRenderLayer>("net.minecraft.util.BlockRenderLayer")
|
||||
val Block = ClassRef<Block>("net.minecraft.block.Block")
|
||||
|
||||
val TextureAtlasSprite = ClassRef<TextureAtlasSprite>("net.minecraft.client.renderer.texture.TextureAtlasSprite")
|
||||
val BufferBuilder = ClassRef<BufferBuilder>("net.minecraft.client.renderer.BufferBuilder")
|
||||
val BufferBuilder_setSprite = MethodRef(BufferBuilder, "setSprite", void, TextureAtlasSprite)
|
||||
val BufferBuilder_sVertexBuilder = FieldRef(BufferBuilder, "sVertexBuilder", SVertexBuilder)
|
||||
val BlockRendererDispatcher = ClassRef<BlockRendererDispatcher>("net.minecraft.client.renderer.BlockRendererDispatcher")
|
||||
val ChunkRenderCache = ClassRef<ChunkRenderCache>("net.minecraft.client.renderer.chunk.ChunkRenderCache")
|
||||
val ResourceLocation = ClassRef<ResourceLocation>("net.minecraft.util.ResourceLocation")
|
||||
val BakedQuad = ClassRef<BakedQuad>("net.minecraft.client.renderer.model.BakedQuad")
|
||||
|
||||
// Optifine
|
||||
val OptifineClassTransformer = ClassRef<Any>("optifine.OptiFineClassTransformer")
|
||||
val BlockPosM = ClassRef<Any>("net.optifine.BlockPosM")
|
||||
object ChunkCacheOF : ClassRef<Any>("net.optifine.override.ChunkCacheOF") {
|
||||
val chunkCache = FieldRef(this, "chunkCache", ChunkRenderCache)
|
||||
}
|
||||
|
||||
object RenderEnv : ClassRef<Any>("net.optifine.render.RenderEnv") {
|
||||
val reset = MethodRef(this, "reset", void, BlockState, BlockPos)
|
||||
}
|
||||
|
||||
// Optifine custom colors
|
||||
val IColorizer = ClassRef<Any>("net.optifine.CustomColors\$IColorizer")
|
||||
object CustomColors : ClassRef<Any>("net.optifine.CustomColors") {
|
||||
val getColorMultiplier = MethodRef(this, "getColorMultiplier", int, BakedQuad, BlockState, IEnvironmentBlockReader, BlockPos, RenderEnv)
|
||||
}
|
||||
|
||||
// Optifine shaders
|
||||
object SVertexBuilder : ClassRef<Any>("net.optifine.shaders.SVertexBuilder") {
|
||||
val pushState = MethodRef(this, "pushEntity", void, BlockState, BlockPos, IEnvironmentBlockReader, BufferBuilder)
|
||||
val pushNum = MethodRef(this, "pushEntity", void, long)
|
||||
val pop = MethodRef(this, "popEntity", void)
|
||||
}
|
||||
|
||||
|
||||
|
||||
129
src/main/kotlin/mods/octarinecore/Utils.kt
Normal file
129
src/main/kotlin/mods/octarinecore/Utils.kt
Normal file
@@ -0,0 +1,129 @@
|
||||
@file:JvmName("Utils")
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
package mods.octarinecore
|
||||
|
||||
import net.minecraft.tileentity.TileEntity
|
||||
import net.minecraft.util.ResourceLocation
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.world.*
|
||||
import org.apache.logging.log4j.Level
|
||||
import org.apache.logging.log4j.Logger
|
||||
import kotlin.reflect.KProperty
|
||||
import java.lang.Math.*
|
||||
|
||||
const val PI2 = 2.0 * PI
|
||||
|
||||
/** Strip the given prefix off the start of the string, if present */
|
||||
inline fun String.stripStart(str: String, ignoreCase: Boolean = true) = if (startsWith(str, ignoreCase)) substring(str.length) else this
|
||||
inline fun String.stripEnd(str: String, ignoreCase: Boolean = true) = if (endsWith(str, ignoreCase)) substring(0, length - str.length) else this
|
||||
|
||||
/** Strip the given prefix off the start of the resource path, if present */
|
||||
inline fun ResourceLocation.stripStart(str: String) = ResourceLocation(namespace, path.stripStart(str))
|
||||
inline fun ResourceLocation.stripEnd(str: String) = ResourceLocation(namespace, path.stripEnd(str))
|
||||
|
||||
/** Mutating version of _map_. Replace each element of the list with the result of the given transformation. */
|
||||
inline fun <reified T> MutableList<T>.replace(transform: (T) -> T) = forEachIndexed { idx, value -> this[idx] = transform(value) }
|
||||
|
||||
/** Exchange the two elements of the list with the given indices */
|
||||
inline fun <T> MutableList<T>.exchange(idx1: Int, idx2: Int) {
|
||||
val e = this[idx1]
|
||||
this[idx1] = this[idx2]
|
||||
this[idx2] = e
|
||||
}
|
||||
|
||||
/** Cross product of this [Iterable] with the parameter. */
|
||||
fun <A, B> Iterable<A>.cross(other: Iterable<B>) = flatMap { a -> other.map { b -> a to b } }
|
||||
|
||||
inline fun <C, R, T> Iterable<T>.mapAs(transform: (C) -> R) = map { transform(it as C) }
|
||||
|
||||
inline fun <T1, T2> forEachNested(list1: Iterable<T1>, list2: Iterable<T2>, func: (T1, T2)-> Unit) =
|
||||
list1.forEach { e1 ->
|
||||
list2.forEach { e2 ->
|
||||
func(e1, e2)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <K, V> Map<K, V?>.filterValuesNotNull() = filterValues { it != null } as Map<K, V>
|
||||
|
||||
inline fun <reified T, R> Iterable<T>.findFirst(func: (T)->R?): R? {
|
||||
forEach { func(it)?.let { return it } }
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Property-level delegate backed by a [ThreadLocal].
|
||||
*
|
||||
* @param[init] Lambda to get initial value
|
||||
*/
|
||||
class ThreadLocalDelegate<T>(init: () -> T) {
|
||||
var tlVal = ThreadLocal.withInitial(init)
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = tlVal.get()
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { tlVal.set(value) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting with the second element of this [Iterable] until the last, call the supplied lambda with
|
||||
* the parameters (index, element, previous element).
|
||||
*/
|
||||
inline fun <reified T> Iterable<T>.forEachPairIndexed(func: (Int, T, T)->Unit) {
|
||||
var previous: T? = null
|
||||
forEachIndexed { idx, current ->
|
||||
if (previous != null) func(idx, current, previous!!)
|
||||
previous = current
|
||||
}
|
||||
}
|
||||
|
||||
/** Call the supplied lambda and return its result, or the given default value if an exception is thrown. */
|
||||
fun <T> tryDefault(default: T, work: ()->T) = try { work() } catch (e: Throwable) { default }
|
||||
|
||||
/** Return a random [Double] value between the given two limits (inclusive min, exclusive max). */
|
||||
fun random(min: Double, max: Double) = Math.random().let { min + (max - min) * it }
|
||||
|
||||
fun semiRandom(x: Int, y: Int, z: Int, seed: Int): Int {
|
||||
var value = (x * x + y * y + z * z + x * y + y * z + z * x + (seed * seed)) and 63
|
||||
value = (3 * x * value + 5 * y * value + 7 * z * value + (11 * seed)) and 63
|
||||
return value
|
||||
}
|
||||
/**
|
||||
* Return this [Double] value if it lies between the two limits. If outside, return the
|
||||
* minimum/maximum value correspondingly.
|
||||
*/
|
||||
fun Double.minmax(minVal: Double, maxVal: Double) = min(max(this, minVal), maxVal)
|
||||
|
||||
/**
|
||||
* Return this [Int] value if it lies between the two limits. If outside, return the
|
||||
* minimum/maximum value correspondingly.
|
||||
*/
|
||||
fun Int.minmax(minVal: Int, maxVal: Int) = min(max(this, minVal), maxVal)
|
||||
|
||||
fun nextPowerOf2(x: Int): Int {
|
||||
return 1 shl (if (x == 0) 0 else 32 - Integer.numberOfLeadingZeros(x - 1))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Chunk containing the given [BlockPos] is loaded.
|
||||
* Works for both [World] and [ChunkCache] (vanilla and OptiFine) instances.
|
||||
*/
|
||||
//fun IWorldReader.isBlockLoaded(pos: BlockPos) = when {
|
||||
// this is World -> isBlockLoaded(pos, false)
|
||||
// this is RenderChunkCache -> isworld.isBlockLoaded(pos, false)
|
||||
// Refs.OptifineChunkCache.isInstance(this) -> (Refs.CCOFChunkCache.get(this) as ChunkCache).world.isBlockLoaded(pos, false)
|
||||
// else -> false
|
||||
//}
|
||||
|
||||
|
||||
/**
|
||||
* Get the [TileEntity] at the given position, suppressing exceptions.
|
||||
* Also returns null if the chunk is unloaded, which can happen because of multithreaded rendering.
|
||||
*/
|
||||
fun IWorldReader.getTileEntitySafe(pos: BlockPos): TileEntity? = tryDefault(null as TileEntity?) {
|
||||
if (isBlockLoaded(pos)) getTileEntity(pos) else null
|
||||
}
|
||||
|
||||
interface HasLogger {
|
||||
val logger: Logger
|
||||
val logName: String get() = this::class.simpleName!!
|
||||
fun log(msg: String) = log(Level.DEBUG, msg)
|
||||
fun log(level: Level, msg: String) = logger.log(level, "[$logName] $msg")
|
||||
}
|
||||
22
src/main/kotlin/mods/octarinecore/client/KeyHandler.kt
Normal file
22
src/main/kotlin/mods/octarinecore/client/KeyHandler.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package mods.octarinecore.client
|
||||
|
||||
import net.minecraft.client.settings.KeyBinding
|
||||
import net.minecraftforge.client.event.InputEvent
|
||||
import net.minecraftforge.common.MinecraftForge
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent
|
||||
import net.minecraftforge.fml.client.registry.ClientRegistry
|
||||
|
||||
class KeyHandler(val modId: String, val defaultKey: Int, val lang: String, val action: (InputEvent.KeyInputEvent)->Unit) {
|
||||
|
||||
val keyBinding = KeyBinding(lang, defaultKey, modId)
|
||||
|
||||
init {
|
||||
ClientRegistry.registerKeyBinding(keyBinding)
|
||||
MinecraftForge.EVENT_BUS.register(this)
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
fun handleKeyPress(event: InputEvent.KeyInputEvent) {
|
||||
if (keyBinding.isPressed) action(event)
|
||||
}
|
||||
}
|
||||
22
src/main/kotlin/mods/octarinecore/client/gui/Utils.kt
Normal file
22
src/main/kotlin/mods/octarinecore/client/gui/Utils.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
@file:JvmName("Utils")
|
||||
package mods.octarinecore.client.gui
|
||||
|
||||
import net.minecraft.util.text.StringTextComponent
|
||||
import net.minecraft.util.text.Style
|
||||
import net.minecraft.util.text.TextFormatting
|
||||
import net.minecraft.util.text.TextFormatting.AQUA
|
||||
import net.minecraft.util.text.TextFormatting.GRAY
|
||||
|
||||
fun stripTooltipDefaultText(tooltip: MutableList<String>) {
|
||||
var defaultRows = false
|
||||
val iter = tooltip.iterator()
|
||||
while (iter.hasNext()) {
|
||||
if (iter.next().startsWith(AQUA.toString())) defaultRows = true
|
||||
if (defaultRows) iter.remove()
|
||||
}
|
||||
}
|
||||
|
||||
fun textComponent(msg: String, color: TextFormatting = GRAY): StringTextComponent {
|
||||
val style = Style().apply { this.color = color }
|
||||
return StringTextComponent(msg).apply { this.style = style }
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package mods.octarinecore.client.render
|
||||
|
||||
import mods.octarinecore.PI2
|
||||
import mods.octarinecore.common.Double3
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.particle.IParticleRenderType
|
||||
import net.minecraft.client.particle.Particle
|
||||
import net.minecraft.client.particle.SpriteTexturedParticle
|
||||
import net.minecraft.client.renderer.ActiveRenderInfo
|
||||
import net.minecraft.client.renderer.BufferBuilder
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite
|
||||
import net.minecraft.entity.Entity
|
||||
import net.minecraft.world.World
|
||||
|
||||
abstract class AbstractEntityFX(world: World, x: Double, y: Double, z: Double) : SpriteTexturedParticle(world, x, y, z) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic val sin = Array(64) { idx -> Math.sin(PI2 / 64.0 * idx) }
|
||||
@JvmStatic val cos = Array(64) { idx -> Math.cos(PI2 / 64.0 * idx) }
|
||||
}
|
||||
|
||||
val billboardRot = Pair(Double3.zero, Double3.zero)
|
||||
val currentPos = Double3.zero
|
||||
val prevPos = Double3.zero
|
||||
val velocity = Double3.zero
|
||||
|
||||
override fun tick() {
|
||||
super.tick()
|
||||
currentPos.setTo(posX, posY, posZ)
|
||||
prevPos.setTo(prevPosX, prevPosY, prevPosZ)
|
||||
velocity.setTo(motionX, motionY, motionZ)
|
||||
update()
|
||||
posX = currentPos.x; posY = currentPos.y; posZ = currentPos.z;
|
||||
motionX = velocity.x; motionY = velocity.y; motionZ = velocity.z;
|
||||
}
|
||||
|
||||
/** Render the particle. */
|
||||
abstract fun render(worldRenderer: BufferBuilder, partialTickTime: Float)
|
||||
|
||||
/** Update particle on world tick. */
|
||||
abstract fun update()
|
||||
|
||||
/** True if the particle is renderable. */
|
||||
abstract val isValid: Boolean
|
||||
|
||||
/** Add the particle to the effect renderer if it is valid. */
|
||||
fun addIfValid() { if (isValid) Minecraft.getInstance().particles.addEffect(this) }
|
||||
|
||||
override fun renderParticle(buffer: BufferBuilder, entity: ActiveRenderInfo, partialTicks: Float, rotX: Float, rotZ: Float, rotYZ: Float, rotXY: Float, rotXZ: Float) {
|
||||
billboardRot.first.setTo(rotX + rotXY, rotZ, rotYZ + rotXZ)
|
||||
billboardRot.second.setTo(rotX - rotXY, -rotZ, rotYZ - rotXZ)
|
||||
render(buffer, partialTicks)
|
||||
}
|
||||
/**
|
||||
* Render a particle quad.
|
||||
*
|
||||
* @param[tessellator] the [Tessellator] instance to use
|
||||
* @param[partialTickTime] partial tick time
|
||||
* @param[currentPos] render position
|
||||
* @param[prevPos] previous tick position for interpolation
|
||||
* @param[size] particle size
|
||||
* @param[rotation] viewpoint-dependent particle rotation (64 steps)
|
||||
* @param[icon] particle texture
|
||||
* @param[isMirrored] mirror particle texture along V-axis
|
||||
* @param[alpha] aplha blending
|
||||
*/
|
||||
fun renderParticleQuad(worldRenderer: BufferBuilder,
|
||||
partialTickTime: Float,
|
||||
currentPos: Double3 = this.currentPos,
|
||||
prevPos: Double3 = this.prevPos,
|
||||
size: Double = particleScale.toDouble(),
|
||||
rotation: Int = 0,
|
||||
icon: TextureAtlasSprite = sprite,
|
||||
isMirrored: Boolean = false,
|
||||
alpha: Float = this.particleAlpha) {
|
||||
|
||||
val minU = (if (isMirrored) icon.minU else icon.maxU).toDouble()
|
||||
val maxU = (if (isMirrored) icon.maxU else icon.minU).toDouble()
|
||||
val minV = icon.minV.toDouble()
|
||||
val maxV = icon.maxV.toDouble()
|
||||
|
||||
val center = currentPos.copy().sub(prevPos).mul(partialTickTime.toDouble()).add(prevPos).sub(interpPosX, interpPosY, interpPosZ)
|
||||
val v1 = if (rotation == 0) billboardRot.first * size else
|
||||
Double3.weight(billboardRot.first, cos[rotation and 63] * size, billboardRot.second, sin[rotation and 63] * size)
|
||||
val v2 = if (rotation == 0) billboardRot.second * size else
|
||||
Double3.weight(billboardRot.first, -sin[rotation and 63] * size, billboardRot.second, cos[rotation and 63] * size)
|
||||
|
||||
val renderBrightness = this.getBrightnessForRender(partialTickTime)
|
||||
val brLow = renderBrightness shr 16 and 65535
|
||||
val brHigh = renderBrightness and 65535
|
||||
|
||||
worldRenderer
|
||||
.pos(center.x - v1.x, center.y - v1.y, center.z - v1.z)
|
||||
.tex(maxU, maxV)
|
||||
.color(particleRed, particleGreen, particleBlue, alpha)
|
||||
.lightmap(brLow, brHigh)
|
||||
.endVertex()
|
||||
|
||||
worldRenderer
|
||||
.pos(center.x - v2.x, center.y - v2.y, center.z - v2.z)
|
||||
.tex(maxU, minV)
|
||||
.color(particleRed, particleGreen, particleBlue, alpha)
|
||||
.lightmap(brLow, brHigh)
|
||||
.endVertex()
|
||||
|
||||
worldRenderer
|
||||
.pos(center.x + v1.x, center.y + v1.y, center.z + v1.z)
|
||||
.tex(minU, minV)
|
||||
.color(particleRed, particleGreen, particleBlue, alpha)
|
||||
.lightmap(brLow, brHigh)
|
||||
.endVertex()
|
||||
|
||||
worldRenderer
|
||||
.pos(center.x + v2.x, center.y + v2.y, center.z + v2.z)
|
||||
.tex(minU, maxV)
|
||||
.color(particleRed, particleGreen, particleBlue, alpha)
|
||||
.lightmap(brLow, brHigh)
|
||||
.endVertex()
|
||||
}
|
||||
|
||||
// override fun getFXLayer() = 1
|
||||
override fun getRenderType(): IParticleRenderType = IParticleRenderType.PARTICLE_SHEET_TRANSLUCENT
|
||||
|
||||
fun setColor(color: Int) {
|
||||
particleBlue = (color and 255) / 256.0f
|
||||
particleGreen = ((color shr 8) and 255) / 256.0f
|
||||
particleRed = ((color shr 16) and 255) / 256.0f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package mods.octarinecore.client.render
|
||||
|
||||
import mods.octarinecore.common.*
|
||||
import mods.octarinecore.semiRandom
|
||||
import net.minecraft.block.Block
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.client.renderer.BlockRendererDispatcher
|
||||
import net.minecraft.client.renderer.BufferBuilder
|
||||
import net.minecraft.util.BlockRenderLayer
|
||||
import net.minecraft.util.Direction
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.world.IBlockReader
|
||||
import net.minecraft.world.IEnviromentBlockReader
|
||||
import net.minecraft.world.World
|
||||
import net.minecraft.world.biome.Biome
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Represents the block being rendered. Has properties and methods to query the neighborhood of the block in
|
||||
* block-relative coordinates.
|
||||
*/
|
||||
interface BlockCtx {
|
||||
val world: IEnviromentBlockReader
|
||||
val pos: BlockPos
|
||||
|
||||
fun offset(dir: Direction) = offset(dir.offset)
|
||||
fun offset(offset: Int3): BlockCtx
|
||||
|
||||
val state: BlockState get() = world.getBlockState(pos)
|
||||
fun state(dir: Direction) = world.getBlockState(pos + dir.offset)
|
||||
fun state(offset: Int3) = world.getBlockState(pos + offset)
|
||||
|
||||
val biome: Biome get() = world.getBiome(pos)
|
||||
|
||||
val isNormalCube: Boolean get() = state.isNormalCube(world, pos)
|
||||
|
||||
fun shouldSideBeRendered(side: Direction) = Block.shouldSideBeRendered(state, world, pos, side)
|
||||
|
||||
/** Get a semi-random value based on the block coordinate and the given seed. */
|
||||
fun semiRandom(seed: Int) = semiRandom(pos.x, pos.y, pos.z, seed)
|
||||
|
||||
/** Get an array of semi-random values based on the block coordinate. */
|
||||
fun semiRandomArray(num: Int): Array<Int> = Array(num) { semiRandom(it) }
|
||||
}
|
||||
|
||||
open class BasicBlockCtx(
|
||||
override val world: IEnviromentBlockReader,
|
||||
override val pos: BlockPos
|
||||
) : BlockCtx {
|
||||
override var state: BlockState = world.getBlockState(pos)
|
||||
protected set
|
||||
override fun offset(offset: Int3) = BasicBlockCtx(world, pos + offset)
|
||||
fun cache() = CachedBlockCtx(world, pos)
|
||||
}
|
||||
|
||||
open class CachedBlockCtx(world: IEnviromentBlockReader, pos: BlockPos) : BasicBlockCtx(world, pos) {
|
||||
var neighbors = Array<BlockState>(6) { world.getBlockState(pos + allDirections[it].offset) }
|
||||
override var biome: Biome = world.getBiome(pos)
|
||||
override fun state(dir: Direction) = neighbors[dir.ordinal]
|
||||
}
|
||||
|
||||
|
||||
data class RenderCtx(
|
||||
val dispatcher: BlockRendererDispatcher,
|
||||
val renderBuffer: BufferBuilder,
|
||||
val layer: BlockRenderLayer,
|
||||
val random: Random
|
||||
) {
|
||||
fun render(worldBlock: BlockCtx) = dispatcher.renderBlock(worldBlock.state, worldBlock.pos, worldBlock.world, renderBuffer, random, null)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package mods.octarinecore.client.render
|
||||
|
||||
import mods.betterfoliage.client.render.canRenderInCutout
|
||||
import mods.betterfoliage.client.render.isCutout
|
||||
import mods.octarinecore.BufferBuilder
|
||||
import mods.octarinecore.BufferBuilder_setSprite
|
||||
import mods.octarinecore.client.render.lighting.*
|
||||
import mods.octarinecore.common.Double3
|
||||
import mods.octarinecore.common.Int3
|
||||
import mods.octarinecore.common.Rotation
|
||||
import mods.octarinecore.common.plus
|
||||
import mods.octarinecore.metaprog.get
|
||||
import mods.octarinecore.metaprog.set
|
||||
import net.minecraft.block.Blocks
|
||||
import net.minecraft.fluid.Fluids
|
||||
import net.minecraft.util.Direction
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.world.IEnviromentBlockReader
|
||||
import net.minecraft.world.LightType
|
||||
import net.minecraft.world.biome.Biomes
|
||||
|
||||
class CombinedContext(
|
||||
val blockCtx: BlockCtx, val renderCtx: RenderCtx, val lightingCtx: DefaultLightingCtx
|
||||
) : BlockCtx by blockCtx, LightingCtx by lightingCtx {
|
||||
|
||||
var hasRendered = false
|
||||
|
||||
fun render(force: Boolean = false) = renderCtx.let {
|
||||
if (force || state.canRenderInLayer(it.layer) || (state.canRenderInCutout() && it.layer.isCutout)) {
|
||||
it.render(blockCtx)
|
||||
hasRendered = true
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
fun exchange(moddedOffset: Int3, targetOffset: Int3) = CombinedContext(
|
||||
BasicBlockCtx(OffsetEnvBlockReader(blockCtx.world, pos + moddedOffset, pos + targetOffset), pos),
|
||||
renderCtx,
|
||||
lightingCtx
|
||||
)
|
||||
|
||||
val isCutout = renderCtx.layer.isCutout
|
||||
|
||||
/** Get the centerpoint of the block being rendered. */
|
||||
val blockCenter: Double3 get() = Double3(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5)
|
||||
|
||||
/** Holds final vertex data before it goes to the [Tessellator]. */
|
||||
val temp = RenderVertex()
|
||||
|
||||
fun render(
|
||||
model: Model,
|
||||
rotation: Rotation = Rotation.identity,
|
||||
translation: Double3 = blockCenter,
|
||||
forceFlat: Boolean = false,
|
||||
quadFilter: (Int, Quad) -> Boolean = { _, _ -> true },
|
||||
icon: QuadIconResolver,
|
||||
postProcess: PostProcessLambda = noPost
|
||||
) {
|
||||
lightingCtx.modelRotation = rotation
|
||||
model.quads.forEachIndexed { quadIdx, quad ->
|
||||
if (quadFilter(quadIdx, quad)) {
|
||||
val drawIcon = icon(this, quadIdx, quad)
|
||||
if (drawIcon != null) {
|
||||
// let OptiFine know the texture we're using, so it can
|
||||
// transform UV coordinates to quad-relative
|
||||
BufferBuilder_setSprite.invoke(renderCtx.renderBuffer, drawIcon)
|
||||
|
||||
quad.verts.forEachIndexed { vertIdx, vert ->
|
||||
temp.init(vert).rotate(lightingCtx.modelRotation).translate(translation)
|
||||
val shader = if (lightingCtx.aoEnabled && !forceFlat) vert.aoShader else vert.flatShader
|
||||
shader.shade(lightingCtx, temp)
|
||||
temp.postProcess(this, quadIdx, quad, vertIdx, vert)
|
||||
temp.setIcon(drawIcon)
|
||||
|
||||
renderCtx.renderBuffer
|
||||
.pos(temp.x, temp.y, temp.z)
|
||||
.color(temp.red, temp.green, temp.blue, 1.0f)
|
||||
.tex(temp.u, temp.v)
|
||||
.lightmap(temp.brightness shr 16 and 65535, temp.brightness and 65535)
|
||||
.endVertex()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hasRendered = true
|
||||
}
|
||||
}
|
||||
|
||||
val allFaces: (Direction) -> Boolean = { true }
|
||||
val topOnly: (Direction) -> Boolean = { it == Direction.UP }
|
||||
|
||||
/** Perform no post-processing */
|
||||
val noPost: PostProcessLambda = { _, _, _, _, _ -> }
|
||||
|
||||
object NonNullWorld : IEnviromentBlockReader {
|
||||
override fun getBlockState(pos: BlockPos) = Blocks.AIR.defaultState
|
||||
override fun getLightFor(type: LightType, pos: BlockPos) = 0
|
||||
override fun getFluidState(pos: BlockPos) = Fluids.EMPTY.defaultState
|
||||
override fun getTileEntity(pos: BlockPos) = null
|
||||
override fun getBiome(pos: BlockPos) = Biomes.THE_VOID
|
||||
}
|
||||
147
src/main/kotlin/mods/octarinecore/client/render/Model.kt
Normal file
147
src/main/kotlin/mods/octarinecore/client/render/Model.kt
Normal file
@@ -0,0 +1,147 @@
|
||||
package mods.octarinecore.client.render
|
||||
|
||||
import mods.octarinecore.client.render.lighting.*
|
||||
import mods.octarinecore.common.*
|
||||
import mods.octarinecore.minmax
|
||||
import mods.octarinecore.replace
|
||||
import net.minecraft.util.Direction
|
||||
import java.lang.Math.max
|
||||
import java.lang.Math.min
|
||||
|
||||
/**
|
||||
* Vertex UV coordinates
|
||||
*
|
||||
* Zero-centered: coordinates fall between (-0.5, 0.5) (inclusive)
|
||||
*/
|
||||
data class UV(val u: Double, val v: Double) {
|
||||
companion object {
|
||||
val topLeft = UV(-0.5, -0.5)
|
||||
val topRight = UV(0.5, -0.5)
|
||||
val bottomLeft = UV(-0.5, 0.5)
|
||||
val bottomRight = UV(0.5, 0.5)
|
||||
}
|
||||
|
||||
val rotate: UV get() = UV(v, -u)
|
||||
|
||||
fun rotate(n: Int) = when(n % 4) {
|
||||
0 -> copy()
|
||||
1 -> UV(v, -u)
|
||||
2 -> UV(-u, -v)
|
||||
else -> UV(-v, u)
|
||||
}
|
||||
|
||||
fun clamp(minU: Double = -0.5, maxU: Double = 0.5, minV: Double = -0.5, maxV: Double = 0.5) =
|
||||
UV(u.minmax(minU, maxU), v.minmax(minV, maxV))
|
||||
|
||||
fun mirror(mirrorU: Boolean, mirrorV: Boolean) = UV(if (mirrorU) -u else u, if (mirrorV) -v else v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Model vertex
|
||||
*
|
||||
* @param[xyz] x, y, z coordinates
|
||||
* @param[uv] u, v coordinates
|
||||
* @param[aoShader] [ModelLighter] instance to use with AO rendering
|
||||
* @param[flatShader] [ModelLighter] instance to use with non-AO rendering
|
||||
*/
|
||||
data class Vertex(val xyz: Double3 = Double3(0.0, 0.0, 0.0),
|
||||
val uv: UV = UV(0.0, 0.0),
|
||||
val aoShader: ModelLighter = NoLighting,
|
||||
val flatShader: ModelLighter = NoLighting)
|
||||
|
||||
/**
|
||||
* Model quad
|
||||
*/
|
||||
data class Quad(val v1: Vertex, val v2: Vertex, val v3: Vertex, val v4: Vertex) {
|
||||
val verts = arrayOf(v1, v2, v3, v4)
|
||||
inline fun transformV(trans: (Vertex)->Vertex): Quad = transformVI { vertex, idx -> trans(vertex) }
|
||||
inline fun transformVI(trans: (Vertex, Int)->Vertex): Quad =
|
||||
Quad(trans(v1, 0), trans(v2, 1), trans(v3, 2), trans(v4, 3))
|
||||
val normal: Double3 get() = (v2.xyz - v1.xyz).cross(v4.xyz - v1.xyz).normalize
|
||||
|
||||
fun move(trans: Double3) = transformV { it.copy(xyz = it.xyz + trans) }
|
||||
fun move(trans: Pair<Double, Direction>) = move(Double3(trans.second) * trans.first)
|
||||
fun scale (scale: Double) = transformV { it.copy(xyz = it.xyz * scale) }
|
||||
fun scale (scale: Double3) = transformV { it.copy(xyz = Double3(it.xyz.x * scale.x, it.xyz.y * scale.y, it.xyz.z * scale.z)) }
|
||||
fun scaleUV (scale: Double) = transformV { it.copy(uv = UV(it.uv.u * scale, it.uv.v * scale)) }
|
||||
fun rotate(rot: Rotation) = transformV {
|
||||
it.copy(xyz = it.xyz.rotate(rot), aoShader = it.aoShader.rotate(rot), flatShader = it.flatShader.rotate(rot))
|
||||
}
|
||||
fun rotateUV(n: Int) = transformV { it.copy(uv = it.uv.rotate(n)) }
|
||||
fun clampUV(minU: Double = -0.5, maxU: Double = 0.5, minV: Double = -0.5, maxV: Double = 0.5) =
|
||||
transformV { it.copy(uv = it.uv.clamp(minU, maxU, minV, maxV)) }
|
||||
fun mirrorUV(mirrorU: Boolean, mirrorV: Boolean) = transformV { it.copy(uv = it.uv.mirror(mirrorU, mirrorV)) }
|
||||
fun setAoShader(factory: ShaderFactory, predicate: (Vertex, Int)->Boolean = { v, vi -> true }) =
|
||||
transformVI { vertex, idx ->
|
||||
if (!predicate(vertex, idx)) vertex else vertex.copy(aoShader = factory(this@Quad, vertex))
|
||||
}
|
||||
fun setFlatShader(factory: ShaderFactory, predicate: (Vertex, Int)->Boolean = { v, vi -> true }) =
|
||||
transformVI { vertex, idx ->
|
||||
if (!predicate(vertex, idx)) vertex else vertex.copy(flatShader = factory(this@Quad, vertex))
|
||||
}
|
||||
fun setFlatShader(shader: ModelLighter) = transformVI { vertex, idx -> vertex.copy(flatShader = shader) }
|
||||
val flipped: Quad get() = Quad(v4, v3, v2, v1)
|
||||
|
||||
fun cycleVertices(n: Int) = when(n % 4) {
|
||||
1 -> Quad(v2, v3, v4, v1)
|
||||
2 -> Quad(v3, v4, v1, v2)
|
||||
3 -> Quad(v4, v1, v2, v3)
|
||||
else -> this.copy()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Model. The basic unit of rendering blocks with OctarineCore.
|
||||
*
|
||||
* The model should be positioned so that (0,0,0) is the block center.
|
||||
* The block extends to (-0.5, 0.5) in all directions (inclusive).
|
||||
*/
|
||||
class Model() {
|
||||
constructor(other: List<Quad>) : this() { quads.addAll(other) }
|
||||
val quads = mutableListOf<Quad>()
|
||||
|
||||
fun Quad.add() = quads.add(this)
|
||||
fun Iterable<Quad>.addAll() = forEach { quads.add(it) }
|
||||
|
||||
fun transformQ(trans: (Quad)->Quad) = quads.replace(trans)
|
||||
fun transformV(trans: (Vertex)->Vertex) = quads.replace{ it.transformV(trans) }
|
||||
|
||||
fun verticalRectangle(x1: Double, z1: Double, x2: Double, z2: Double, yBottom: Double, yTop: Double) = Quad(
|
||||
Vertex(Double3(x1, yBottom, z1), UV.bottomLeft),
|
||||
Vertex(Double3(x2, yBottom, z2), UV.bottomRight),
|
||||
Vertex(Double3(x2, yTop, z2), UV.topRight),
|
||||
Vertex(Double3(x1, yTop, z1), UV.topLeft)
|
||||
)
|
||||
|
||||
fun horizontalRectangle(x1: Double, z1: Double, x2: Double, z2: Double, y: Double): Quad {
|
||||
val xMin = min(x1, x2); val xMax = max(x1, x2)
|
||||
val zMin = min(z1, z2); val zMax = max(z1, z2)
|
||||
return Quad(
|
||||
Vertex(Double3(xMin, y, zMin), UV.topLeft),
|
||||
Vertex(Double3(xMin, y, zMax), UV.bottomLeft),
|
||||
Vertex(Double3(xMax, y, zMax), UV.bottomRight),
|
||||
Vertex(Double3(xMax, y, zMin), UV.topRight)
|
||||
)
|
||||
}
|
||||
|
||||
fun faceQuad(face: Direction): Quad {
|
||||
val base = face.vec * 0.5
|
||||
val top = boxFaces[face].top * 0.5
|
||||
val left = boxFaces[face].left * 0.5
|
||||
return Quad(
|
||||
Vertex(base + top + left, UV.topLeft),
|
||||
Vertex(base - top + left, UV.bottomLeft),
|
||||
Vertex(base - top - left, UV.bottomRight),
|
||||
Vertex(base + top - left, UV.topRight)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val fullCube = Model().apply {
|
||||
allDirections.forEach {
|
||||
faceQuad(it)
|
||||
.setAoShader(faceOrientedAuto(corner = cornerAo(it.axis), edge = null))
|
||||
.setFlatShader(faceOrientedAuto(corner = cornerFlat, edge = null))
|
||||
.add()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package mods.octarinecore.client.render
|
||||
|
||||
import mods.octarinecore.common.Int3
|
||||
import mods.octarinecore.common.plus
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.world.IBlockReader
|
||||
import net.minecraft.world.IEnviromentBlockReader
|
||||
import net.minecraft.world.LightType
|
||||
|
||||
/**
|
||||
* Delegating [IBlockAccess] that fakes a _modified_ location to return values from a _target_ location.
|
||||
* All other locations are handled normally.
|
||||
*
|
||||
* @param[original] the [IBlockAccess] that is delegated to
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE", "NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", "HasPlatformType")
|
||||
open class OffsetBlockReader(open val original: IBlockReader, val modded: BlockPos, val target: BlockPos) : IBlockReader {
|
||||
inline fun actualPos(pos: BlockPos) = if (pos != null && pos.x == modded.x && pos.y == modded.y && pos.z == modded.z) target else pos
|
||||
|
||||
override fun getBlockState(pos: BlockPos) = original.getBlockState(actualPos(pos))
|
||||
override fun getTileEntity(pos: BlockPos) = original.getTileEntity(actualPos(pos))
|
||||
override fun getFluidState(pos: BlockPos) = original.getFluidState(actualPos(pos))
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE", "NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", "HasPlatformType")
|
||||
class OffsetEnvBlockReader(val original: IEnviromentBlockReader, val modded: BlockPos, val target: BlockPos) : IEnviromentBlockReader by original {
|
||||
inline fun actualPos(pos: BlockPos) = if (pos != null && pos.x == modded.x && pos.y == modded.y && pos.z == modded.z) target else pos
|
||||
|
||||
override fun getBlockState(pos: BlockPos) = original.getBlockState(actualPos(pos))
|
||||
override fun getTileEntity(pos: BlockPos) = original.getTileEntity(actualPos(pos))
|
||||
override fun getFluidState(pos: BlockPos) = original.getFluidState(actualPos(pos))
|
||||
|
||||
override fun getLightFor(type: LightType, pos: BlockPos) = original.getLightFor(type, actualPos(pos))
|
||||
override fun getCombinedLight(pos: BlockPos, light: Int) = original.getCombinedLight(actualPos(pos), light)
|
||||
override fun getBiome(pos: BlockPos) = original.getBiome(actualPos(pos))
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily replaces the [IBlockReader] used by this [BlockContext] and the corresponding [ExtendedRenderBlocks]
|
||||
* to use an [OffsetEnvBlockReader] while executing this lambda.
|
||||
*
|
||||
* @param[modded] the _modified_ location
|
||||
* @param[target] the _target_ location
|
||||
* @param[func] the lambda to execute
|
||||
*/
|
||||
//inline fun <reified T> BlockContext.withOffset(modded: Int3, target: Int3, func: () -> T): T {
|
||||
// val original = reader!!
|
||||
// reader = OffsetEnvBlockReader(original, pos + modded, pos + target)
|
||||
// val result = func()
|
||||
// reader = original
|
||||
// return result
|
||||
//}
|
||||
@@ -0,0 +1,45 @@
|
||||
@file:JvmName("RendererHolder")
|
||||
package mods.octarinecore.client.render
|
||||
|
||||
import mods.betterfoliage.client.render.canRenderInCutout
|
||||
import mods.betterfoliage.client.render.isCutout
|
||||
import mods.octarinecore.ThreadLocalDelegate
|
||||
import mods.octarinecore.client.resource.ResourceHandler
|
||||
import mods.octarinecore.common.Double3
|
||||
import mods.octarinecore.common.Int3
|
||||
import mods.octarinecore.common.allDirOffsets
|
||||
import mods.octarinecore.common.plus
|
||||
import mods.octarinecore.semiRandom
|
||||
import net.minecraft.block.Block
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.renderer.BlockRendererDispatcher
|
||||
import net.minecraft.client.renderer.BufferBuilder
|
||||
import net.minecraft.client.renderer.color.BlockColors
|
||||
import net.minecraft.util.BlockRenderLayer
|
||||
import net.minecraft.util.Direction
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.util.math.MathHelper
|
||||
import net.minecraft.world.IEnviromentBlockReader
|
||||
import net.minecraft.world.biome.Biome
|
||||
import net.minecraftforge.client.model.data.IModelData
|
||||
import net.minecraftforge.eventbus.api.IEventBus
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
abstract class RenderDecorator(modId: String, modBus: IEventBus) : ResourceHandler(modId, modBus) {
|
||||
|
||||
open val renderOnCutout: Boolean get() = true
|
||||
open val onlyOnCutout: Boolean get() = false
|
||||
|
||||
// ============================
|
||||
// Custom rendering
|
||||
// ============================
|
||||
abstract fun isEligible(ctx: CombinedContext): Boolean
|
||||
abstract fun render(ctx: CombinedContext)
|
||||
|
||||
}
|
||||
|
||||
data class BlockData(val state: BlockState, val color: Int, val brightness: Int)
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package mods.octarinecore.client.render.lighting
|
||||
|
||||
import mods.octarinecore.client.render.*
|
||||
import mods.octarinecore.common.*
|
||||
import net.minecraft.util.Direction
|
||||
import net.minecraft.util.Direction.*
|
||||
import java.lang.Math.min
|
||||
|
||||
typealias EdgeShaderFactory = (Direction, Direction) -> ModelLighter
|
||||
typealias CornerShaderFactory = (Direction, Direction, Direction) -> ModelLighter
|
||||
typealias ShaderFactory = (Quad, Vertex) -> ModelLighter
|
||||
|
||||
/** Holds lighting values for block corners as calculated by vanilla Minecraft rendering. */
|
||||
class CornerLightData {
|
||||
var valid = false
|
||||
var brightness = 0
|
||||
var red: Float = 0.0f
|
||||
var green: Float = 0.0f
|
||||
var blue: Float = 0.0f
|
||||
|
||||
fun reset() { valid = false }
|
||||
|
||||
fun set(brightness: Int, red: Float, green: Float, blue: Float) {
|
||||
if (valid) return
|
||||
this.valid = true
|
||||
this.brightness = brightness
|
||||
this.red = red
|
||||
this.green = green
|
||||
this.blue = blue
|
||||
}
|
||||
|
||||
fun set(brightness: Int, colorMultiplier: Float) {
|
||||
this.valid = true
|
||||
this.brightness = brightness
|
||||
this.red = colorMultiplier
|
||||
this.green = colorMultiplier
|
||||
this.blue = colorMultiplier
|
||||
}
|
||||
|
||||
companion object {
|
||||
val black: CornerLightData get() = CornerLightData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instances of this interface are associated with [Model] vertices, and used to apply brightness and color
|
||||
* values to a [RenderVertex].
|
||||
*/
|
||||
interface ModelLighter {
|
||||
/**
|
||||
* Set shading values of a [RenderVertex]
|
||||
*
|
||||
* @param[context] context that can be queried for lighting data in a [Model]-relative frame of reference
|
||||
* @param[vertex] the [RenderVertex] to manipulate
|
||||
*/
|
||||
fun shade(context: LightingCtx, vertex: RenderVertex)
|
||||
|
||||
/**
|
||||
* Return a new rotated version of this [ModelLighter]. Used during [Model] setup when rotating the model itself.
|
||||
*/
|
||||
fun rotate(rot: Rotation): ModelLighter
|
||||
|
||||
/** Set all lighting values on the [RenderVertex] to match the given [CornerLightData]. */
|
||||
fun RenderVertex.shade(shading: CornerLightData) {
|
||||
brightness = shading.brightness; red = shading.red; green = shading.green; blue = shading.blue
|
||||
}
|
||||
|
||||
/** Set the lighting values on the [RenderVertex] to a weighted average of the two [CornerLightData] instances. */
|
||||
fun RenderVertex.shade(shading1: CornerLightData, shading2: CornerLightData, weight1: Float = 0.5f, weight2: Float = 0.5f) {
|
||||
red = min(shading1.red * weight1 + shading2.red * weight2, 1.0f)
|
||||
green = min(shading1.green * weight1 + shading2.green * weight2, 1.0f)
|
||||
blue = min(shading1.blue * weight1 + shading2.blue * weight2, 1.0f)
|
||||
brightness = brWeighted(shading1.brightness, weight1, shading2.brightness, weight2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the lighting values on the [RenderVertex] directly.
|
||||
*
|
||||
* @param[brightness] packed brightness value
|
||||
* @param[color] packed color value
|
||||
*/
|
||||
fun RenderVertex.shade(brightness: Int, color: Int) {
|
||||
this.brightness = brightness; setColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [ModelLighter] resolver for quads that point towards one of the 6 block faces.
|
||||
* The resolver works the following way:
|
||||
* - determines which face the _quad_ normal points towards (if not overridden)
|
||||
* - determines the distance of the _vertex_ to the corners and edge midpoints on that block face
|
||||
* - if _corner_ is given, and the _vertex_ is closest to a block corner, returns the [ModelLighter] created by _corner_
|
||||
* - if _edge_ is given, and the _vertex_ is closest to an edge midpoint, returns the [ModelLighter] created by _edge_
|
||||
*
|
||||
* @param[overrideFace] assume the given face instead of going by the _quad_ normal
|
||||
* @param[corner] [ModelLighter] instantiation lambda for corner vertices
|
||||
* @param[edge] [ModelLighter] instantiation lambda for edge midpoint vertices
|
||||
*/
|
||||
fun faceOrientedAuto(overrideFace: Direction? = null,
|
||||
corner: CornerShaderFactory? = null,
|
||||
edge: EdgeShaderFactory? = null) =
|
||||
fun(quad: Quad, vertex: Vertex): ModelLighter {
|
||||
val quadFace = overrideFace ?: quad.normal.nearestCardinal
|
||||
val nearestCorner = nearestPosition(vertex.xyz, boxFaces[quadFace].allCorners) {
|
||||
(quadFace.vec + it.first.vec + it.second.vec) * 0.5
|
||||
}
|
||||
val nearestEdge = nearestPosition(vertex.xyz, quadFace.perpendiculars) {
|
||||
(quadFace.vec + it.vec) * 0.5
|
||||
}
|
||||
if (edge != null && (nearestEdge.second < nearestCorner.second || corner == null))
|
||||
return edge(quadFace, nearestEdge.first)
|
||||
else return corner!!(quadFace, nearestCorner.first.first, nearestCorner.first.second)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a ModelLighter resolver for quads that point towards one of the 12 block edges.
|
||||
* The resolver works the following way:
|
||||
* - determines which edge the _quad_ normal points towards (if not overridden)
|
||||
* - determines which face midpoint the _vertex_ is closest to, of the 2 block faces that share this edge
|
||||
* - determines which block corner _of this face_ the _vertex_ is closest to
|
||||
* - returns the [ModelLighter] created by _corner_
|
||||
*
|
||||
* @param[overrideEdge] assume the given edge instead of going by the _quad_ normal
|
||||
* @param[corner] ModelLighter instantiation lambda
|
||||
*/
|
||||
fun edgeOrientedAuto(overrideEdge: Pair<Direction, Direction>? = null,
|
||||
corner: CornerShaderFactory) =
|
||||
fun(quad: Quad, vertex: Vertex): ModelLighter {
|
||||
val edgeDir = overrideEdge ?: nearestAngle(quad.normal, boxEdges) { it.first.vec + it.second.vec }.first
|
||||
val nearestFace = nearestPosition(vertex.xyz, edgeDir.toList()) { it.vec }.first
|
||||
val nearestCorner = nearestPosition(vertex.xyz, boxFaces[nearestFace].allCorners) {
|
||||
(nearestFace.vec + it.first.vec + it.second.vec) * 0.5
|
||||
}.first
|
||||
return corner(nearestFace, nearestCorner.first, nearestCorner.second)
|
||||
}
|
||||
|
||||
fun faceOrientedInterpolate(overrideFace: Direction? = null) =
|
||||
fun(quad: Quad, vertex: Vertex): ModelLighter {
|
||||
val resolver = faceOrientedAuto(overrideFace, edge = { face, edgeDir ->
|
||||
val axis = axes.find { it != face.axis && it != edgeDir.axis }!!
|
||||
val vec = Double3((axis to AxisDirection.POSITIVE).face)
|
||||
val pos = vertex.xyz.dot(vec)
|
||||
EdgeInterpolateFallback(face, edgeDir, pos)
|
||||
})
|
||||
return resolver(quad, vertex)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package mods.octarinecore.client.render.lighting
|
||||
|
||||
import mods.octarinecore.client.render.BlockCtx
|
||||
import mods.octarinecore.common.*
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.renderer.BlockModelRenderer
|
||||
import net.minecraft.util.Direction
|
||||
import net.minecraft.world.IEnviromentBlockReader
|
||||
import java.util.*
|
||||
|
||||
val Direction.aoMultiplier: Float get() = when(this) {
|
||||
Direction.UP -> 1.0f
|
||||
Direction.DOWN -> 0.5f
|
||||
Direction.NORTH, Direction.SOUTH -> 0.8f
|
||||
Direction.EAST, Direction.WEST -> 0.6f
|
||||
}
|
||||
|
||||
interface LightingCtx {
|
||||
val modelRotation: Rotation
|
||||
val blockContext: BlockCtx
|
||||
val aoEnabled: Boolean
|
||||
|
||||
val brightness get() = brightness(Int3.zero)
|
||||
val color get() = color(Int3.zero)
|
||||
fun brightness(face: Direction) = brightness(face.offset)
|
||||
fun color(face: Direction) = color(face.offset)
|
||||
|
||||
fun brightness(offset: Int3) = offset.rotate(modelRotation).let {
|
||||
blockContext.state(it).getPackedLightmapCoords(blockContext.world, blockContext.pos + it)
|
||||
}
|
||||
fun color(offset: Int3) = blockContext.offset(offset.rotate(modelRotation)).let { Minecraft.getInstance().blockColors.getColor(it.state, it.world, it.pos, 0) }
|
||||
|
||||
fun lighting(face: Direction, corner1: Direction, corner2: Direction): CornerLightData
|
||||
}
|
||||
|
||||
class DefaultLightingCtx(blockContext: BlockCtx) : LightingCtx {
|
||||
override var modelRotation = Rotation.identity
|
||||
|
||||
override var aoEnabled = false
|
||||
protected set
|
||||
override var blockContext: BlockCtx = blockContext
|
||||
protected set
|
||||
override var brightness = brightness(Int3.zero)
|
||||
protected set
|
||||
override var color = color(Int3.zero)
|
||||
protected set
|
||||
|
||||
override fun brightness(face: Direction) = brightness(face.offset)
|
||||
override fun color(face: Direction) = color(face.offset)
|
||||
|
||||
// smooth lighting stuff
|
||||
val lightingData = Array(6) { FaceLightData(allDirections[it]) }
|
||||
override fun lighting(face: Direction, corner1: Direction, corner2: Direction): CornerLightData = lightingData[face.rotate(modelRotation)].let { faceData ->
|
||||
if (!faceData.isValid) faceData.update(blockContext, faceData.face.aoMultiplier)
|
||||
return faceData[corner1.rotate(modelRotation), corner2.rotate(modelRotation)]
|
||||
}
|
||||
|
||||
fun reset(blockContext: BlockCtx) {
|
||||
this.blockContext = blockContext
|
||||
brightness = brightness(Int3.zero)
|
||||
color = color(Int3.zero)
|
||||
modelRotation = Rotation.identity
|
||||
lightingData.forEach { it.isValid = false }
|
||||
aoEnabled = Minecraft.isAmbientOcclusionEnabled()
|
||||
// allDirections.forEach { lightingData[it].update(blockContext, it.aoMultiplier) }
|
||||
}
|
||||
}
|
||||
|
||||
private val vanillaAOFactory = BlockModelRenderer.AmbientOcclusionFace::class.java.let {
|
||||
it.getDeclaredConstructor(BlockModelRenderer::class.java).apply { isAccessible = true }
|
||||
}.let { ctor -> { ctor.newInstance(Minecraft.getInstance().blockRendererDispatcher.blockModelRenderer) } }
|
||||
|
||||
class FaceLightData(val face: Direction) {
|
||||
val topDir = boxFaces[face].top
|
||||
val leftDir = boxFaces[face].left
|
||||
|
||||
val topLeft = CornerLightData()
|
||||
val topRight = CornerLightData()
|
||||
val bottomLeft = CornerLightData()
|
||||
val bottomRight = CornerLightData()
|
||||
|
||||
val vanillaOrdered = when(face) {
|
||||
Direction.DOWN -> listOf(topLeft, bottomLeft, bottomRight, topRight)
|
||||
Direction.UP -> listOf(bottomRight, topRight, topLeft, bottomLeft)
|
||||
Direction.NORTH -> listOf(bottomLeft, bottomRight, topRight, topLeft)
|
||||
Direction.SOUTH -> listOf(topLeft, bottomLeft, bottomRight, topRight)
|
||||
Direction.WEST -> listOf(bottomLeft, bottomRight, topRight, topLeft)
|
||||
Direction.EAST -> listOf(topRight, topLeft, bottomLeft, bottomRight)
|
||||
}
|
||||
|
||||
val delegate = vanillaAOFactory()
|
||||
var isValid = false
|
||||
|
||||
fun update(blockCtx: BlockCtx, multiplier: Float) {
|
||||
val quadBounds = FloatArray(12)
|
||||
val flags = BitSet(3).apply { set(0) }
|
||||
delegate.updateVertexBrightness(blockCtx.world, blockCtx.state, blockCtx.pos, face, quadBounds, flags)
|
||||
vanillaOrdered.forEachIndexed { idx, corner -> corner.set(delegate.vertexBrightness[idx], delegate.vertexColorMultiplier[idx] * multiplier) }
|
||||
isValid = true
|
||||
}
|
||||
|
||||
operator fun get(dir1: Direction, dir2: Direction): CornerLightData {
|
||||
val isTop = topDir == dir1 || topDir == dir2
|
||||
val isLeft = leftDir == dir1 || leftDir == dir2
|
||||
return if (isTop) {
|
||||
if (isLeft) topLeft else topRight
|
||||
} else {
|
||||
if (isLeft) bottomLeft else bottomRight
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package mods.octarinecore.client.render.lighting
|
||||
|
||||
import mods.octarinecore.common.*
|
||||
import net.minecraft.util.Direction
|
||||
|
||||
|
||||
const val defaultCornerDimming = 0.5f
|
||||
const val defaultEdgeDimming = 0.8f
|
||||
|
||||
// ================================
|
||||
// Shader instantiation lambdas
|
||||
// ================================
|
||||
fun cornerAo(fallbackAxis: Direction.Axis): CornerShaderFactory = { face, dir1, dir2 ->
|
||||
val fallbackDir = listOf(face, dir1, dir2).find { it.axis == fallbackAxis }!!
|
||||
CornerSingleFallback(face, dir1, dir2, fallbackDir)
|
||||
}
|
||||
val cornerFlat = { face: Direction, dir1: Direction, dir2: Direction -> FaceFlat(face) }
|
||||
fun cornerAoTri(func: (CornerLightData, CornerLightData)-> CornerLightData) = { face: Direction, dir1: Direction, dir2: Direction ->
|
||||
CornerTri(face, dir1, dir2, func)
|
||||
}
|
||||
val cornerAoMaxGreen = cornerAoTri { s1, s2 -> if (s1.green > s2.green) s1 else s2 }
|
||||
|
||||
fun cornerInterpolate(edgeAxis: Direction.Axis, weight: Float, dimming: Float): CornerShaderFactory = { dir1, dir2, dir3 ->
|
||||
val edgeDir = listOf(dir1, dir2, dir3).find { it.axis == edgeAxis }!!
|
||||
val faceDirs = listOf(dir1, dir2, dir3).filter { it.axis != edgeAxis }
|
||||
CornerInterpolateDimming(faceDirs[0], faceDirs[1], edgeDir, weight, dimming)
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Shaders
|
||||
// ================================
|
||||
object NoLighting : ModelLighter {
|
||||
override fun shade(context: LightingCtx, vertex: RenderVertex) = vertex.shade(CornerLightData.black)
|
||||
override fun rotate(rot: Rotation) = this
|
||||
}
|
||||
|
||||
class CornerSingleFallback(val face: Direction, val dir1: Direction, val dir2: Direction, val fallbackDir: Direction, val fallbackDimming: Float = defaultCornerDimming) : ModelLighter {
|
||||
val offset = Int3(fallbackDir)
|
||||
override fun shade(context: LightingCtx, vertex: RenderVertex) {
|
||||
val shading = context.lighting(face, dir1, dir2)
|
||||
if (shading.valid)
|
||||
vertex.shade(shading)
|
||||
else {
|
||||
vertex.shade(context.brightness(offset) brMul fallbackDimming, context.color(offset) colorMul fallbackDimming)
|
||||
}
|
||||
}
|
||||
override fun rotate(rot: Rotation) = CornerSingleFallback(face.rotate(rot), dir1.rotate(rot), dir2.rotate(rot), fallbackDir.rotate(rot), fallbackDimming)
|
||||
}
|
||||
|
||||
inline fun accumulate(v1: CornerLightData?, v2: CornerLightData?, func: ((CornerLightData, CornerLightData)-> CornerLightData)): CornerLightData? {
|
||||
val v1ok = v1 != null && v1.valid
|
||||
val v2ok = v2 != null && v2.valid
|
||||
if (v1ok && v2ok) return func(v1!!, v2!!)
|
||||
if (v1ok) return v1
|
||||
if (v2ok) return v2
|
||||
return null
|
||||
}
|
||||
|
||||
class CornerTri(val face: Direction, val dir1: Direction, val dir2: Direction,
|
||||
val func: ((CornerLightData, CornerLightData)-> CornerLightData)) : ModelLighter {
|
||||
override fun shade(context: LightingCtx, vertex: RenderVertex) {
|
||||
var acc = accumulate(
|
||||
context.lighting(face, dir1, dir2),
|
||||
context.lighting(dir1, face, dir2),
|
||||
func)
|
||||
acc = accumulate(
|
||||
acc,
|
||||
context.lighting(dir2, face, dir1),
|
||||
func)
|
||||
vertex.shade(acc ?: CornerLightData.black)
|
||||
}
|
||||
override fun rotate(rot: Rotation) = CornerTri(face.rotate(rot), dir1.rotate(rot), dir2.rotate(rot), func)
|
||||
}
|
||||
|
||||
class EdgeInterpolateFallback(val face: Direction, val edgeDir: Direction, val pos: Double, val fallbackDimming: Float = defaultEdgeDimming): ModelLighter {
|
||||
val offset = Int3(edgeDir)
|
||||
val edgeAxis = axes.find { it != face.axis && it != edgeDir.axis }!!
|
||||
val weightN = (0.5 - pos).toFloat()
|
||||
val weightP = (0.5 + pos).toFloat()
|
||||
|
||||
override fun shade(context: LightingCtx, vertex: RenderVertex) {
|
||||
val shadingP = context.lighting(face, edgeDir, (edgeAxis to Direction.AxisDirection.POSITIVE).face)
|
||||
val shadingN = context.lighting(face, edgeDir, (edgeAxis to Direction.AxisDirection.NEGATIVE).face)
|
||||
if (!shadingP.valid && !shadingN.valid) {
|
||||
return vertex.shade(context.brightness(offset) brMul fallbackDimming, context.color(offset) colorMul fallbackDimming)
|
||||
}
|
||||
if (!shadingP.valid) return vertex.shade(shadingN)
|
||||
if (!shadingN.valid) return vertex.shade(shadingP)
|
||||
vertex.shade(shadingP, shadingN, weightP, weightN)
|
||||
}
|
||||
override fun rotate(rot: Rotation) = EdgeInterpolateFallback(face.rotate(rot), edgeDir.rotate(rot), pos)
|
||||
}
|
||||
|
||||
class CornerInterpolateDimming(val face1: Direction, val face2: Direction, val edgeDir: Direction,
|
||||
val weight: Float, val dimming: Float, val fallbackDimming: Float = defaultCornerDimming) : ModelLighter {
|
||||
val offset = Int3(edgeDir)
|
||||
override fun shade(context: LightingCtx, vertex: RenderVertex) {
|
||||
var shading1 = context.lighting(face1, edgeDir, face2)
|
||||
var shading2 = context.lighting(face2, edgeDir, face1)
|
||||
var weight1 = weight
|
||||
var weight2 = 1.0f - weight
|
||||
if (!shading1.valid && !shading2.valid) {
|
||||
return vertex.shade(context.brightness(offset) brMul fallbackDimming, context.color(offset) colorMul fallbackDimming)
|
||||
}
|
||||
if (!shading1.valid) { shading1 = shading2; weight1 *= dimming }
|
||||
if (!shading2.valid) { shading2 = shading1; weight2 *= dimming }
|
||||
vertex.shade(shading1, shading2, weight1, weight2)
|
||||
}
|
||||
|
||||
override fun rotate(rot: Rotation) =
|
||||
CornerInterpolateDimming(face1.rotate(rot), face2.rotate(rot), edgeDir.rotate(rot), weight, dimming, fallbackDimming)
|
||||
}
|
||||
|
||||
class FaceCenter(val face: Direction): ModelLighter {
|
||||
override fun shade(context: LightingCtx, vertex: RenderVertex) {
|
||||
vertex.red = 0.0f; vertex.green = 0.0f; vertex.blue = 0.0f;
|
||||
val b = IntArray(4)
|
||||
boxFaces[face].allCorners.forEachIndexed { idx, corner ->
|
||||
val shading = context.lighting(face, corner.first, corner.second)
|
||||
vertex.red += shading.red
|
||||
vertex.green += shading.green
|
||||
vertex.blue += shading.blue
|
||||
b[idx] = shading.brightness
|
||||
}
|
||||
vertex.apply { red *= 0.25f; green *= 0.25f; blue *= 0.25f }
|
||||
vertex.brightness = brSum(0.25f, *b)
|
||||
}
|
||||
override fun rotate(rot: Rotation) = FaceCenter(face.rotate(rot))
|
||||
}
|
||||
|
||||
class FaceFlat(val face: Direction): ModelLighter {
|
||||
override fun shade(context: LightingCtx, vertex: RenderVertex) {
|
||||
vertex.shade(context.brightness(face.offset), context.color(Int3.zero))
|
||||
}
|
||||
override fun rotate(rot: Rotation): ModelLighter = FaceFlat(face.rotate(rot))
|
||||
}
|
||||
|
||||
class FlatOffset(val offset: Int3): ModelLighter {
|
||||
override fun shade(context: LightingCtx, vertex: RenderVertex) {
|
||||
vertex.brightness = context.brightness(offset)
|
||||
vertex.setColor(context.color(offset))
|
||||
}
|
||||
override fun rotate(rot: Rotation): ModelLighter = this
|
||||
}
|
||||
|
||||
class FlatOffsetNoColor(val offset: Int3): ModelLighter {
|
||||
override fun shade(context: LightingCtx, vertex: RenderVertex) {
|
||||
vertex.brightness = context.brightness(offset)
|
||||
vertex.red = 1.0f; vertex.green = 1.0f; vertex.blue = 1.0f
|
||||
}
|
||||
override fun rotate(rot: Rotation): ModelLighter = this
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@file:JvmName("PixelFormat")
|
||||
package mods.octarinecore.client.render.lighting
|
||||
|
||||
import java.awt.Color
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package mods.octarinecore.client.render.lighting
|
||||
|
||||
import mods.octarinecore.client.render.CombinedContext
|
||||
import mods.octarinecore.client.render.Quad
|
||||
import mods.octarinecore.client.render.Vertex
|
||||
import mods.octarinecore.common.Double3
|
||||
import mods.octarinecore.common.Rotation
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite
|
||||
import net.minecraft.util.Direction.*
|
||||
import java.awt.Color
|
||||
|
||||
typealias QuadIconResolver = (CombinedContext, Int, Quad) -> TextureAtlasSprite?
|
||||
typealias PostProcessLambda = RenderVertex.(CombinedContext, Int, Quad, Int, Vertex) -> Unit
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
class RenderVertex {
|
||||
var x: Double = 0.0
|
||||
var y: Double = 0.0
|
||||
var z: Double = 0.0
|
||||
var u: Double = 0.0
|
||||
var v: Double = 0.0
|
||||
var brightness: Int = 0
|
||||
var red: Float = 0.0f
|
||||
var green: Float = 0.0f
|
||||
var blue: Float = 0.0f
|
||||
|
||||
val rawData = IntArray(7)
|
||||
|
||||
fun init(vertex: Vertex, rot: Rotation, trans: Double3): RenderVertex {
|
||||
val result = vertex.xyz.rotate(rot) + trans
|
||||
x = result.x; y = result.y; z = result.z
|
||||
return this
|
||||
}
|
||||
fun init(vertex: Vertex): RenderVertex {
|
||||
x = vertex.xyz.x; y = vertex.xyz.y; z = vertex.xyz.z;
|
||||
u = vertex.uv.u; v = vertex.uv.v
|
||||
return this
|
||||
}
|
||||
fun translate(trans: Double3): RenderVertex { x += trans.x; y += trans.y; z += trans.z; return this }
|
||||
fun rotate(rot: Rotation): RenderVertex {
|
||||
if (rot === Rotation.identity) return this
|
||||
val rotX = rot.rotatedComponent(EAST, x, y, z)
|
||||
val rotY = rot.rotatedComponent(UP, x, y, z)
|
||||
val rotZ = rot.rotatedComponent(SOUTH, x, y, z)
|
||||
x = rotX; y = rotY; z = rotZ
|
||||
return this
|
||||
}
|
||||
inline fun rotateUV(n: Int): RenderVertex {
|
||||
when (n % 4) {
|
||||
1 -> { val t = v; v = -u; u = t; return this }
|
||||
2 -> { u = -u; v = -v; return this }
|
||||
3 -> { val t = -v; v = u; u = t; return this }
|
||||
else -> { return this }
|
||||
}
|
||||
}
|
||||
inline fun mirrorUV(mirrorU: Boolean, mirrorV: Boolean) {
|
||||
if (mirrorU) u = -u
|
||||
if (mirrorV) v = -v
|
||||
}
|
||||
inline fun setIcon(icon: TextureAtlasSprite): RenderVertex {
|
||||
u = (icon.maxU - icon.minU) * (u + 0.5) + icon.minU
|
||||
v = (icon.maxV - icon.minV) * (v + 0.5) + icon.minV
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun setGrey(level: Float) {
|
||||
val grey = Math.min((red + green + blue) * 0.333f * level, 1.0f)
|
||||
red = grey; green = grey; blue = grey
|
||||
}
|
||||
inline fun multiplyColor(color: Int) {
|
||||
red *= (color shr 16 and 255) / 256.0f
|
||||
green *= (color shr 8 and 255) / 256.0f
|
||||
blue *= (color and 255) / 256.0f
|
||||
}
|
||||
inline fun setColor(color: Int) {
|
||||
red = (color shr 16 and 255) / 256.0f
|
||||
green = (color shr 8 and 255) / 256.0f
|
||||
blue = (color and 255) / 256.0f
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** List of bit-shift offsets in packed brightness values where meaningful (4-bit) data is contained. */
|
||||
var brightnessComponents = listOf(20, 4)
|
||||
|
||||
/** Multiply the components of this packed brightness value with the given [Float]. */
|
||||
infix fun Int.brMul(f: Float): Int {
|
||||
val weight = (f * 256.0f).toInt()
|
||||
var result = 0
|
||||
brightnessComponents.forEach { shift ->
|
||||
val raw = (this shr shift) and 15
|
||||
val weighted = (raw) * weight / 256
|
||||
result = result or (weighted shl shift)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Multiply the components of this packed color value with the given [Float]. */
|
||||
infix fun Int.colorMul(f: Float): Int {
|
||||
val weight = (f * 256.0f).toInt()
|
||||
val red = (this shr 16 and 255) * weight / 256
|
||||
val green = (this shr 8 and 255) * weight / 256
|
||||
val blue = (this and 255) * weight / 256
|
||||
return (red shl 16) or (green shl 8) or blue
|
||||
}
|
||||
|
||||
/** Sum the components of all packed brightness values given. */
|
||||
fun brSum(multiplier: Float?, vararg brightness: Int): Int {
|
||||
val sum = Array(brightnessComponents.size) { 0 }
|
||||
brightnessComponents.forEachIndexed { idx, shift -> brightness.forEach { br ->
|
||||
val comp = (br shr shift) and 15
|
||||
sum[idx] += comp
|
||||
} }
|
||||
var result = 0
|
||||
brightnessComponents.forEachIndexed { idx, shift ->
|
||||
val comp = if (multiplier == null)
|
||||
((sum[idx]) shl shift)
|
||||
else
|
||||
((sum[idx].toFloat() * multiplier).toInt() shl shift)
|
||||
result = result or comp
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun brWeighted(br1: Int, weight1: Float, br2: Int, weight2: Float): Int {
|
||||
val w1int = (weight1 * 256.0f + 0.5f).toInt()
|
||||
val w2int = (weight2 * 256.0f + 0.5f).toInt()
|
||||
var result = 0
|
||||
brightnessComponents.forEachIndexed { idx, shift ->
|
||||
val comp1 = (br1 shr shift) and 15
|
||||
val comp2 = (br2 shr shift) and 15
|
||||
val compWeighted = (comp1 * w1int + comp2 * w2int) / 256
|
||||
result = result or ((compWeighted and 15) shl shift)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
data class HSB(var hue: Float, var saturation: Float, var brightness: Float) {
|
||||
companion object {
|
||||
fun fromColor(color: Int): HSB {
|
||||
val hsbVals = Color.RGBtoHSB((color shr 16) and 255, (color shr 8) and 255, color and 255, null)
|
||||
return HSB(hsbVals[0], hsbVals[1], hsbVals[2])
|
||||
}
|
||||
}
|
||||
val asColor: Int get() = Color.HSBtoRGB(hue, saturation, brightness)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package mods.octarinecore.client.resource
|
||||
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.betterfoliage.client.resource.Sprite
|
||||
import mods.octarinecore.common.map
|
||||
import net.minecraft.client.renderer.texture.AtlasTexture
|
||||
import net.minecraft.client.renderer.texture.MissingTextureSprite
|
||||
import net.minecraft.profiler.IProfiler
|
||||
import net.minecraft.resources.IResourceManager
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
/**
|
||||
* Main entry point to atlas manipulation. Called from mixins that wrap [AtlasTexture.stitch] calls.
|
||||
*
|
||||
* 1. All registered providers receive an [AsyncSpriteProvider.setup] call. Providers can set up their
|
||||
* processing chain at this point, but should not do anything yet except configuration and housekeeping.
|
||||
* 2. The [CompletableFuture] of the stitch source finishes, starting the "discovery" phase. Providers
|
||||
* may register sprites in the [AtlasFuture].
|
||||
* 3. After all providers finish their discovery, the atlas is stitched.
|
||||
* 4. The [AtlasFuture] finishes, starting the "cleanup" phase. All [AtlasFuture.runAfter] and
|
||||
* [AtlasFuture.mapAfter] tasks are processed.
|
||||
* 5. After all providers finish their cleanup, we return to the original code path.
|
||||
*/
|
||||
class AsnycSpriteProviderManager<SOURCE: Any>(val profilerSection: String) {
|
||||
|
||||
val providers = mutableListOf<AsyncSpriteProvider<SOURCE>>()
|
||||
|
||||
/**
|
||||
* Needed in order to keep the actual [AtlasTexture.stitch] call in the original method, in case
|
||||
* other modders want to modify it too.
|
||||
*/
|
||||
class StitchWrapper(val idList: Iterable<Identifier>, val onComplete: (AtlasTexture.SheetData)->Unit) {
|
||||
fun complete(sheet: AtlasTexture.SheetData) = onComplete(sheet)
|
||||
}
|
||||
|
||||
var currentAtlas: AtlasFuture? = null
|
||||
var currentPhases: List<StitchPhases> = emptyList()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun prepare(sourceObj: Any, manager: IResourceManager, idList: Iterable<Identifier>, profiler: IProfiler): Set<Identifier> {
|
||||
profiler.startSection(profilerSection)
|
||||
|
||||
val source = CompletableFuture<SOURCE>()
|
||||
currentAtlas = AtlasFuture(idList)
|
||||
|
||||
currentPhases = providers.map { it.setup(manager, source, currentAtlas!!) }
|
||||
source.complete(sourceObj as SOURCE)
|
||||
currentPhases.forEach { it.discovery.get() }
|
||||
|
||||
return currentAtlas!!.idSet
|
||||
}
|
||||
|
||||
fun finish(sheetData: AtlasTexture.SheetData, profiler: IProfiler): AtlasTexture.SheetData {
|
||||
currentAtlas!!.complete(sheetData)
|
||||
currentPhases.forEach { it.cleanup.get() }
|
||||
currentAtlas = null
|
||||
currentPhases = emptyList()
|
||||
profiler.endSection()
|
||||
return sheetData
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a way for [AsyncSpriteProvider]s to register sprites to receive [CompletableFuture]s.
|
||||
* Tracks sprite ids that need to be stitched.
|
||||
*/
|
||||
class AtlasFuture(initial: Iterable<Identifier>) {
|
||||
val idSet = Collections.synchronizedSet(mutableSetOf<Identifier>().apply { addAll(initial) })
|
||||
protected val sheet = CompletableFuture<AtlasTexture.SheetData>()
|
||||
protected val finished = CompletableFuture<Void>()
|
||||
|
||||
fun complete(sheetData: AtlasTexture.SheetData) {
|
||||
sheet.complete(sheetData)
|
||||
finished.complete(null)
|
||||
}
|
||||
|
||||
fun sprite(id: String) = sprite(Identifier(id))
|
||||
fun sprite(id: Identifier): CompletableFuture<Sprite> {
|
||||
idSet.add(id)
|
||||
return sheet.map { sheetData -> sheetData.sprites.find { it.name == id } ?: throw IllegalStateException("Atlas does not contain $id") }
|
||||
}
|
||||
val missing = sheet.map { sheetData -> sheetData.sprites.find { it.name == MissingTextureSprite.getLocation() } }
|
||||
fun <T> mapAfter(supplier: ()->T): CompletableFuture<T> = finished.map{ supplier() }
|
||||
fun runAfter(action: ()->Unit): CompletableFuture<Void> = finished.thenRun(action)
|
||||
}
|
||||
|
||||
class StitchPhases(
|
||||
val discovery: CompletableFuture<Void>,
|
||||
val cleanup: CompletableFuture<Void>
|
||||
)
|
||||
|
||||
interface AsyncSpriteProvider<SOURCE: Any> {
|
||||
fun setup(manager: IResourceManager, source: CompletableFuture<SOURCE>, atlas: AtlasFuture): StitchPhases
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package mods.octarinecore.client.resource
|
||||
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.betterfoliage.client.texture.loadSprite
|
||||
import net.minecraft.resources.IResourceManager
|
||||
import java.awt.image.BufferedImage
|
||||
import java.lang.Math.max
|
||||
|
||||
data class CenteredSprite(val sprite: Identifier, val atlas: Atlas = Atlas.BLOCKS, val aspectHeight: Int = 1, val aspectWidth: Int = 1) {
|
||||
|
||||
fun register(pack: GeneratedBlockTexturePack) = pack.register(this, this::draw)
|
||||
|
||||
fun draw(resourceManager: IResourceManager): ByteArray {
|
||||
val baseTexture = resourceManager.loadSprite(atlas.wrap(sprite))
|
||||
|
||||
val frameWidth = baseTexture.width
|
||||
val frameHeight = baseTexture.width * aspectHeight / aspectWidth
|
||||
val frames = baseTexture.height / frameHeight
|
||||
val size = max(frameWidth, frameHeight)
|
||||
|
||||
val resultTexture = BufferedImage(size, size * frames, BufferedImage.TYPE_4BYTE_ABGR)
|
||||
val graphics = resultTexture.createGraphics()
|
||||
|
||||
// iterate all frames
|
||||
for (frame in 0 until frames) {
|
||||
val baseFrame = baseTexture.getSubimage(0, size * frame, frameWidth, frameHeight)
|
||||
val resultFrame = BufferedImage(size, size, BufferedImage.TYPE_4BYTE_ABGR)
|
||||
|
||||
resultFrame.createGraphics().apply {
|
||||
drawImage(baseFrame, (size - frameWidth) / 2, (size - frameHeight) / 2, null)
|
||||
}
|
||||
graphics.drawImage(resultFrame, 0, size * frame, null)
|
||||
}
|
||||
|
||||
return resultTexture.bytes
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package mods.octarinecore.client.resource
|
||||
|
||||
import com.google.common.base.Joiner
|
||||
import mods.betterfoliage.client.resource.ModelIdentifier
|
||||
import mods.octarinecore.HasLogger
|
||||
import mods.octarinecore.client.render.BlockCtx
|
||||
import mods.octarinecore.common.Int3
|
||||
import mods.octarinecore.common.config.IBlockMatcher
|
||||
import mods.octarinecore.common.config.ModelTextureList
|
||||
import mods.octarinecore.common.plus
|
||||
import mods.octarinecore.findFirst
|
||||
import mods.octarinecore.common.sinkAsync
|
||||
import net.minecraft.block.BlockState
|
||||
import net.minecraft.client.renderer.BlockModelShapes
|
||||
import net.minecraft.client.renderer.model.BlockModel
|
||||
import net.minecraft.client.renderer.model.IUnbakedModel
|
||||
import net.minecraft.client.renderer.model.ModelBakery
|
||||
import net.minecraft.client.renderer.model.VariantList
|
||||
import net.minecraft.resources.IResourceManager
|
||||
import net.minecraft.util.ResourceLocation
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.world.IBlockReader
|
||||
import net.minecraftforge.registries.ForgeRegistries
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
interface ModelRenderRegistry<T> {
|
||||
operator fun get(ctx: BlockCtx) = get(ctx.state, ctx.world, ctx.pos)
|
||||
operator fun get(ctx: BlockCtx, offset: Int3) = get(ctx.state(offset), ctx.world, ctx.pos + offset)
|
||||
operator fun get(state: BlockState, world: IBlockReader, pos: BlockPos): T?
|
||||
}
|
||||
|
||||
abstract class ModelRenderRegistryRoot<T> : ModelRenderRegistry<T> {
|
||||
val registries = mutableListOf<ModelRenderRegistry<T>>()
|
||||
override fun get(state: BlockState, world: IBlockReader, pos: BlockPos) = registries.findFirst { it[state, world, pos] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about a single BlockState and all the IUnbakedModel it could render as.
|
||||
*/
|
||||
class ModelDiscoveryContext(
|
||||
bakery: ModelBakery,
|
||||
val state: BlockState,
|
||||
val modelId: ModelIdentifier
|
||||
) {
|
||||
val models = bakery.unwrapVariants(bakery.getUnbakedModel(modelId) to modelId)
|
||||
.filter { it.second != bakery.getUnbakedModel(ModelBakery.MODEL_MISSING) }
|
||||
|
||||
fun ModelBakery.unwrapVariants(modelAndLoc: Pair<IUnbakedModel, ResourceLocation>): List<Pair<IUnbakedModel, ResourceLocation>> = when(val model = modelAndLoc.first) {
|
||||
is VariantList -> model.variantList.flatMap { variant -> unwrapVariants(getUnbakedModel(variant.modelLocation) to variant.modelLocation) }
|
||||
is BlockModel -> listOf(modelAndLoc)
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ModelDiscovery<T> : HasLogger, AsyncSpriteProvider<ModelBakery>, ModelRenderRegistry<T> {
|
||||
|
||||
var modelData: Map<BlockState, T> = emptyMap()
|
||||
protected set
|
||||
|
||||
override fun get(state: BlockState, world: IBlockReader, pos: BlockPos) = modelData[state]
|
||||
|
||||
abstract fun processModel(ctx: ModelDiscoveryContext, atlas: AtlasFuture): CompletableFuture<T>?
|
||||
|
||||
override fun setup(manager: IResourceManager, bakeryF: CompletableFuture<ModelBakery>, atlas: AtlasFuture): StitchPhases {
|
||||
val modelDataTemp = mutableMapOf<BlockState, CompletableFuture<T>>()
|
||||
|
||||
return StitchPhases(
|
||||
discovery = bakeryF.sinkAsync { bakery ->
|
||||
var errors = 0
|
||||
bakery.iterateModels { ctx ->
|
||||
try {
|
||||
processModel(ctx, atlas)?.let { modelDataTemp[ctx.state] = it }
|
||||
} catch (e: Exception) {
|
||||
errors++
|
||||
}
|
||||
}
|
||||
log("${modelDataTemp.size} BlockStates discovered, $errors errors")
|
||||
},
|
||||
cleanup = atlas.runAfter {
|
||||
modelData = modelDataTemp.filterValues { !it.isCompletedExceptionally }.mapValues { it.value.get() }
|
||||
val errors = modelDataTemp.values.filter { it.isCompletedExceptionally }.size
|
||||
log("${modelData.size} BlockStates loaded, $errors errors")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun ModelBakery.iterateModels(func: (ModelDiscoveryContext)->Unit) {
|
||||
ForgeRegistries.BLOCKS.flatMap { block ->
|
||||
block.stateContainer.validStates.map { state -> state to BlockModelShapes.getModelLocation(state) }
|
||||
}.forEach { (state, stateModelResource) ->
|
||||
func(ModelDiscoveryContext(this, state, stateModelResource))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ConfigurableModelDiscovery<T> : ModelDiscovery<T>() {
|
||||
|
||||
abstract val matchClasses: IBlockMatcher
|
||||
abstract val modelTextures: List<ModelTextureList>
|
||||
|
||||
abstract fun processModel(state: BlockState, textures: List<String>, atlas: AtlasFuture): CompletableFuture<T>?
|
||||
|
||||
override fun processModel(ctx: ModelDiscoveryContext, atlas: AtlasFuture): CompletableFuture<T>? {
|
||||
val matchClass = matchClasses.matchingClass(ctx.state.block) ?: return null
|
||||
log("block state ${ctx.state.toString()}")
|
||||
log(" class ${ctx.state.block.javaClass.name} matches ${matchClass.name}")
|
||||
|
||||
if (ctx.models.isEmpty()) {
|
||||
log(" no models found")
|
||||
return null
|
||||
}
|
||||
|
||||
ctx.models.filter { it.first is BlockModel }.forEach { (model, location) ->
|
||||
model as BlockModel
|
||||
val modelMatch = modelTextures.firstOrNull { (model to location).derivesFrom(it.modelLocation) }
|
||||
if (modelMatch != null) {
|
||||
log(" model ${model} matches ${modelMatch.modelLocation}")
|
||||
|
||||
val textures = modelMatch.textureNames.map { it to model.resolveTextureName(it) }
|
||||
val texMapString = Joiner.on(", ").join(textures.map { "${it.first}=${it.second}" })
|
||||
log(" sprites [$texMapString]")
|
||||
|
||||
if (textures.all { it.second != "missingno" }) {
|
||||
// found a valid model (all required textures exist)
|
||||
return processModel(ctx.state, textures.map { it.second}, atlas)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package mods.octarinecore.client.resource
|
||||
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.octarinecore.HasLogger
|
||||
import mods.octarinecore.common.completedVoid
|
||||
import mods.octarinecore.common.map
|
||||
import net.minecraft.client.renderer.model.ModelBakery
|
||||
import net.minecraft.client.resources.ClientResourcePackInfo
|
||||
import net.minecraft.resources.*
|
||||
import net.minecraft.resources.ResourcePackType.CLIENT_RESOURCES
|
||||
import net.minecraft.resources.data.IMetadataSectionSerializer
|
||||
import net.minecraft.util.text.StringTextComponent
|
||||
import org.apache.logging.log4j.Logger
|
||||
import java.io.IOException
|
||||
import java.lang.IllegalStateException
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.function.Predicate
|
||||
import java.util.function.Supplier
|
||||
|
||||
/**
|
||||
* [IResourcePack] containing generated resources
|
||||
*
|
||||
* @param[name] Name of the resource pack
|
||||
* @param[generators] List of resource generators
|
||||
*/
|
||||
class GeneratedBlockTexturePack(val nameSpace: String, val packName: String, override val logger: Logger) : HasLogger, IResourcePack, AsyncSpriteProvider<ModelBakery> {
|
||||
|
||||
override fun getName() = packName
|
||||
override fun getResourceNamespaces(type: ResourcePackType) = setOf(nameSpace)
|
||||
override fun <T : Any?> getMetadata(deserializer: IMetadataSectionSerializer<T>) = null
|
||||
override fun getRootResourceStream(id: String) = null
|
||||
override fun getAllResourceLocations(type: ResourcePackType, path: String, maxDepth: Int, filter: Predicate<String>) = emptyList<Identifier>()
|
||||
override fun close() {}
|
||||
|
||||
protected var manager: CompletableFuture<IResourceManager>? = null
|
||||
val identifiers = Collections.synchronizedMap(mutableMapOf<Any, Identifier>())
|
||||
val resources = Collections.synchronizedMap(mutableMapOf<Identifier, CompletableFuture<ByteArray>>())
|
||||
|
||||
fun register(key: Any, func: (IResourceManager)->ByteArray): Identifier {
|
||||
if (manager == null) throw IllegalStateException("Cannot register resources unless block textures are being reloaded")
|
||||
identifiers[key]?.let { return it }
|
||||
|
||||
val id = Identifier(nameSpace, UUID.randomUUID().toString())
|
||||
val resource = manager!!.map { func(it) }
|
||||
|
||||
identifiers[key] = id
|
||||
resources[Atlas.BLOCKS.wrap(id)] = resource
|
||||
log("generated resource $key -> $id")
|
||||
return id
|
||||
}
|
||||
|
||||
override fun getResourceStream(type: ResourcePackType, id: Identifier) =
|
||||
if (type != CLIENT_RESOURCES) null else
|
||||
try { resources[id]!!.get().inputStream() }
|
||||
catch (e: ExecutionException) { (e.cause as? IOException)?.let { throw it } } // rethrow wrapped IOException if present
|
||||
|
||||
override fun resourceExists(type: ResourcePackType, id: Identifier) =
|
||||
type == CLIENT_RESOURCES && resources.containsKey(id)
|
||||
|
||||
override fun setup(manager: IResourceManager, bakeryF: CompletableFuture<ModelBakery>, atlas: AtlasFuture): StitchPhases {
|
||||
this.manager = CompletableFuture.completedFuture(manager)
|
||||
return StitchPhases(
|
||||
completedVoid(),
|
||||
atlas.runAfter {
|
||||
this.manager = null
|
||||
identifiers.clear()
|
||||
resources.clear()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val finder = object : IPackFinder {
|
||||
val packInfo = ClientResourcePackInfo(
|
||||
packName, true, Supplier { this@GeneratedBlockTexturePack },
|
||||
StringTextComponent(packName),
|
||||
StringTextComponent("Generated block textures resource pack"),
|
||||
PackCompatibility.COMPATIBLE, ResourcePackInfo.Priority.TOP, true, null, true
|
||||
)
|
||||
override fun <T : ResourcePackInfo> addPackInfosToMap(nameToPackMap: MutableMap<String, T>, packInfoFactory: ResourcePackInfo.IFactory<T>) {
|
||||
(nameToPackMap as MutableMap<String, ResourcePackInfo>).put(packName, packInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package mods.octarinecore.client.resource
|
||||
|
||||
import mods.betterfoliage.BetterFoliage
|
||||
import mods.betterfoliage.client.resource.Identifier
|
||||
import mods.betterfoliage.client.resource.Sprite
|
||||
import mods.octarinecore.client.render.Model
|
||||
import mods.octarinecore.common.Double3
|
||||
import mods.octarinecore.common.Int3
|
||||
import mods.octarinecore.common.completedVoid
|
||||
import mods.octarinecore.common.sink
|
||||
import mods.octarinecore.stripEnd
|
||||
import mods.octarinecore.stripStart
|
||||
import net.minecraft.resources.IResourceManager
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.util.math.MathHelper
|
||||
import net.minecraft.world.IWorld
|
||||
import net.minecraft.world.gen.SimplexNoiseGenerator
|
||||
import net.minecraftforge.client.event.TextureStitchEvent
|
||||
import net.minecraftforge.event.world.WorldEvent
|
||||
import net.minecraftforge.eventbus.api.IEventBus
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent
|
||||
import net.minecraftforge.fml.config.ModConfig
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
enum class Atlas(val basePath: String) {
|
||||
BLOCKS("textures"),
|
||||
PARTICLES("textures/particle");
|
||||
|
||||
fun wrap(resource: Identifier) = Identifier(resource.namespace, "$basePath/${resource.path}.png")
|
||||
fun unwrap(resource: Identifier) = resource.stripStart("$basePath/").stripEnd(".png")
|
||||
fun matches(event: TextureStitchEvent) = event.map.basePath == basePath
|
||||
}
|
||||
|
||||
// ============================
|
||||
// Resource types
|
||||
// ============================
|
||||
interface IConfigChangeListener { fun onConfigChange() }
|
||||
interface IWorldLoadListener { fun onWorldLoad(world: IWorld) }
|
||||
|
||||
/**
|
||||
* Base class for declarative resource handling.
|
||||
*
|
||||
* Resources are automatically reloaded/recalculated when the appropriate events are fired.
|
||||
*
|
||||
* @param[modId] mod ID associated with this handler (used to filter config change events)
|
||||
*/
|
||||
open class ResourceHandler(
|
||||
val modId: String,
|
||||
val modBus: IEventBus,
|
||||
val targetAtlas: Atlas = Atlas.BLOCKS
|
||||
) {
|
||||
|
||||
val resources = mutableListOf<Any>()
|
||||
// ============================
|
||||
// Self-registration
|
||||
// ============================
|
||||
init { modBus.register(this) }
|
||||
|
||||
// ============================
|
||||
// Resource declarations
|
||||
// ============================
|
||||
fun sprite(id: Identifier) = sprite { id }
|
||||
fun sprite(idFunc: ()->Identifier) = AsyncSpriteDelegate(idFunc).apply { BetterFoliage.getSpriteManager(targetAtlas).providers.add(this) }
|
||||
fun spriteSet(idFunc: (Int)->Identifier) = AsyncSpriteSet(targetAtlas, idFunc).apply { BetterFoliage.getSpriteManager(targetAtlas).providers.add(this) }
|
||||
fun spriteSetTransformed(check: (Int)->Identifier, register: (Identifier)->Identifier) =
|
||||
AsyncSpriteSet(targetAtlas, check, register).apply { BetterFoliage.getSpriteManager(targetAtlas).providers.add(this) }
|
||||
fun model(init: Model.()->Unit) = ModelHolder(init).apply { resources.add(this) }
|
||||
fun modelSet(num: Int, init: Model.(Int)->Unit) = ModelSet(num, init).apply { resources.add(this) }
|
||||
fun vectorSet(num: Int, init: (Int)-> Double3) = VectorSet(num, init).apply { resources.add(this) }
|
||||
fun simplexNoise() = SimplexNoise().apply { resources.add(this) }
|
||||
|
||||
// ============================
|
||||
// Event registration
|
||||
// ============================
|
||||
@SubscribeEvent
|
||||
fun handleModConfigChange(event: ModConfig.ModConfigEvent) {
|
||||
resources.forEach { (it as? IConfigChangeListener)?.onConfigChange() }
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
fun handleWorldLoad(event: WorldEvent.Load) =
|
||||
resources.forEach { (it as? IWorldLoadListener)?.onWorldLoad(event.world) }
|
||||
}
|
||||
|
||||
// ============================
|
||||
// Resource container classes
|
||||
// ============================
|
||||
class AsyncSpriteDelegate(val idFunc: ()->Identifier) : ReadOnlyProperty<Any, Sprite>, AsyncSpriteProvider<Any> {
|
||||
protected lateinit var value: Sprite
|
||||
override fun getValue(thisRef: Any, property: KProperty<*>) = value
|
||||
|
||||
override fun setup(manager: IResourceManager, sourceF: CompletableFuture<Any>, atlas: AtlasFuture): StitchPhases {
|
||||
sourceF.thenRun {
|
||||
val sprite = atlas.sprite(idFunc())
|
||||
atlas.runAfter {
|
||||
sprite.handle { sprite, error -> value = sprite ?: atlas.missing.get()!! }
|
||||
}
|
||||
}
|
||||
return StitchPhases(completedVoid(), completedVoid())
|
||||
}
|
||||
}
|
||||
|
||||
interface SpriteSet {
|
||||
val num: Int
|
||||
operator fun get(idx: Int): Sprite
|
||||
}
|
||||
|
||||
class AsyncSpriteSet(val targetAtlas: Atlas = Atlas.BLOCKS, val idFunc: (Int)->Identifier, val transform: (Identifier)->Identifier = { it }) : AsyncSpriteProvider<Any> {
|
||||
var num = 0
|
||||
protected set
|
||||
protected var sprites: List<Sprite> = emptyList()
|
||||
|
||||
override fun setup(manager: IResourceManager, sourceF: CompletableFuture<Any>, atlas: AtlasFuture): StitchPhases {
|
||||
var list: List<CompletableFuture<Sprite>> = emptyList()
|
||||
|
||||
return StitchPhases(
|
||||
discovery = sourceF.sink {
|
||||
list = (0 until 16).map { idFunc(it) }
|
||||
.filter { manager.hasResource( targetAtlas.wrap(it)) }
|
||||
.map { transform(it) }
|
||||
.map { atlas.sprite(it) }
|
||||
},
|
||||
cleanup = atlas.runAfter {
|
||||
sprites = list.filter { !it.isCompletedExceptionally }.map { it.get() }
|
||||
if (sprites.isEmpty()) sprites = listOf(atlas.missing.get()!!)
|
||||
num = sprites.size
|
||||
}
|
||||
)
|
||||
}
|
||||
operator fun get(idx: Int) = sprites[idx % num]
|
||||
}
|
||||
|
||||
class ModelHolder(val init: Model.()->Unit): IConfigChangeListener {
|
||||
var model: Model = Model()
|
||||
override fun onConfigChange() { model = Model().apply(init) }
|
||||
}
|
||||
|
||||
class ModelSet(val num: Int, val init: Model.(Int)->Unit): IConfigChangeListener {
|
||||
val models = Array(num) { Model() }
|
||||
override fun onConfigChange() { (0 until num).forEach { models[it] = Model().apply{ init(it) } } }
|
||||
operator fun get(idx: Int) = models[idx % num]
|
||||
}
|
||||
|
||||
class VectorSet(val num: Int, val init: (Int)->Double3): IConfigChangeListener {
|
||||
val models = Array(num) { Double3.zero }
|
||||
override fun onConfigChange() { (0 until num).forEach { models[it] = init(it) } }
|
||||
operator fun get(idx: Int) = models[idx % num]
|
||||
}
|
||||
|
||||
class SimplexNoise : IWorldLoadListener {
|
||||
var noise = SimplexNoiseGenerator(Random())
|
||||
override fun onWorldLoad(world: IWorld) { noise = SimplexNoiseGenerator(Random(world.worldInfo.seed))
|
||||
}
|
||||
operator fun get(x: Int, z: Int) = MathHelper.floor((noise.getValue(x.toDouble(), z.toDouble()) + 1.0) * 32.0)
|
||||
operator fun get(pos: Int3) = get(pos.x, pos.z)
|
||||
operator fun get(pos: BlockPos) = get(pos.x, pos.z)
|
||||
}
|
||||
108
src/main/kotlin/mods/octarinecore/client/resource/Utils.kt
Normal file
108
src/main/kotlin/mods/octarinecore/client/resource/Utils.kt
Normal file
@@ -0,0 +1,108 @@
|
||||
@file:JvmName("Utils")
|
||||
package mods.octarinecore.client.resource
|
||||
|
||||
import mods.octarinecore.PI2
|
||||
import mods.octarinecore.client.render.lighting.HSB
|
||||
import mods.octarinecore.stripStart
|
||||
import mods.octarinecore.tryDefault
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.renderer.model.BlockModel
|
||||
import net.minecraft.client.renderer.texture.AtlasTexture
|
||||
import net.minecraft.client.renderer.texture.MissingTextureSprite
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite
|
||||
import net.minecraft.resources.IResource
|
||||
import net.minecraft.resources.IResourceManager
|
||||
import net.minecraft.resources.SimpleReloadableResourceManager
|
||||
import net.minecraft.util.ResourceLocation
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.lang.Math.*
|
||||
import javax.imageio.ImageIO
|
||||
import kotlin.math.atan2
|
||||
|
||||
/** Concise getter for the Minecraft resource manager. */
|
||||
val resourceManager: SimpleReloadableResourceManager
|
||||
get() =
|
||||
Minecraft.getInstance().resourceManager as SimpleReloadableResourceManager
|
||||
|
||||
/** Append a string to the [ResourceLocation]'s path. */
|
||||
operator fun ResourceLocation.plus(str: String) = ResourceLocation(namespace, path + str)
|
||||
|
||||
/** Index operator to get a resource. */
|
||||
operator fun IResourceManager.get(domain: String, path: String): IResource? = get(ResourceLocation(domain, path))
|
||||
/** Index operator to get a resource. */
|
||||
operator fun IResourceManager.get(location: ResourceLocation): IResource? = tryDefault(null) { getResource(location) }
|
||||
|
||||
/** Index operator to get a texture sprite. */
|
||||
operator fun AtlasTexture.get(res: ResourceLocation): TextureAtlasSprite? = getSprite(res)
|
||||
operator fun AtlasTexture.get(name: String): TextureAtlasSprite? = getSprite(ResourceLocation(name))
|
||||
|
||||
val missingSprite: TextureAtlasSprite get() = MissingTextureSprite.func_217790_a()
|
||||
|
||||
/** Load an image resource. */
|
||||
fun IResource.loadImage(): BufferedImage? = ImageIO.read(this.inputStream)
|
||||
|
||||
/** Get the lines of a text resource. */
|
||||
fun IResource.getLines(): List<String> {
|
||||
val result = arrayListOf<String>()
|
||||
inputStream.bufferedReader().useLines { it.forEach { result.add(it) } }
|
||||
return result
|
||||
}
|
||||
|
||||
/** Index operator to get the RGB value of a pixel. */
|
||||
operator fun BufferedImage.get(x: Int, y: Int) = this.getRGB(x, y)
|
||||
/** Index operator to set the RGB value of a pixel. */
|
||||
operator fun BufferedImage.set(x: Int, y: Int, value: Int) = this.setRGB(x, y, value)
|
||||
|
||||
/** Get an [InputStream] to an image object in PNG format. */
|
||||
val BufferedImage.asStream: InputStream get() =
|
||||
ByteArrayInputStream(ByteArrayOutputStream().let { ImageIO.write(this, "PNG", it); it.toByteArray() })
|
||||
val BufferedImage.bytes: ByteArray get() =
|
||||
ByteArrayOutputStream().let { ImageIO.write(this, "PNG", it); it.toByteArray() }
|
||||
/**
|
||||
* Calculate the average color of a texture.
|
||||
*
|
||||
* Only non-transparent pixels are considered. Averages are taken in the HSB color space (note: Hue is a circular average),
|
||||
* and the result transformed back to the RGB color space.
|
||||
*/
|
||||
val TextureAtlasSprite.averageColor: Int get() {
|
||||
var numOpaque = 0
|
||||
var sumHueX = 0.0
|
||||
var sumHueY = 0.0
|
||||
var sumSaturation = 0.0f
|
||||
var sumBrightness = 0.0f
|
||||
for (x in 0 until width)
|
||||
for (y in 0 until height) {
|
||||
val pixel = getPixelRGBA(0, x, y)
|
||||
val alpha = (pixel shr 24) and 255
|
||||
val hsb = HSB.fromColor(pixel)
|
||||
if (alpha == 255) {
|
||||
numOpaque++
|
||||
sumHueX += cos((hsb.hue.toDouble() - 0.5) * PI2)
|
||||
sumHueY += sin((hsb.hue.toDouble() - 0.5) * PI2)
|
||||
sumSaturation += hsb.saturation
|
||||
sumBrightness += hsb.brightness
|
||||
}
|
||||
}
|
||||
|
||||
// circular average - transform sum vector to polar angle
|
||||
val avgHue = (atan2(sumHueY, sumHueX) / PI2 + 0.5).toFloat()
|
||||
return HSB(avgHue, sumSaturation / numOpaque.toFloat(), sumBrightness / numOpaque.toFloat()).asColor
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual location of a texture from the name of its [TextureAtlasSprite].
|
||||
*/
|
||||
fun textureLocation(iconName: String) = ResourceLocation(iconName).let {
|
||||
if (it.path.startsWith("mcpatcher")) it
|
||||
else ResourceLocation(it.namespace, "textures/${it.path}")
|
||||
}
|
||||
|
||||
fun Pair<BlockModel, ResourceLocation>.derivesFrom(targetLocation: ResourceLocation): Boolean {
|
||||
if (second.stripStart("models/") == targetLocation) return true
|
||||
if (first.parent != null && first.parentLocation != null)
|
||||
return Pair(first.parent!!, first.parentLocation!!).derivesFrom(targetLocation)
|
||||
return false
|
||||
}
|
||||
15
src/main/kotlin/mods/octarinecore/common/Futures.kt
Normal file
15
src/main/kotlin/mods/octarinecore/common/Futures.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package mods.octarinecore.common
|
||||
|
||||
import net.minecraft.client.Minecraft
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.CompletionStage
|
||||
import java.util.function.Consumer
|
||||
import java.util.function.Function
|
||||
|
||||
fun completedVoid() = CompletableFuture.completedFuture<Void>(null)!!
|
||||
|
||||
fun <T, U> CompletionStage<T>.map(func: (T)->U) = thenApply(Function(func)).toCompletableFuture()!!
|
||||
fun <T, U> CompletionStage<T>.mapAsync(func: (T)->U) = thenApplyAsync(Function(func), Minecraft.getInstance()).toCompletableFuture()!!
|
||||
|
||||
fun <T> CompletionStage<T>.sink(func: (T)->Unit) = thenAccept(Consumer(func)).toCompletableFuture()!!
|
||||
fun <T> CompletionStage<T>.sinkAsync(func: (T)->Unit) = thenAcceptAsync(Consumer(func), Minecraft.getInstance()).toCompletableFuture()!!
|
||||
222
src/main/kotlin/mods/octarinecore/common/Geometry.kt
Normal file
222
src/main/kotlin/mods/octarinecore/common/Geometry.kt
Normal file
@@ -0,0 +1,222 @@
|
||||
package mods.octarinecore.common
|
||||
|
||||
import mods.octarinecore.cross
|
||||
import net.minecraft.util.Direction
|
||||
import net.minecraft.util.Direction.*
|
||||
import net.minecraft.util.Direction.Axis.*
|
||||
import net.minecraft.util.Direction.AxisDirection.NEGATIVE
|
||||
import net.minecraft.util.Direction.AxisDirection.POSITIVE
|
||||
import net.minecraft.util.math.BlockPos
|
||||
|
||||
// ================================
|
||||
// Axes and directions
|
||||
// ================================
|
||||
val axes = listOf(X, Y, Z)
|
||||
val axisDirs = listOf(POSITIVE, NEGATIVE)
|
||||
val Direction.dir: AxisDirection get() = axisDirection
|
||||
val AxisDirection.sign: String get() = when(this) { POSITIVE -> "+"; NEGATIVE -> "-" }
|
||||
val allDirections = Direction.values()
|
||||
val horizontalDirections = listOf(NORTH, SOUTH, EAST, WEST)
|
||||
val allDirOffsets = allDirections.map { Int3(it) }
|
||||
val Pair<Axis, AxisDirection>.face: Direction get() = when(this) {
|
||||
X to POSITIVE -> EAST; X to NEGATIVE -> WEST;
|
||||
Y to POSITIVE -> UP; Y to NEGATIVE -> DOWN;
|
||||
Z to POSITIVE -> SOUTH; else -> NORTH;
|
||||
}
|
||||
val Direction.perpendiculars: List<Direction> get() =
|
||||
axes.filter { it != this.axis }.cross(axisDirs).map { it.face }
|
||||
val Direction.offset: Int3 get() = allDirOffsets[ordinal]
|
||||
|
||||
/** Old ForgeDirection rotation matrix yanked from 1.7.10 */
|
||||
val ROTATION_MATRIX: Array<IntArray> get() = arrayOf(
|
||||
intArrayOf(0, 1, 4, 5, 3, 2, 6),
|
||||
intArrayOf(0, 1, 5, 4, 2, 3, 6),
|
||||
intArrayOf(5, 4, 2, 3, 0, 1, 6),
|
||||
intArrayOf(4, 5, 2, 3, 1, 0, 6),
|
||||
intArrayOf(2, 3, 1, 0, 4, 5, 6),
|
||||
intArrayOf(3, 2, 0, 1, 4, 5, 6)
|
||||
)
|
||||
|
||||
// ================================
|
||||
// Vectors
|
||||
// ================================
|
||||
operator fun Direction.times(scale: Double) =
|
||||
Double3(directionVec.x.toDouble() * scale, directionVec.y.toDouble() * scale, directionVec.z.toDouble() * scale)
|
||||
val Direction.vec: Double3 get() = Double3(directionVec.x.toDouble(), directionVec.y.toDouble(), directionVec.z.toDouble())
|
||||
|
||||
operator fun BlockPos.plus(other: Int3) = BlockPos(x + other.x, y + other.y, z + other.z)
|
||||
|
||||
/** 3D vector of [Double]s. Offers both mutable operations, and immutable operations in operator notation. */
|
||||
data class Double3(var x: Double, var y: Double, var z: Double) {
|
||||
constructor(x: Float, y: Float, z: Float) : this(x.toDouble(), y.toDouble(), z.toDouble())
|
||||
constructor(dir: Direction) : this(dir.directionVec.x.toDouble(), dir.directionVec.y.toDouble(), dir.directionVec.z.toDouble())
|
||||
companion object {
|
||||
val zero: Double3 get() = Double3(0.0, 0.0, 0.0)
|
||||
fun weight(v1: Double3, weight1: Double, v2: Double3, weight2: Double) =
|
||||
Double3(v1.x * weight1 + v2.x * weight2, v1.y * weight1 + v2.y * weight2, v1.z * weight1 + v2.z * weight2)
|
||||
}
|
||||
|
||||
// immutable operations
|
||||
operator fun plus(other: Double3) = Double3(x + other.x, y + other.y, z + other.z)
|
||||
operator fun unaryMinus() = Double3(-x, -y, -z)
|
||||
operator fun minus(other: Double3) = Double3(x - other.x, y - other.y, z - other.z)
|
||||
operator fun times(scale: Double) = Double3(x * scale, y * scale, z * scale)
|
||||
operator fun times(other: Double3) = Double3(x * other.x, y * other.y, z * other.z)
|
||||
|
||||
/** Rotate this vector, and return coordinates in the unrotated frame */
|
||||
fun rotate(rot: Rotation) = Double3(
|
||||
rot.rotatedComponent(EAST, x, y, z),
|
||||
rot.rotatedComponent(UP, x, y, z),
|
||||
rot.rotatedComponent(SOUTH, x, y, z)
|
||||
)
|
||||
|
||||
// mutable operations
|
||||
fun setTo(other: Double3): Double3 { x = other.x; y = other.y; z = other.z; return this }
|
||||
fun setTo(x: Double, y: Double, z: Double): Double3 { this.x = x; this.y = y; this.z = z; return this }
|
||||
fun setTo(x: Float, y: Float, z: Float) = setTo(x.toDouble(), y.toDouble(), z.toDouble())
|
||||
fun add(other: Double3): Double3 { x += other.x; y += other.y; z += other.z; return this }
|
||||
fun add(x: Double, y: Double, z: Double): Double3 { this.x += x; this.y += y; this.z += z; return this }
|
||||
fun sub(other: Double3): Double3 { x -= other.x; y -= other.y; z -= other.z; return this }
|
||||
fun sub(x: Double, y: Double, z: Double): Double3 { this.x -= x; this.y -= y; this.z -= z; return this }
|
||||
fun invert(): Double3 { x = -x; y = -y; z = -z; return this }
|
||||
fun mul(scale: Double): Double3 { x *= scale; y *= scale; z *= scale; return this }
|
||||
fun mul(other: Double3): Double3 { x *= other.x; y *= other.y; z *= other.z; return this }
|
||||
fun rotateMut(rot: Rotation): Double3 {
|
||||
val rotX = rot.rotatedComponent(EAST, x, y, z)
|
||||
val rotY = rot.rotatedComponent(UP, x, y, z)
|
||||
val rotZ = rot.rotatedComponent(SOUTH, x, y, z)
|
||||
return setTo(rotX, rotY, rotZ)
|
||||
}
|
||||
|
||||
// misc operations
|
||||
infix fun dot(other: Double3) = x * other.x + y * other.y + z * other.z
|
||||
infix fun cross(o: Double3) = Double3(y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x)
|
||||
val length: Double get() = Math.sqrt(x * x + y * y + z * z)
|
||||
val normalize: Double3 get() = (1.0 / length).let { Double3(x * it, y * it, z * it) }
|
||||
val nearestCardinal: Direction get() = nearestAngle(this, allDirections.asIterable()) { it.vec }.first
|
||||
}
|
||||
|
||||
/** 3D vector of [Int]s. Offers both mutable operations, and immutable operations in operator notation. */
|
||||
data class Int3(var x: Int, var y: Int, var z: Int) {
|
||||
constructor(dir: Direction) : this(dir.directionVec.x, dir.directionVec.y, dir.directionVec.z)
|
||||
constructor(offset: Pair<Int, Direction>) : this(
|
||||
offset.first * offset.second.directionVec.x,
|
||||
offset.first * offset.second.directionVec.y,
|
||||
offset.first * offset.second.directionVec.z
|
||||
)
|
||||
companion object {
|
||||
val zero = Int3(0, 0, 0)
|
||||
}
|
||||
|
||||
// immutable operations
|
||||
operator fun plus(other: Int3) = Int3(x + other.x, y + other.y, z + other.z)
|
||||
operator fun plus(other: Pair<Int, Direction>) = Int3(
|
||||
x + other.first * other.second.directionVec.x,
|
||||
y + other.first * other.second.directionVec.y,
|
||||
z + other.first * other.second.directionVec.z
|
||||
)
|
||||
operator fun unaryMinus() = Int3(-x, -y, -z)
|
||||
operator fun minus(other: Int3) = Int3(x - other.x, y - other.y, z - other.z)
|
||||
operator fun times(scale: Int) = Int3(x * scale, y * scale, z * scale)
|
||||
operator fun times(other: Int3) = Int3(x * other.x, y * other.y, z * other.z)
|
||||
|
||||
/** Rotate this vector, and return coordinates in the unrotated frame */
|
||||
fun rotate(rot: Rotation) = Int3(
|
||||
rot.rotatedComponent(EAST, x, y, z),
|
||||
rot.rotatedComponent(UP, x, y, z),
|
||||
rot.rotatedComponent(SOUTH, x, y, z)
|
||||
)
|
||||
|
||||
// mutable operations
|
||||
fun setTo(other: Int3): Int3 { x = other.x; y = other.y; z = other.z; return this }
|
||||
fun setTo(x: Int, y: Int, z: Int): Int3 { this.x = x; this.y = y; this.z = z; return this }
|
||||
fun add(other: Int3): Int3 { x += other.x; y += other.y; z += other.z; return this }
|
||||
fun sub(other: Int3): Int3 { x -= other.x; y -= other.y; z -= other.z; return this }
|
||||
fun invert(): Int3 { x = -x; y = -y; z = -z; return this }
|
||||
fun mul(scale: Int): Int3 { x *= scale; y *= scale; z *= scale; return this }
|
||||
fun mul(other: Int3): Int3 { x *= other.x; y *= other.y; z *= other.z; return this }
|
||||
fun rotateMut(rot: Rotation): Int3 {
|
||||
val rotX = rot.rotatedComponent(EAST, x, y, z)
|
||||
val rotY = rot.rotatedComponent(UP, x, y, z)
|
||||
val rotZ = rot.rotatedComponent(SOUTH, x, y, z)
|
||||
return setTo(rotX, rotY, rotZ)
|
||||
}
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Rotation
|
||||
// ================================
|
||||
val Direction.rotations: Array<Direction> get() =
|
||||
Array(6) { idx -> Direction.values()[ROTATION_MATRIX[ordinal][idx]] }
|
||||
fun Direction.rotate(rot: Rotation) = rot.forward[ordinal]
|
||||
fun rot(axis: Direction) = Rotation.rot90[axis.ordinal]
|
||||
|
||||
/**
|
||||
* Class representing an arbitrary rotation (or combination of rotations) around cardinal axes by 90 degrees.
|
||||
* In effect, a permutation of [ForgeDirection]s.
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
class Rotation(val forward: Array<Direction>, val reverse: Array<Direction>) {
|
||||
operator fun plus(other: Rotation) = Rotation(
|
||||
Array(6) { idx -> forward[other.forward[idx].ordinal] },
|
||||
Array(6) { idx -> other.reverse[reverse[idx].ordinal] }
|
||||
)
|
||||
operator fun unaryMinus() = Rotation(reverse, forward)
|
||||
operator fun times(num: Int) = when(num % 4) { 1 -> this; 2 -> this + this; 3 -> -this; else -> identity }
|
||||
|
||||
inline fun rotatedComponent(dir: Direction, x: Int, y: Int, z: Int) =
|
||||
when(reverse[dir.ordinal]) { EAST -> x; WEST -> -x; UP -> y; DOWN -> -y; SOUTH -> z; NORTH -> -z; else -> 0 }
|
||||
inline fun rotatedComponent(dir: Direction, x: Double, y: Double, z: Double) =
|
||||
when(reverse[dir.ordinal]) { EAST -> x; WEST -> -x; UP -> y; DOWN -> -y; SOUTH -> z; NORTH -> -z; else -> 0.0 }
|
||||
|
||||
companion object {
|
||||
// Forge rotation matrix is left-hand
|
||||
val rot90 = Array(6) { idx -> Rotation(allDirections[idx].opposite.rotations, allDirections[idx].rotations) }
|
||||
val identity = Rotation(allDirections, allDirections)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Miscellaneous
|
||||
// ================================
|
||||
|
||||
inline operator fun <reified T> Array<T>.get(face: Direction): T = get(face.ordinal)
|
||||
|
||||
data class BoxFace(val top: Direction, val left: Direction) {
|
||||
|
||||
val allCorners = listOf(top to left, top to left.opposite, top.opposite to left, top.opposite to left.opposite)
|
||||
}
|
||||
val boxFaces = allDirections.map { when(it) {
|
||||
DOWN -> BoxFace(SOUTH, WEST)
|
||||
UP -> BoxFace(SOUTH, EAST)
|
||||
NORTH -> BoxFace(WEST, UP)
|
||||
SOUTH -> BoxFace(UP, WEST)
|
||||
WEST -> BoxFace(SOUTH, UP)
|
||||
EAST -> BoxFace(SOUTH, DOWN)
|
||||
}}.toTypedArray()
|
||||
|
||||
/** List of all 12 box edges, represented as a [Pair] of [ForgeDirection]s */
|
||||
val boxEdges = allDirections.flatMap { face1 -> allDirections.filter { it.axis > face1.axis }.map { face1 to it } }
|
||||
|
||||
/**
|
||||
* Get the closest object to the specified point from a list of objects.
|
||||
*
|
||||
* @param[vertex] the reference point
|
||||
* @param[objs] list of geomertric objects
|
||||
* @param[objPos] lambda to calculate the position of an object
|
||||
* @return [Pair] of (object, distance)
|
||||
*/
|
||||
fun <T> nearestPosition(vertex: Double3, objs: Iterable<T>, objPos: (T)-> Double3): Pair<T, Double> =
|
||||
objs.map { it to (objPos(it) - vertex).length }.minBy { it.second }!!
|
||||
|
||||
/**
|
||||
* Get the object closest in orientation to the specified vector from a list of objects.
|
||||
*
|
||||
* @param[vector] the reference vector (direction)
|
||||
* @param[objs] list of geomertric objects
|
||||
* @param[objAngle] lambda to calculate the orientation of an object
|
||||
* @return [Pair] of (object, normalized dot product)
|
||||
*/
|
||||
fun <T> nearestAngle(vector: Double3, objs: Iterable<T>, objAngle: (T)-> Double3): Pair<T, Double> =
|
||||
objs.map { it to objAngle(it).dot(vector) }.maxBy { it.second }!!
|
||||
@@ -0,0 +1,89 @@
|
||||
@file:JvmName("DelegatingConfigKt")
|
||||
|
||||
package mods.octarinecore.common.config
|
||||
|
||||
import mods.octarinecore.metaprog.reflectDelegates
|
||||
import mods.octarinecore.metaprog.reflectNestedObjects
|
||||
import net.minecraftforge.common.ForgeConfigSpec
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
open class DelegatingConfig(val modId: String, val langPrefix: String) {
|
||||
fun build() = ForgeConfigSpec.Builder().apply { ConfigBuildContext(langPrefix, emptyList(), this).addCategory(this@DelegatingConfig) }.build()
|
||||
}
|
||||
|
||||
class ConfigBuildContext(val langPrefix: String, val path: List<String>, val builder: ForgeConfigSpec.Builder) {
|
||||
|
||||
fun addCategory(configObj: Any) {
|
||||
configObj.reflectNestedObjects.forEach { (name, category) ->
|
||||
builder.push(name)
|
||||
descend(name).addCategory(category)
|
||||
builder.pop()
|
||||
}
|
||||
configObj.reflectDelegates(ConfigDelegate::class.java).forEach { (name, delegate) ->
|
||||
descend(name).apply { delegate.addToBuilder(this) }
|
||||
}
|
||||
}
|
||||
|
||||
fun descend(pathName: String) = ConfigBuildContext(langPrefix, path + pathName, builder)
|
||||
}
|
||||
|
||||
open class ConfigCategory(val comment: String? = null) {
|
||||
}
|
||||
|
||||
abstract class ConfigDelegate<T> : ReadOnlyProperty<Any, T> {
|
||||
lateinit var configValue: ForgeConfigSpec.ConfigValue<T>
|
||||
var cachedValue: T? = null
|
||||
|
||||
override fun getValue(thisRef: Any, property: KProperty<*>): T {
|
||||
if (cachedValue == null) cachedValue = configValue.get()
|
||||
return cachedValue!!
|
||||
}
|
||||
|
||||
abstract fun getConfigValue(name: String, builder: ForgeConfigSpec.Builder): ForgeConfigSpec.ConfigValue<T>
|
||||
fun addToBuilder(ctx: ConfigBuildContext) {
|
||||
val langKey = ctx.langPrefix + "." + (langPrefixOverride ?: ctx.path.joinToString("."))
|
||||
ctx.builder.translation(langKey)
|
||||
configValue = getConfigValue(ctx.path.last(), ctx.builder)
|
||||
}
|
||||
|
||||
var langPrefixOverride: String? = null
|
||||
fun lang(prefix: String) = apply { langPrefixOverride = prefix }
|
||||
|
||||
}
|
||||
|
||||
class DelegatingBooleanValue(val defaultValue: Boolean) : ConfigDelegate<Boolean>() {
|
||||
override fun getConfigValue(name: String, builder: ForgeConfigSpec.Builder) = builder.define(name, defaultValue)
|
||||
}
|
||||
|
||||
class DelegatingIntValue(
|
||||
val minValue: Int = 0,
|
||||
val maxValue: Int = 1,
|
||||
val defaultValue: Int = 0
|
||||
) : ConfigDelegate<Int>() {
|
||||
override fun getConfigValue(name: String, builder: ForgeConfigSpec.Builder) = builder.defineInRange(name, defaultValue, minValue, maxValue)
|
||||
}
|
||||
|
||||
class DelegatingLongValue(
|
||||
val minValue: Long = 0,
|
||||
val maxValue: Long = 1,
|
||||
val defaultValue: Long = 0
|
||||
) : ConfigDelegate<Long>() {
|
||||
override fun getConfigValue(name: String, builder: ForgeConfigSpec.Builder) = builder.defineInRange(name, defaultValue, minValue, maxValue)
|
||||
}
|
||||
|
||||
class DelegatingDoubleValue(
|
||||
val minValue: Double = 0.0,
|
||||
val maxValue: Double = 1.0,
|
||||
val defaultValue: Double = 0.0
|
||||
) : ConfigDelegate<Double>() {
|
||||
override fun getConfigValue(name: String, builder: ForgeConfigSpec.Builder) = builder.defineInRange(name, defaultValue, minValue, maxValue)
|
||||
}
|
||||
|
||||
// ============================
|
||||
// Delegate factory methods
|
||||
// ============================
|
||||
fun double(min: Double = 0.0, max: Double = 1.0, default: Double) = DelegatingDoubleValue(min, max, default)
|
||||
fun int(min: Int = 0, max: Int, default: Int) = DelegatingIntValue(min, max, default)
|
||||
fun long(min: Long = 0, max: Long, default: Long) = DelegatingLongValue(min, max, default)
|
||||
fun boolean(default: Boolean) = DelegatingBooleanValue(default)
|
||||
74
src/main/kotlin/mods/octarinecore/common/config/Matchers.kt
Normal file
74
src/main/kotlin/mods/octarinecore/common/config/Matchers.kt
Normal file
@@ -0,0 +1,74 @@
|
||||
package mods.octarinecore.common.config
|
||||
|
||||
import mods.octarinecore.client.resource.getLines
|
||||
import mods.octarinecore.client.resource.resourceManager
|
||||
import mods.octarinecore.metaprog.getJavaClass
|
||||
import net.minecraft.block.Block
|
||||
import net.minecraft.util.ResourceLocation
|
||||
import org.apache.logging.log4j.Logger
|
||||
|
||||
interface IBlockMatcher {
|
||||
fun matchesClass(block: Block): Boolean
|
||||
fun matchingClass(block: Block): Class<*>?
|
||||
}
|
||||
|
||||
class SimpleBlockMatcher(vararg val classes: Class<*>) : IBlockMatcher {
|
||||
override fun matchesClass(block: Block) = matchingClass(block) != null
|
||||
|
||||
override fun matchingClass(block: Block): Class<*>? {
|
||||
val blockClass = block.javaClass
|
||||
classes.forEach { if (it.isAssignableFrom(blockClass)) return it }
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigurableBlockMatcher(val logger: Logger, val location: ResourceLocation) : IBlockMatcher {
|
||||
|
||||
val blackList = mutableListOf<Class<*>>()
|
||||
val whiteList = mutableListOf<Class<*>>()
|
||||
// override fun convertValue(line: String) = getJavaClass(line)
|
||||
|
||||
override fun matchesClass(block: Block): Boolean {
|
||||
val blockClass = block.javaClass
|
||||
blackList.forEach { if (it.isAssignableFrom(blockClass)) return false }
|
||||
whiteList.forEach { if (it.isAssignableFrom(blockClass)) return true }
|
||||
return false
|
||||
}
|
||||
|
||||
override fun matchingClass(block: Block): Class<*>? {
|
||||
val blockClass = block.javaClass
|
||||
blackList.forEach { if (it.isAssignableFrom(blockClass)) return null }
|
||||
whiteList.forEach { if (it.isAssignableFrom(blockClass)) return it }
|
||||
return null
|
||||
}
|
||||
|
||||
fun readDefaults() {
|
||||
blackList.clear()
|
||||
whiteList.clear()
|
||||
resourceManager.getAllResources(location).forEach { resource ->
|
||||
logger.debug("Reading resource $location from pack ${resource.packName}")
|
||||
resource.getLines().map{ it.trim() }.filter { !it.startsWith("//") && it.isNotEmpty() }.forEach { line ->
|
||||
if (line.startsWith("-")) getJavaClass(line.substring(1))?.let { blackList.add(it) }
|
||||
else getJavaClass(line)?.let { whiteList.add(it) }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ModelTextureList(val modelLocation: ResourceLocation, val textureNames: List<String>) {
|
||||
constructor(vararg args: String) : this(ResourceLocation(args[0]), listOf(*args).drop(1))
|
||||
}
|
||||
|
||||
class ModelTextureListConfiguration(val logger: Logger, val location: ResourceLocation) {
|
||||
val modelList = mutableListOf<ModelTextureList>()
|
||||
fun readDefaults() {
|
||||
resourceManager.getAllResources(location).forEach { resource ->
|
||||
logger.debug("Reading resource $location from pack ${resource.packName}")
|
||||
resource.getLines().map{ it.trim() }.filter { !it.startsWith("//") && it.isNotEmpty() }.forEach { line ->
|
||||
val elements = line.split(",")
|
||||
modelList.add(ModelTextureList(ResourceLocation(elements.first()), elements.drop(1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
182
src/main/kotlin/mods/octarinecore/metaprog/Reflection.kt
Normal file
182
src/main/kotlin/mods/octarinecore/metaprog/Reflection.kt
Normal file
@@ -0,0 +1,182 @@
|
||||
@file:JvmName("Reflection")
|
||||
package mods.octarinecore.metaprog
|
||||
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import mods.octarinecore.metaprog.Namespace.*
|
||||
import mods.octarinecore.tryDefault
|
||||
import kotlin.reflect.KCallable
|
||||
import kotlin.reflect.KFunction
|
||||
|
||||
/** Get a Java class with the given name. */
|
||||
fun getJavaClass(name: String) = tryDefault(null) { Class.forName(name) }
|
||||
|
||||
/** Get the field with the given name and type using reflection. */
|
||||
inline fun <reified T> Any.reflectField(field: String): T? =
|
||||
tryDefault(null) { this.javaClass.getDeclaredField(field) }?.let {
|
||||
it.isAccessible = true
|
||||
it.get(this) as T
|
||||
}
|
||||
|
||||
/** Get the static field with the given name and type using reflection. */
|
||||
inline fun <reified T> Class<*>.reflectStaticField(field: String): T? =
|
||||
tryDefault(null) { this.getDeclaredField(field) }?.let {
|
||||
it.isAccessible = true
|
||||
it.get(null) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all nested _object_s of this _object_ with reflection.
|
||||
*
|
||||
* @return [Pair]s of (name, instance)
|
||||
*/
|
||||
val Any.reflectNestedObjects: List<Pair<String, Any>> get() = this.javaClass.declaredClasses.map {
|
||||
tryDefault(null) { it.name.split("$")[1] to it.getField("INSTANCE").get(null) }
|
||||
}.filterNotNull()
|
||||
|
||||
/**
|
||||
* Get all fields of this instance that match (or subclass) any of the given classes.
|
||||
*
|
||||
* @param[types] classes to look for
|
||||
* @return [Pair]s of (field name, instance)
|
||||
*/
|
||||
fun Any.reflectFieldsOfType(vararg types: Class<*>) = this.javaClass.declaredFields
|
||||
.filter { field -> types.any { it.isAssignableFrom(field.type) } }
|
||||
.map { field -> field.name to field.let { it.isAccessible = true; it.get(this) } }
|
||||
.filterNotNull()
|
||||
|
||||
fun <T> Any.reflectFields(type: Class<T>) = this.javaClass.declaredFields
|
||||
.filter { field -> type.isAssignableFrom(field.type) }
|
||||
.map { field -> field.name to field.let { it.isAccessible = true; it.get(this) as T } }
|
||||
|
||||
fun <T> Any.reflectDelegates(type: Class<T>) = this.javaClass.declaredFields
|
||||
.filter { field -> type.isAssignableFrom(field.type) && field.name.contains("$") }
|
||||
.map { field -> field.name.split("$")[0] to field.let { it.isAccessible = true; it.get(this) } as T }
|
||||
|
||||
enum class Namespace { MCP, SRG }
|
||||
|
||||
abstract class Resolvable<T> {
|
||||
abstract fun resolve(): T?
|
||||
val element: T? by lazy { resolve() }
|
||||
}
|
||||
|
||||
/** Return true if all given elements are found. */
|
||||
fun allAvailable(vararg codeElement: Resolvable<*>) = codeElement.all { it.element != null }
|
||||
|
||||
/**
|
||||
* Reference to a class.
|
||||
*
|
||||
* @param[name] MCP name of the class
|
||||
*/
|
||||
open class ClassRef<T: Any?>(val name: String) : Resolvable<Class<T>>() {
|
||||
|
||||
companion object {
|
||||
val int = ClassRefPrimitive("I", Int::class.java)
|
||||
val long = ClassRefPrimitive("J", Long::class.java)
|
||||
val float = ClassRefPrimitive("F", Float::class.java)
|
||||
val boolean = ClassRefPrimitive("Z", Boolean::class.java)
|
||||
val void = ClassRefPrimitive("V", Void::class.java)
|
||||
}
|
||||
|
||||
open fun asmDescriptor(namespace: Namespace) = "L${name.replace(".", "/")};"
|
||||
|
||||
override fun resolve() = getJavaClass(name) as Class<T>?
|
||||
|
||||
fun isInstance(obj: Any) = element?.isInstance(obj) ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to a primitive type.
|
||||
*
|
||||
* @param[name] ASM descriptor of this primitive type
|
||||
* @param[clazz] class of this primitive type
|
||||
*/
|
||||
class ClassRefPrimitive<T>(name: String, val clazz: Class<T>?) : ClassRef<T>(name) {
|
||||
override fun asmDescriptor(namespace: Namespace) = name
|
||||
override fun resolve() = clazz
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to a method.
|
||||
*
|
||||
* @param[parentClass] reference to the class containing the method
|
||||
* @param[mcpName] MCP name of the method
|
||||
* @param[srgName] SRG name of the method
|
||||
* @param[returnType] reference to the return type
|
||||
* @param[returnType] references to the argument types
|
||||
*/
|
||||
class MethodRef<T: Any?>(val parentClass: ClassRef<*>,
|
||||
val mcpName: String,
|
||||
val srgName: String,
|
||||
val returnType: ClassRef<T>,
|
||||
vararg val argTypes: ClassRef<*>
|
||||
) : Resolvable<Method>() {
|
||||
constructor(parentClass: ClassRef<*>, mcpName: String, returnType: ClassRef<T>, vararg argTypes: ClassRef<*>) :
|
||||
this(parentClass, mcpName, mcpName, returnType, *argTypes)
|
||||
|
||||
fun name(namespace: Namespace) = when(namespace) { SRG -> srgName; MCP -> mcpName }
|
||||
fun asmDescriptor(namespace: Namespace) = "(${argTypes.map { it.asmDescriptor(namespace) }.fold(""){ s1, s2 -> s1 + s2 } })${returnType.asmDescriptor(namespace)}"
|
||||
|
||||
override fun resolve(): Method? =
|
||||
if (parentClass.element == null || argTypes.any { it.element == null }) null
|
||||
else {
|
||||
val args = argTypes.map { it.element!! }.toTypedArray()
|
||||
listOf(srgName, mcpName).map { tryDefault(null) {
|
||||
parentClass.element!!.getDeclaredMethod(it, *args)
|
||||
}}.filterNotNull().firstOrNull()
|
||||
?.apply { isAccessible = true }
|
||||
}
|
||||
|
||||
/** Invoke this method using reflection. */
|
||||
operator fun invoke(receiver: Any, vararg args: Any?) = element?.invoke(receiver, *args) as T
|
||||
|
||||
/** Invoke this static method using reflection. */
|
||||
fun invokeStatic(vararg args: Any) = element?.invoke(null, *args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to a field.
|
||||
*
|
||||
* @param[parentClass] reference to the class containing the field
|
||||
* @param[mcpName] MCP name of the field
|
||||
* @param[srgName] SRG name of the field
|
||||
* @param[type] reference to the field type
|
||||
*/
|
||||
class FieldRef<T>(val parentClass: ClassRef<*>,
|
||||
val mcpName: String,
|
||||
val srgName: String,
|
||||
val type: ClassRef<T>
|
||||
) : Resolvable<Field>() {
|
||||
constructor(parentClass: ClassRef<*>, mcpName: String, type: ClassRef<T>) : this(parentClass, mcpName, mcpName, type)
|
||||
|
||||
fun name(namespace: Namespace) = when(namespace) { SRG -> srgName; MCP -> mcpName }
|
||||
fun asmDescriptor(namespace: Namespace) = type.asmDescriptor(namespace)
|
||||
|
||||
override fun resolve(): Field? =
|
||||
if (parentClass.element == null) null
|
||||
else {
|
||||
listOf(srgName, mcpName).mapNotNull { tryDefault(null) {
|
||||
parentClass.element!!.getDeclaredField(it)
|
||||
} }.firstOrNull()
|
||||
?.apply{ isAccessible = true }
|
||||
}
|
||||
|
||||
/** Get this field using reflection. */
|
||||
operator fun get(receiver: Any?) = element?.get(receiver) as T
|
||||
|
||||
/** Get this static field using reflection. */
|
||||
fun getStatic() = get(null)
|
||||
|
||||
fun set(receiver: Any?, obj: Any?) { element?.set(receiver, obj) }
|
||||
}
|
||||
|
||||
|
||||
fun Any.isInstance(cls: ClassRef<*>) = cls.isInstance(this)
|
||||
interface ReflectionCallable<T> {
|
||||
operator fun invoke(vararg args: Any): T
|
||||
}
|
||||
inline operator fun <reified T> Any.get(field: FieldRef<T>) = field.get(this)
|
||||
inline operator fun <reified T> Any.set(field: FieldRef<T>, value: T) = field.set(this, value)
|
||||
inline operator fun <T> Any.get(methodRef: MethodRef<T>) = object : ReflectionCallable<T> {
|
||||
override fun invoke(vararg args: Any) = methodRef.invoke(this@get, args)
|
||||
}
|
||||
6
src/main/resources/META-INF/MANIFEST.MF
Normal file
6
src/main/resources/META-INF/MANIFEST.MF
Normal file
@@ -0,0 +1,6 @@
|
||||
Manifest-Version: 1.0
|
||||
Specification-Title: BetterFoliage
|
||||
Implementation-Title: BetterFoliage
|
||||
Specification-Vendor: octarine-noise
|
||||
Implementation-Vendor: octarine-noise
|
||||
MixinConnector: mods.betterfoliage.MixinConnector
|
||||
10
src/main/resources/META-INF/accesstransformer.cfg
Normal file
10
src/main/resources/META-INF/accesstransformer.cfg
Normal file
@@ -0,0 +1,10 @@
|
||||
public net.minecraft.client.renderer.BlockModelRenderer$AmbientOcclusionFace
|
||||
public net.minecraft.client.renderer.BlockModelRenderer$AmbientOcclusionFace <init>
|
||||
public net.minecraft.client.renderer.BlockModelRenderer$AmbientOcclusionFace field_178206_b #vertexColorMultiplier
|
||||
public net.minecraft.client.renderer.BlockModelRenderer$AmbientOcclusionFace field_178207_c #vertexBrightness
|
||||
|
||||
public net.minecraft.block.BlockState$Cache
|
||||
|
||||
public net.minecraft.client.renderer.chunk.ChunkRenderCache field_212408_i #world
|
||||
|
||||
public net.minecraft.client.renderer.texture.AtlasTexture$SheetData field_217808_d # sprites
|
||||
14
src/main/resources/META-INF/mods.toml
Normal file
14
src/main/resources/META-INF/mods.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
modLoader="kotlinfml"
|
||||
loaderVersion="[1.4,)"
|
||||
issueTrackerURL="https://github.com/octarine-noise/BetterFoliage/issues"
|
||||
|
||||
[[mods]]
|
||||
modId="betterfoliage"
|
||||
version="${version}"
|
||||
displayName="Better Foliage"
|
||||
authors="octarine-noise"
|
||||
description='''
|
||||
Leafier leaves and grassier grass.
|
||||
'''
|
||||
displayURL="https://www.curseforge.com/minecraft/mc-mods/better-foliage"
|
||||
side="CLIENT"
|
||||
15
src/main/resources/assets/betterfoliage/crop_default.cfg
Normal file
15
src/main/resources/assets/betterfoliage/crop_default.cfg
Normal file
@@ -0,0 +1,15 @@
|
||||
// Vanilla
|
||||
net.minecraft.block.TallGrassBlock
|
||||
net.minecraft.block.CropsBlock
|
||||
-net.minecraft.block.ReedBlock
|
||||
-net.minecraft.block.DoublePlantBlock
|
||||
-net.minecraft.block.CarrotBlock
|
||||
-net.minecraft.block.PotatoBlock
|
||||
|
||||
// Biomes O'Plenty
|
||||
biomesoplenty.common.block.BlockBOPFlower
|
||||
biomesoplenty.common.block.BlockBOPTurnip
|
||||
biomesoplenty.common.block.BlockBOPPlant
|
||||
|
||||
// Tinkers' Construct
|
||||
tconstruct.blocks.slime.SlimeTallGrass
|
||||
2
src/main/resources/assets/betterfoliage/dirt_default.cfg
Normal file
2
src/main/resources/assets/betterfoliage/dirt_default.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
// Vanilla
|
||||
net.minecraft.block.DirtBlock
|
||||
@@ -0,0 +1,2 @@
|
||||
// Vanilla
|
||||
net.minecraft.block.GrassBlock
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vanilla
|
||||
block/grass_block,top
|
||||
block/cube_bottom_top,top
|
||||
250
src/main/resources/assets/betterfoliage/lang/en_us.lang
Normal file
250
src/main/resources/assets/betterfoliage/lang/en_us.lang
Normal file
@@ -0,0 +1,250 @@
|
||||
key.betterfoliage.gui=Open Settings
|
||||
|
||||
betterfoliage.global.enabled=Enable Mod
|
||||
betterfoliage.global.enabled.tooltip=If set to false, BetterFoliage will not render anything
|
||||
betterfoliage.global.nVidia=nVidia GPU
|
||||
betterfoliage.global.nVidia.tooltip=Specify whether you have an nVidia GPU
|
||||
|
||||
betterfoliage.enabled=Enable
|
||||
betterfoliage.enabled.tooltip=Is this feature enabled?
|
||||
betterfoliage.hOffset=Horizontal offset
|
||||
betterfoliage.hOffset.tooltip=The distance this element is shifted horizontally, in blocks
|
||||
betterfoliage.vOffset=Vertical offset
|
||||
betterfoliage.vOffset.tooltip=The distance this element is shifted vertically, in blocks
|
||||
betterfoliage.size=Size
|
||||
betterfoliage.size.tooltip=Size of this element
|
||||
betterfoliage.heightMin=Minimum height
|
||||
betterfoliage.heightMin.tooltip=Minimum height of element
|
||||
betterfoliage.heightMax=Maximum height
|
||||
betterfoliage.heightMax.tooltip=Maximum height of element
|
||||
betterfoliage.population=Population
|
||||
betterfoliage.population.tooltip=Chance (N in 64) that an eligible block will have this feature
|
||||
betterfoliage.shaderWind=Shader wind effects
|
||||
betterfoliage.shaderWind.tooltip=Apply wind effects from ShaderMod shaders to this element?
|
||||
betterfoliage.distance=Distance limit
|
||||
betterfoliage.distance.tooltip=Maximum distance from player at which to render this feature
|
||||
|
||||
betterfoliage.rendererror=§a[BetterFoliage]§f Error rendering block %s at position %s
|
||||
|
||||
betterfoliage.blocks=Block Types
|
||||
betterfoliage.blocks.tooltip=Configure lists of block classes that will have specific features applied to them
|
||||
|
||||
betterfoliage.blocks.dirtWhitelist=Dirt Whitelist
|
||||
betterfoliage.blocks.dirtBlacklist=Dirt Blacklist
|
||||
betterfoliage.blocks.dirtWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.dirtBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.dirtWhitelist.tooltip=Blocks recognized as Dirt. Has an impact on Reeds, Algae, Connected Grass
|
||||
betterfoliage.blocks.dirtBlacklist.tooltip=Blocks never accepted as Dirt. Has an impact on Reeds, Algae, Connected Grass
|
||||
|
||||
betterfoliage.blocks.grassClassesWhitelist=Grass Whitelist
|
||||
betterfoliage.blocks.grassClassesBlacklist=Grass Blacklist
|
||||
betterfoliage.blocks.grassClassesWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.grassClassesBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.grassModels=Grass Models
|
||||
betterfoliage.blocks.grassModels.arrayEntry=%d entries
|
||||
betterfoliage.blocks.grassWhitelist.tooltip=Blocks recognized as Grass. Has an impact on Short Grass, Connected Grass
|
||||
betterfoliage.blocks.grassBlacklist.tooltip=Blocks never accepted as Grass. Has an impact on Short Grass, Connected Grass
|
||||
betterfoliage.blocks.grassModels.tooltip=Models and textures recognized for grass blocks
|
||||
|
||||
betterfoliage.blocks.leavesClassesWhitelist=Leaves Whitelist
|
||||
betterfoliage.blocks.leavesClassesBlacklist=Leaves Blacklist
|
||||
betterfoliage.blocks.leavesClassesWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.leavesClassesBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.leavesModels=Leaves Models
|
||||
betterfoliage.blocks.leavesModels.arrayEntry=%d entries
|
||||
betterfoliage.blocks.leavesClassesWhitelist.tooltip=Blocks recognized as Leaves. Has an impact on Extra Leaves, Falling Leaves. Leaves will render with leaves block ID in shader programs
|
||||
betterfoliage.blocks.leavesClassesBlacklist.tooltip=Blocks never accepted as Leaves. Has an impact on Extra Leaves, Falling Leaves. Leaves will render with leaves block ID in shader programs
|
||||
betterfoliage.blocks.leavesModels.tooltip=Models and textures recognized for leaves blocks
|
||||
|
||||
betterfoliage.blocks.cropsWhitelist=Crop Whitelist
|
||||
betterfoliage.blocks.cropsBlacklist=Crop Blacklist
|
||||
betterfoliage.blocks.cropsWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.cropsBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.cropsWhitelist.tooltip=Blocks recognized as crops. Crops will render with tallgrass block ID in shader programs
|
||||
betterfoliage.blocks.cropsBlacklist.tooltip=Blocks never accepted as crops. Crops will render with tallgrass block ID in shader programs
|
||||
|
||||
betterfoliage.blocks.logClassesWhitelist=Wood Log Whitelist
|
||||
betterfoliage.blocks.logClassesBlacklist=Wood Log Blacklist
|
||||
betterfoliage.blocks.logClassesWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.logClassesBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.logModels=Wood Log Models
|
||||
betterfoliage.blocks.logModels.arrayEntry=%d entries
|
||||
betterfoliage.blocks.logClassesWhitelist.tooltip=Blocks recognized as wooden logs. Has an impact on Rounded Logs
|
||||
betterfoliage.blocks.logClassesBlacklist.tooltip=Blocks never accepted as wooden logs. Has an impact on Rounded Logs
|
||||
betterfoliage.blocks.logModels.tooltip=Models and textures recognized for wood log blocks
|
||||
|
||||
betterfoliage.blocks.sandWhitelist=Sand Whitelist
|
||||
betterfoliage.blocks.sandBlacklist=Sand Blacklist
|
||||
betterfoliage.blocks.sandWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.sandBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.sandWhitelist.tooltip=Blocks recognized as Sand. Has an impact on Coral
|
||||
betterfoliage.blocks.sandBlacklist.tooltip=Blocks never accepted Sand. Has an impact on Coral
|
||||
|
||||
betterfoliage.blocks.lilypadWhitelist=Lilypad Whitelist
|
||||
betterfoliage.blocks.lilypadBlacklist=Lilypad Blacklist
|
||||
betterfoliage.blocks.lilypadWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.lilypadBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.lilypadWhitelist.tooltip=Blocks recognized as Lilypad. Has an impact on Better Lilypad
|
||||
betterfoliage.blocks.lilypadBlacklist.tooltip=Blocks never accepted Lilypad. Has an impact on Better Lilypad
|
||||
|
||||
betterfoliage.blocks.cactusWhitelist=Cactus Whitelist
|
||||
betterfoliage.blocks.cactusBlacklist=Cactus Blacklist
|
||||
betterfoliage.blocks.cactusWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.cactusBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.cactusWhitelist.tooltip=Blocks recognized as Cactus. Has an impact on Better Cactus
|
||||
betterfoliage.blocks.cactusBlacklist.tooltip=Blocks never accepted Cactus. Has an impact on Better Cactus
|
||||
|
||||
betterfoliage.blocks.myceliumWhitelist=Mycelium Whitelist
|
||||
betterfoliage.blocks.myceliumBlacklist=Mycelium Blacklist
|
||||
betterfoliage.blocks.myceliumWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.myceliumBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.myceliumWhitelist.tooltip=Blocks recognized as Mycelium. Has an impact on Better Grass
|
||||
betterfoliage.blocks.myceliumBlacklist.tooltip=Blocks never accepted Mycelium. Has an impact on Better Grass
|
||||
|
||||
betterfoliage.blocks.netherrackWhitelist=Netherrack Whitelist
|
||||
betterfoliage.blocks.netherrackBlacklist=Netherrack Blacklist
|
||||
betterfoliage.blocks.netherrackWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.netherrackBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.netherrackWhitelist.tooltip=Blocks recognized as Netherrack. Has an impact on Netherrack Vines
|
||||
betterfoliage.blocks.netherrackBlacklist.tooltip=Blocks never accepted Netherrack. Has an impact on Netherrack Vines
|
||||
|
||||
betterfoliage.shaders=Shader configuration
|
||||
betterfoliage.shaders.tooltip=Configure integration with shaders
|
||||
betterfoliage.shaders.leavesId=Leaves ID
|
||||
betterfoliage.shaders.leavesId.tooltip=Block ID reported to shader programs for all kinds of leaves. If your shader uses a §6block.properties§e file, you'll probably need to change this to match the shader's mappings.
|
||||
betterfoliage.shaders.grassId=Grass ID
|
||||
betterfoliage.shaders.grassId.tooltip=Block ID reported to shader programs for all grasses and crops. If your shader uses a §6block.properties§e file, you'll probably need to change this to match the shader's mappings.
|
||||
|
||||
betterfoliage.leaves=Extra Leaves
|
||||
betterfoliage.leaves.tooltip=Extra round leaves on leaf blocks
|
||||
betterfoliage.leaves.dense=Dense mode
|
||||
betterfoliage.leaves.dense.tooltip=Dense mode has more round leaves
|
||||
betterfoliage.leaves.snowEnabled=Enable snow
|
||||
betterfoliage.leaves.snowEnabled.tooltip=Enable snow on extra leaves?
|
||||
betterfoliage.leaves.hideInternal=Hide internal leaves
|
||||
betterfoliage.leaves.hideInternal.tooltip=Skip rendering extra leaves if leaf block is completely surrounded by other leaves or solid blocks
|
||||
|
||||
betterfoliage.shortGrass=Short Grass & Mycelium
|
||||
betterfoliage.shortGrass.tooltip=Tufts of grass/mycelium on top of appropriate blocks
|
||||
betterfoliage.shortGrass.useGenerated=Use generated texture for grass
|
||||
betterfoliage.shortGrass.useGenerated.tooltip=Generated texture is made by slicing the tallgrass texture from the active resource pack in half
|
||||
betterfoliage.shortGrass.myceliumEnabled=Enable Mycelium
|
||||
betterfoliage.shortGrass.myceliumEnabled.tooltip=Is this feature enabled for mycelium blocks?
|
||||
betterfoliage.shortGrass.grassEnabled=Enable Grass
|
||||
betterfoliage.shortGrass.grassEnabled.tooltip=Is this feature enabled for grass blocks?
|
||||
betterfoliage.shortGrass.snowEnabled=Enable under snow
|
||||
betterfoliage.shortGrass.snowEnabled.tooltip=Enable on snowed grass blocks?
|
||||
betterfoliage.shortGrass.saturationThreshold=Saturation threshold
|
||||
betterfoliage.shortGrass.saturationThreshold.tooltip=Color saturation cutoff between "colorless" blocks (using biome color) and "colorful" blocks (using their own specific color)
|
||||
|
||||
betterfoliage.hangingGrass=Hanging Grass
|
||||
betterfoliage.hangingGrass.tooltip=Grass tufts hanging down from the top edges of grass blocks
|
||||
betterfoliage.hangingGrass.separation=Separation
|
||||
betterfoliage.hangingGrass.separation.tooltip=How much the hanging grass stands out from the block
|
||||
|
||||
betterfoliage.cactus=Better Cactus
|
||||
betterfoliage.cactus.tooltip=Enhance cactus with extra bits and smooth shading
|
||||
betterfoliage.cactus.sizeVariation=Size variation
|
||||
betterfoliage.cactus.sizeVariation.tooltip=Amount of random variation on cactus size
|
||||
|
||||
betterfoliage.lilypad=Better Lilypad
|
||||
betterfoliage.lilypad.tooltip=Enhance lilypad with roots and occasional flowers
|
||||
betterfoliage.lilypad.flowerChance=Flower chance
|
||||
betterfoliage.lilypad.flowerChance.tooltip=Chance (N in 64) of a lilypad having a flower on it
|
||||
|
||||
betterfoliage.reed=Reeds
|
||||
betterfoliage.reed.tooltip=Reeds on dirt blocks in shallow water
|
||||
betterfoliage.reed.biomes=Biome List
|
||||
betterfoliage.reed.biomes.tooltip=Configure which biomes reeds are allowed to appear in
|
||||
betterfoliage.reed.biomes.tooltip.element=Should reeds appear in the %s biome?
|
||||
|
||||
betterfoliage.algae=Algae
|
||||
betterfoliage.algae.tooltip=Algae on dirt blocks in deep water
|
||||
betterfoliage.algae.biomes=Biome List
|
||||
betterfoliage.algae.biomes.tooltip=Configure which biomes algae is allowed to appear in
|
||||
betterfoliage.algae.biomes.tooltip.element=Should algae appear in the %s biome?
|
||||
|
||||
betterfoliage.coral=Coral
|
||||
betterfoliage.coral.tooltip=Coral on sand blocks in deep water
|
||||
betterfoliage.coral.size=Coral size
|
||||
betterfoliage.coral.size.tooltip=Size of coral bits sticking out
|
||||
betterfoliage.coral.crustSize=Crust size
|
||||
betterfoliage.coral.crustSize.tooltip=Size of the flat coral part
|
||||
betterfoliage.coral.chance=Coral chance
|
||||
betterfoliage.coral.chance.tooltip=Chance (N in 64) of a specific face of the block to show coral
|
||||
betterfoliage.coral.biomes=Biome List
|
||||
betterfoliage.coral.biomes.tooltip=Configure which biomes coral is allowed to appear in
|
||||
betterfoliage.coral.biomes.tooltip.element=Should coral appear in the %s biome?
|
||||
betterfoliage.coral.shallowWater=Shallow water coral
|
||||
betterfoliage.coral.shallowWater.tooltip=Should coral appear in 1 block deep water?
|
||||
|
||||
betterfoliage.netherrack=Netherrack Vines
|
||||
betterfoliage.netherrack.tooltip=Hanging Vines under netherrack
|
||||
|
||||
betterfoliage.fallingLeaves=Falling leaves
|
||||
betterfoliage.fallingLeaves.tooltip=Falling leaf particle FX emitted from the bottom of leaf blocks
|
||||
betterfoliage.fallingLeaves.speed=Particle speed
|
||||
betterfoliage.fallingLeaves.speed.tooltip=Overall particle speed
|
||||
betterfoliage.fallingLeaves.windStrength=Wind strength
|
||||
betterfoliage.fallingLeaves.windStrength.tooltip=Magnitude of wind effects in good weather (spread of normal distribution centered on 0)
|
||||
betterfoliage.fallingLeaves.stormStrength=Storm strength
|
||||
betterfoliage.fallingLeaves.stormStrength.tooltip=Additional magnitude of wind effects in rainy weather (spread of normal distribution centered on 0)
|
||||
betterfoliage.fallingLeaves.size=Particle size
|
||||
betterfoliage.fallingLeaves.chance=Particle chance
|
||||
betterfoliage.fallingLeaves.chance.tooltip=Chance of each random render tick hitting a leaf block to spawn a particle
|
||||
betterfoliage.fallingLeaves.perturb=Perturbation
|
||||
betterfoliage.fallingLeaves.perturb.tooltip=Magnitude of perturbation effect. Adds a corkscrew-like motion to the particle synchronized to its rotation
|
||||
betterfoliage.fallingLeaves.lifetime=Maximum lifetime
|
||||
betterfoliage.fallingLeaves.lifetime.tooltip=Maximum lifetime of particle in seconds. Minimum lifetime is 60%% of this value
|
||||
betterfoliage.fallingLeaves.opacityHack=Opaque particles
|
||||
betterfoliage.fallingLeaves.opacityHack.tooltip=Stop transparent blocks obscuring particles even when particle is in front. WARNING: may cause glitches.
|
||||
|
||||
betterfoliage.risingSoul=Rising souls
|
||||
betterfoliage.risingSoul.tooltip=Rising soul particle FX emitted from the top of soulsand blocks
|
||||
betterfoliage.risingSoul.chance=Particle chance
|
||||
betterfoliage.risingSoul.chance.tooltip=Chance of each random render tick hitting a soulsand block to spawn a particle
|
||||
betterfoliage.risingSoul.speed=Particle speed
|
||||
betterfoliage.risingSoul.speed.tooltip=Vertical speed of soul particles
|
||||
betterfoliage.risingSoul.perturb=Perturbation
|
||||
betterfoliage.risingSoul.perturb.tooltip=Magnitude of perturbation effect. Adds a corkscrew-like motion to the particle
|
||||
betterfoliage.risingSoul.headSize=Soul size
|
||||
betterfoliage.risingSoul.headSize.tooltip=Size of the soul particle
|
||||
betterfoliage.risingSoul.trailSize=Trail size
|
||||
betterfoliage.risingSoul.trailSize.tooltip=Initial size of the particle trail
|
||||
betterfoliage.risingSoul.opacity=Opacity
|
||||
betterfoliage.risingSoul.opacity.tooltip=Opacity of the particle effect
|
||||
betterfoliage.risingSoul.sizeDecay=Size decay
|
||||
betterfoliage.risingSoul.sizeDecay.tooltip=Trail particle size relative to its size in the previous tick
|
||||
betterfoliage.risingSoul.opacityDecay=Opacity decay
|
||||
betterfoliage.risingSoul.opacityDecay.tooltip=Trail particle opacity relative to its opacity in the previous tick
|
||||
betterfoliage.risingSoul.lifetime=Maximum lifetime
|
||||
betterfoliage.risingSoul.lifetime.tooltip=Maximum lifetime of particle effect in seconds. Minimum lifetime is 60%% of this value
|
||||
betterfoliage.risingSoul.trailLength=Trail length
|
||||
betterfoliage.risingSoul.trailLength.tooltip=Number of previous positions the particle remembers in ticks
|
||||
betterfoliage.risingSoul.trailDensity=Trail density
|
||||
betterfoliage.risingSoul.trailDensity.tooltip=Render every Nth previous position in the particle trail
|
||||
|
||||
betterfoliage.connectedGrass=Connected grass textures
|
||||
betterfoliage.connectedGrass.enabled=Enable
|
||||
betterfoliage.connectedGrass.enabled.tooltip=If there is a grass block on top of a dirt block: draw grass top texture on all grass block sides,
|
||||
|
||||
betterfoliage.roundLogs=Round Logs
|
||||
betterfoliage.roundLogs.tooltip=Connect round blocks to solid full blocks?
|
||||
betterfoliage.roundLogs.connectSolids=Connect to solid
|
||||
betterfoliage.roundLogs.connectSolids.tooltip=Connect round blocks to solid full blocks?
|
||||
betterfoliage.roundLogs.connectPerpendicular=Connect to perpendicular logs
|
||||
betterfoliage.roundLogs.connectPerpendicular.tooltip=Connect round logs to perpendicular logs along its axis?
|
||||
betterfoliage.roundLogs.lenientConnect=Lenient rounding
|
||||
betterfoliage.roundLogs.lenientConnect.tooltip=Connect parallel round logs in an L-shape too, not just 2x2
|
||||
betterfoliage.roundLogs.connectGrass=Connect Grass
|
||||
betterfoliage.roundLogs.connectGrass.tooltip=Render grass block under trees instead of dirt if there is grass nearby
|
||||
betterfoliage.roundLogs.radiusSmall=Chamfer radius
|
||||
betterfoliage.roundLogs.radiusSmall.tooltip=How much to chop off from the log corner
|
||||
betterfoliage.roundLogs.radiusLarge=Connected chamfer radius
|
||||
betterfoliage.roundLogs.radiusLarge.tooltip=How much to chop off from the outer corner of connected logs
|
||||
betterfoliage.roundLogs.dimming=Dimming
|
||||
betterfoliage.roundLogs.dimming.tooltip=Amount to darken obscured log faces
|
||||
betterfoliage.roundLogs.zProtection=Z-Protection
|
||||
betterfoliage.roundLogs.zProtection.tooltip=Amount to scale parallel log connection bits to stop Z-fighting (flickering). Try to set it as high as possible without having glitches.
|
||||
betterfoliage.roundLogs.defaultY=Default to vertical
|
||||
betterfoliage.roundLogs.defaultY.tooltip=If true, log blocks where the orientation cannot be determined will be rendered as vertical. Otherwise, they will be rendered as cube blocks.
|
||||
216
src/main/resources/assets/betterfoliage/lang/ko_kr.lang
Normal file
216
src/main/resources/assets/betterfoliage/lang/ko_kr.lang
Normal file
@@ -0,0 +1,216 @@
|
||||
key.betterfoliage.gui=설정
|
||||
|
||||
betterfoliage.global.enabled=모드 활성화
|
||||
betterfoliage.global.enabled.tooltip=비활성화 할 경우, 환경강화모드 렌더링이 보이지 않습니다.
|
||||
|
||||
betterfoliage.enabled=활성화
|
||||
betterfoliage.enabled.tooltip=이 기능이 활성화 되어 있습니까?
|
||||
betterfoliage.hOffset=수평(가로) 상쇄시키다
|
||||
betterfoliage.hOffset.tooltip=이 성분이 블럭 수평으로 이동된다.
|
||||
betterfoliage.vOffset=수직(세로) 상쇄시키다
|
||||
betterfoliage.vOffset.tooltip=이 성분이 블럭 수직으로 이동된다.
|
||||
betterfoliage.size=사이즈(크기)
|
||||
betterfoliage.size.tooltip=사이즈(크기)의 최소
|
||||
betterfoliage.heightMin=최소한 높이
|
||||
betterfoliage.heightMin.tooltip=높이 최소한의 최소
|
||||
betterfoliage.heightMax=최대한 높이
|
||||
betterfoliage.heightMax.tooltip=높이 최대한의 최소
|
||||
betterfoliage.population=주민
|
||||
betterfoliage.population.tooltip=자격을 갖춘 블럭의 기능 확률(N 분의 64)
|
||||
betterfoliage.shaderWind=쉐이더 바람 효과
|
||||
betterfoliage.shaderWind.tooltip=바람효과를 쉐이더에 적용시키겠습니까?
|
||||
betterfoliage.distance=거리 제한
|
||||
betterfoliage.distance.tooltip=이 기능을 렌더링하는 플레이어의 최대거리
|
||||
|
||||
betterfoliage.blocks=블록 타입
|
||||
betterfoliage.blocks.tooltip=세팅 된 것에 따라 블록이 바뀔 것입니다.
|
||||
|
||||
betterfoliage.blocks.dirtWhitelist=흙 허용목록
|
||||
betterfoliage.blocks.dirtBlacklist=흙 차단목록
|
||||
betterfoliage.blocks.dirtWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.dirtBlacklist.arrayEntry=%d entries
|
||||
|
||||
betterfoliage.blocks.grassWhitelist=잔디 허용목록
|
||||
betterfoliage.blocks.grassBlacklist=잔디 차단목록
|
||||
betterfoliage.blocks.grassWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.grassBlacklist.arrayEntry=%d entries
|
||||
|
||||
betterfoliage.blocks.leavesWhitelist=잎 허용목록
|
||||
betterfoliage.blocks.leavesBlacklist=잎 차단목록
|
||||
betterfoliage.blocks.leavesWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.leavesBlacklist.arrayEntry=%d entries
|
||||
|
||||
betterfoliage.blocks.cropsWhitelist=농작물 허용목록
|
||||
betterfoliage.blocks.cropsBlacklist=농작물 차단목록
|
||||
betterfoliage.blocks.cropsWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.cropsBlacklist.arrayEntry=%d entries
|
||||
|
||||
betterfoliage.blocks.logsWhitelist=나무 허용목록
|
||||
betterfoliage.blocks.logsBlacklist=나무 차단목록
|
||||
betterfoliage.blocks.logsWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.logsBlacklist.arrayEntry=%d entries
|
||||
|
||||
betterfoliage.blocks.sandWhitelist=모래 허용목록
|
||||
betterfoliage.blocks.sandBlacklist=모래 차단목록
|
||||
betterfoliage.blocks.sandWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.sandBlacklist.arrayEntry=%d entries
|
||||
|
||||
betterfoliage.blocks.lilypadWhitelist=연꽃 허용목록
|
||||
betterfoliage.blocks.lilypadBlacklist=연꽃 차단목록
|
||||
betterfoliage.blocks.lilypadWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.lilypadBlacklist.arrayEntry=%d entries
|
||||
|
||||
betterfoliage.blocks.cactusWhitelist=선인장 허용목록
|
||||
betterfoliage.blocks.cactusBlacklist=선인장 차단목록
|
||||
betterfoliage.blocks.cactusWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.cactusBlacklist.arrayEntry=%d entries
|
||||
|
||||
|
||||
betterfoliage.blocks.dirtWhitelist.tooltip=흙으로 인식됩니다. 갈대, 조류, 잔디 텍스쳐 연결에 영향을 줍니다.
|
||||
betterfoliage.blocks.dirtBlacklist.tooltip=흙으로 인식 되지 않습니다. 갈대, 조류, 잔디 텍스쳐 연결에 영향을 주지 않습니다.
|
||||
betterfoliage.blocks.grassWhitelist.tooltip=잔디로 인식됩니다. 짧은 잔디, 잔디 텍스쳐 연결에 영향을 줍니다.
|
||||
betterfoliage.blocks.grassBlacklist.tooltip=잔디로 인식 되지 않습니다. 짧은 잔디, 잔디 텍스쳐 연결에 영향을 주지 않습니다.
|
||||
betterfoliage.blocks.leavesWhitelist.tooltip=잎으로 인식됩니다. 추가 잎, 떨어지는 잎에 영향을 줍니다.
|
||||
betterfoliage.blocks.leavesBlacklist.tooltip=잎으로 인식 되지 않습니다. 추가 잎, 떨어지는 잎에 영향을 주지 않습니다.
|
||||
betterfoliage.blocks.cropsWhitelist.tooltip=농작물로 인식됩니다. 농작물은 쉐이더 적용하면 큰잔디로 렌더링 됩니다.
|
||||
betterfoliage.blocks.cropsBlacklist.tooltip=농작물로 인식 되지 않습니다. 농작물은 쉐이더 적용하면 큰잔디로 렌더링 되지 않습니다.
|
||||
betterfoliage.blocks.logsWhitelist.tooltip=나무로 인식됩니다. 둥근 나무에 영향을 줍니다.
|
||||
betterfoliage.blocks.logsBlacklist.tooltip=나무로 인식 되지 않습니다. 둥근 나무에 영향을 주지 않습니다.
|
||||
betterfoliage.blocks.sandWhitelist.tooltip=모래로 인식됩니다. 산호에 영향을 줍니다
|
||||
betterfoliage.blocks.sandBlacklist.tooltip=모래로 인식되지 않습니다. 산호에 영향을 주지 않습니다.
|
||||
betterfoliage.blocks.lilypadWhitelist.tooltip=연꽃으로 인식됩니다. 보다 나은 연꽃에 영향을 줍니다.
|
||||
betterfoliage.blocks.lilypadBlacklist.tooltip=연꽃으로 인식되지 않습니다. 보다 나은 연꽃에 영향을 주지 않습니다.
|
||||
betterfoliage.blocks.cactusWhitelist.tooltip=선인장으로 인식됩니다. 보다 나은 선인장에 영향을 줍니다.
|
||||
betterfoliage.blocks.cactusBlacklist.tooltip=선인장으로 인식되지 않습니다. 보다 나은 선인장에 영향을 주지 않습니다.
|
||||
|
||||
betterfoliage.leaves=잎 추가
|
||||
betterfoliage.leaves.tooltip=둥글게 나뭇잎을 추가시켜줍니다.
|
||||
betterfoliage.leaves.dense=조밀한 모드
|
||||
betterfoliage.leaves.dense.tooltip=조밀한 모드는 둥근 나뭇잎을 더 추가시켜줍니다.
|
||||
|
||||
betterfoliage.shortGrass=이쁜 잔디 & 균사체
|
||||
betterfoliage.shortGrass.tooltip=블록 상단에 잔디 / 균사체
|
||||
betterfoliage.shortGrass.useGenerated=잔디텍스쳐를 활성화 합니다.
|
||||
betterfoliage.shortGrass.useGenerated.tooltip=소스팩의 일부분에 활성화 되어있는 큰잔디 텍스쳐를 생성시킵니다.
|
||||
betterfoliage.shortGrass.myceliumEnabled=균사체 활성화
|
||||
betterfoliage.shortGrass.myceliumEnabled.tooltip=균사체 블록에 있는 기능을 활성화 시키겠습니까?
|
||||
betterfoliage.shortGrass.grassEnabled=잔디 활성화
|
||||
betterfoliage.shortGrass.grassEnabled.tooltip=잔디 블록에 있는 기능을 활성화 시키겠습니까?
|
||||
betterfoliage.shortGrass.snowEnabled=눈 활성화
|
||||
betterfoliage.shortGrass.snowEnabled.tooltip=잔디 블록위에 있는 눈을 활성화 시키겠습니까?
|
||||
betterfoliage.shortGrass.saturationThreshold=채도 임계값
|
||||
betterfoliage.shortGrass.saturationThreshold.tooltip=(특정 색상을 사용하여)"무채색"블록과 (바이옴 색을 사용하여)"화려한"블록 사이의 채도 차단
|
||||
|
||||
betterfoliage.hangingGrass=매달려있는 잔디
|
||||
betterfoliage.hangingGrass.tooltip=잔디 블록 상단 가장자리에서 아래로 매달려 있는 잔디 다발
|
||||
betterfoliage.hangingGrass.separation=분리
|
||||
betterfoliage.hangingGrass.separation.tooltip=잔디블럭에 매달려있는 잔디의 양
|
||||
|
||||
betterfoliage.cactus=선인장
|
||||
betterfoliage.cactus.tooltip=선인장의 비트 수와 부드러운 그림자를 추가
|
||||
betterfoliage.cactus.sizeVariation=사이즈 변화
|
||||
betterfoliage.cactus.sizeVariation.tooltip=선인장의 사이즈 크기를 무작위 변화 시킵니다.
|
||||
|
||||
betterfoliage.lilypad=연꽃잎
|
||||
betterfoliage.lilypad.tooltip=연꽃의 뿌리와 약간의 꽃 추가
|
||||
betterfoliage.lilypad.flowerChance=꽃 확률
|
||||
betterfoliage.lilypad.flowerChance.tooltip=연꽃 위에 있는 꽃 확률(N 분의 64)
|
||||
|
||||
betterfoliage.reed=갈대
|
||||
betterfoliage.reed.tooltip=물 속에서 흙블럭 위에 있는 갈대
|
||||
betterfoliage.reed.biomes=바이옴 리스트
|
||||
betterfoliage.reed.biomes.tooltip=갈대를 바이옴에 따라 표시
|
||||
betterfoliage.reed.biomes.tooltip.element=갈대를 %s 바이옴에 따라 나타내시겠습니까?
|
||||
|
||||
betterfoliage.algae=해조류
|
||||
betterfoliage.algae.tooltip=깊은 물 속 흙 블럭 위에 있는 해조류
|
||||
betterfoliage.algae.biomes=바이옴 리스트
|
||||
betterfoliage.algae.biomes.tooltip=해조류를 바이옴에 따라 표시
|
||||
betterfoliage.algae.biomes.tooltip.element=해조류를 %s 바이옴에 따라 나타내시겠습니까?
|
||||
|
||||
betterfoliage.coral=산호
|
||||
betterfoliage.coral.tooltip=깊은 물 속 모래 블럭 위에있는 산호
|
||||
betterfoliage.coral.size=산호 사이즈(크기)
|
||||
betterfoliage.coral.size.tooltip=산호 이미지만큼 적용
|
||||
betterfoliage.coral.crustSize=껍질 크기
|
||||
betterfoliage.coral.crustSize.tooltip=산호 부분의 크기
|
||||
betterfoliage.coral.chance=산호 확률
|
||||
betterfoliage.coral.chance.tooltip=산호를 특정블록에 표시하는 확률(N 분의 64)
|
||||
betterfoliage.coral.biomes=바이옴 리스트
|
||||
betterfoliage.coral.biomes.tooltip=산호를 바이옴에 따라 표시
|
||||
betterfoliage.coral.biomes.tooltip.element=산호를 %s 바이옴에 따라 나타내시겠습니까?
|
||||
betterfoliage.coral.shallowWater=얕은 물 산호
|
||||
betterfoliage.coral.shallowWater.tooltip=산호를 깊은 물에 표시하시겠습니까?
|
||||
|
||||
betterfoliage.netherrack=네더랙 덩굴
|
||||
betterfoliage.netherrack.tooltip=네더랙에 매달려있는 덩굴
|
||||
|
||||
betterfoliage.fallingLeaves=떨어지는 나뭇잎
|
||||
betterfoliage.fallingLeaves.tooltip=잎 블록에서 바닥으로 떨어지는 잎 파티클
|
||||
betterfoliage.fallingLeaves.speed=파티클 속도
|
||||
betterfoliage.fallingLeaves.speed.tooltip=전체 파티클 속도
|
||||
betterfoliage.fallingLeaves.windStrength=바람 세기
|
||||
betterfoliage.fallingLeaves.windStrength.tooltip=날씨 바람 추가효과 (0을 중심으로 정규분포의 확산)
|
||||
betterfoliage.fallingLeaves.stormStrength=폭풍 세기
|
||||
betterfoliage.fallingLeaves.stormStrength.tooltip=비 날씨에 바람 추가효과 (0을 중심으로 정규분포의 확산)
|
||||
betterfoliage.fallingLeaves.size=파티클 크기
|
||||
betterfoliage.fallingLeaves.chance=파티클 확률
|
||||
betterfoliage.fallingLeaves.chance.tooltip=잎 파티클 랜덤 확률 설정
|
||||
betterfoliage.fallingLeaves.perturb=움직임
|
||||
betterfoliage.fallingLeaves.perturb.tooltip=움직임 효과의 크기. 코르크 같은 움직임을 추가합니다.
|
||||
betterfoliage.fallingLeaves.lifetime=파티클 지속시간
|
||||
betterfoliage.fallingLeaves.lifetime.tooltip=최대 파티클 지속시간을 설정합니다. 최소 지속시간은 60%입니다.
|
||||
betterfoliage.fallingLeaves.opacityHack=불투명 파티클
|
||||
betterfoliage.fallingLeaves.opacityHack.tooltip=파티클이 앞에 있어도 파티클을 가리는 투명블럭을 없앱니다. 경고: 오류주의
|
||||
|
||||
betterfoliage.risingSoul=소울 상승
|
||||
betterfoliage.risingSoul.tooltip=소울 블럭에서 올라오는 파티클
|
||||
betterfoliage.risingSoul.chance=파티클 수정
|
||||
betterfoliage.risingSoul.chance.tooltip=소울 파티클 랜덤 확률 설정
|
||||
betterfoliage.risingSoul.speed=파티클 속도
|
||||
betterfoliage.risingSoul.speed.tooltip=파티클 속도
|
||||
betterfoliage.risingSoul.perturb=움직임
|
||||
betterfoliage.risingSoul.perturb.tooltip=움직임 효과의 크기. 코르크 같은 움직임을 추가합니다.
|
||||
betterfoliage.risingSoul.headSize=소울 크기
|
||||
betterfoliage.risingSoul.headSize.tooltip=소울 파티클의 크기
|
||||
betterfoliage.risingSoul.trailSize=자취 사이즈
|
||||
betterfoliage.risingSoul.trailSize.tooltip=자취의 크기
|
||||
betterfoliage.risingSoul.opacity=불투명
|
||||
betterfoliage.risingSoul.opacity.tooltip=파티클의 불투명
|
||||
betterfoliage.risingSoul.sizeDecay=크기 감소
|
||||
betterfoliage.risingSoul.sizeDecay.tooltip=상대적인 자취 파티클 크기
|
||||
betterfoliage.risingSoul.opacityDecay=불투명 감소
|
||||
betterfoliage.risingSoul.opacityDecay.tooltip=상대적인 입자의 파티클 불투명도
|
||||
betterfoliage.risingSoul.lifetime=최대 지속시간
|
||||
betterfoliage.risingSoul.lifetime.tooltip=최대 파티클 지속시간을 설정합니다. 최소 지속시간은 60%입니다.
|
||||
betterfoliage.risingSoul.trailLength=자취의 세기
|
||||
betterfoliage.risingSoul.trailLength.tooltip=이전 파티클 틱으로 자취를 생성합니다.
|
||||
betterfoliage.risingSoul.trailDensity=자취의 밀도
|
||||
betterfoliage.risingSoul.trailDensity.tooltip=파티클 자취를 모두 렌더링 합니다.
|
||||
|
||||
betterfoliage.connectedGrass=잔디 연결 텍스쳐
|
||||
betterfoliage.connectedGrass.enabled=활성화
|
||||
betterfoliage.connectedGrass.enabled.tooltip=잔디블록이 흙블록 위에 있을 경우 잔디블록 윗텍스쳐를 옆텍스쳐에 전부 씌웁니다.
|
||||
|
||||
betterfoliage.roundLogs=둥근 나무
|
||||
betterfoliage.roundLogs.tooltip=둥근부분을 블럭과 연결.
|
||||
betterfoliage.roundLogs.connectSolids=블럭과 연결
|
||||
betterfoliage.roundLogs.connectSolids.tooltip=둥근부분을 블럭과 연결.
|
||||
betterfoliage.roundLogs.connectPerpendicular=나무 세로부분과 연결
|
||||
betterfoliage.roundLogs.connectPerpendicular.tooltip=나무 세로부분을 나무부분끼리 연결
|
||||
betterfoliage.roundLogs.lenientConnect=부드럽게 둥글게 연결
|
||||
betterfoliage.roundLogs.lenientConnect.tooltip=2x2사이즈처럼 나무 평형된 부분끼리 서로 연결합니다.
|
||||
betterfoliage.roundLogs.connectGrass=잔디 연결 텍스쳐
|
||||
betterfoliage.roundLogs.connectGrass.tooltip=잔디가 근처에 있을 경우 나무 아래 잔디 블록을 렌더링
|
||||
betterfoliage.roundLogs.radiusSmall=모서리 깎는 반지름
|
||||
betterfoliage.roundLogs.radiusSmall.tooltip=나무 모서리 깎는 정도
|
||||
betterfoliage.roundLogs.radiusLarge=모서리 깎는 부분 연결
|
||||
betterfoliage.roundLogs.radiusLarge.tooltip=나무 모서리 부분을 연결
|
||||
betterfoliage.roundLogs.dimming=조광
|
||||
betterfoliage.roundLogs.dimming.tooltip=나무 표면부분을 어둡게하는 양
|
||||
betterfoliage.roundLogs.zProtection=Z-Protection
|
||||
betterfoliage.roundLogs.zProtection.tooltip=Amount to scale parallel log connection bits to stop Z-fighting (flickering). Try to set it as high as possible without having glitches.
|
||||
betterfoliage.roundLogs.defaultY=수직선에대한 기본값
|
||||
betterfoliage.roundLogs.defaultY.tooltip=true 일경우, 나무 블럭이 수직선에대한 렌더링을 할수가 없습니다. 그렇지 아니하면, 네모난 블럭으로 렌더링 될것임니다.
|
||||
|
||||
# Translate by IS_Jump for feedback mail acarus22@gmail.com
|
||||
213
src/main/resources/assets/betterfoliage/lang/ru_ru.lang
Normal file
213
src/main/resources/assets/betterfoliage/lang/ru_ru.lang
Normal file
@@ -0,0 +1,213 @@
|
||||
key.betterfoliage.gui=Открыть настройки
|
||||
|
||||
betterfoliage.global.enabled=Включить мод
|
||||
betterfoliage.global.enabled.tooltip=Если установлено на false, BetterFoliage не будет ничего рендерить
|
||||
|
||||
betterfoliage.enabled=Включить
|
||||
betterfoliage.enabled.tooltip=Включена ли эта функция?
|
||||
betterfoliage.hOffset=Горизонтальное смещение
|
||||
betterfoliage.hOffset.tooltip=Дистанция горизонтального смещения этого элемента в блоках
|
||||
betterfoliage.vOffset=Вертикальное смещение
|
||||
betterfoliage.vOffset.tooltip=Дистанция вертикального смещения этого элемента в блоках
|
||||
betterfoliage.size=Размер
|
||||
betterfoliage.size.tooltip=Размер этого элемента
|
||||
betterfoliage.heightMin=Минимальная высота
|
||||
betterfoliage.heightMin.tooltip=Минимальная высота элемента
|
||||
betterfoliage.heightMax=Максимальная высота
|
||||
betterfoliage.heightMax.tooltip=Максимальная высота этого элемента
|
||||
betterfoliage.population=Популяция
|
||||
betterfoliage.population.tooltip=Шанс (N к 64), что блок будет иметь эту функцию
|
||||
betterfoliage.shaderWind=Шейдерные эффекты ветра
|
||||
betterfoliage.shaderWind.tooltip=Применить эффекты ветра с ShaderMod для этого элемента?
|
||||
betterfoliage.distance=Лимит дистанции
|
||||
betterfoliage.distance.tooltip=Максимальное расстояние от игрока для рендеринга этой функции
|
||||
|
||||
betterfoliage.blocks=Типы блоков
|
||||
betterfoliage.blocks.tooltip=Настройки списка классов блоков, которые будут иметь примененные к ним функции
|
||||
|
||||
betterfoliage.blocks.dirtWhitelist=Белый список земли
|
||||
betterfoliage.blocks.dirtBlacklist=Черный список земли
|
||||
betterfoliage.blocks.dirtWhitelist.arrayEntry=%d записей
|
||||
betterfoliage.blocks.dirtBlacklist.arrayEntry=%d записей
|
||||
|
||||
betterfoliage.blocks.grassWhitelist=Белый список травы
|
||||
betterfoliage.blocks.grassBlacklist=Черный список травы
|
||||
betterfoliage.blocks.grassWhitelist.arrayEntry=%d записей
|
||||
betterfoliage.blocks.grassBlacklist.arrayEntry=%d записей
|
||||
|
||||
betterfoliage.blocks.leavesWhitelist=Белый список листвы
|
||||
betterfoliage.blocks.leavesBlacklist=Черный список листвы
|
||||
betterfoliage.blocks.leavesWhitelist.arrayEntry=%d записей
|
||||
betterfoliage.blocks.leavesBlacklist.arrayEntry=%d записей
|
||||
|
||||
betterfoliage.blocks.cropsWhitelist=Белый список урожая
|
||||
betterfoliage.blocks.cropsBlacklist=Черный список урожая
|
||||
betterfoliage.blocks.cropsWhitelist.arrayEntry=%d записей
|
||||
betterfoliage.blocks.cropsBlacklist.arrayEntry=%d записей
|
||||
|
||||
betterfoliage.blocks.logsWhitelist=Белый список древесины
|
||||
betterfoliage.blocks.logsBlacklist=Черный список древесины
|
||||
betterfoliage.blocks.logsWhitelist.arrayEntry=%d записей
|
||||
betterfoliage.blocks.logsBlacklist.arrayEntry=%d записей
|
||||
|
||||
betterfoliage.blocks.sandWhitelist=Белый список песка
|
||||
betterfoliage.blocks.sandBlacklist=Черный список песка
|
||||
betterfoliage.blocks.sandWhitelist.arrayEntry=%d записей
|
||||
betterfoliage.blocks.sandBlacklist.arrayEntry=%d записей
|
||||
|
||||
betterfoliage.blocks.lilypadWhitelist=Белый список кувшинок
|
||||
betterfoliage.blocks.lilypadBlacklist=Черный список кувшинок
|
||||
betterfoliage.blocks.lilypadWhitelist.arrayEntry=%d записей
|
||||
betterfoliage.blocks.lilypadBlacklist.arrayEntry=%d записей
|
||||
|
||||
betterfoliage.blocks.cactusWhitelist=Белый список кактусов
|
||||
betterfoliage.blocks.cactusBlacklist=Черный список кактусов
|
||||
betterfoliage.blocks.cactusWhitelist.arrayEntry=%d записей
|
||||
betterfoliage.blocks.cactusBlacklist.arrayEntry=%d записей
|
||||
|
||||
|
||||
betterfoliage.blocks.dirtWhitelist.tooltip=Блоки, которые будут восприниматься в качестве земли. Влияет на камыши, водоросли, соединенную траву.
|
||||
betterfoliage.blocks.dirtBlacklist.tooltip=Блоки, которые не будут восприниматься в качестве земли. Влияет на камыши, водоросли, соединенную траву.
|
||||
betterfoliage.blocks.grassWhitelist.tooltip=Блоки, которые будут восприниматься в качестве травы. Влияет на короткую и соединенную траву.
|
||||
betterfoliage.blocks.grassBlacklist.tooltip=Блоки, которые не будут восприниматься в качестве травы. Влияет на короткую и соединенную траву.
|
||||
betterfoliage.blocks.leavesWhitelist.tooltip=Блоки, которые будут восприниматься в качестве листвы. Влияет на дополнительную листву, падающую листву. Листва будут рендериться с ID листвы в шейдер-программах.
|
||||
betterfoliage.blocks.leavesBlacklist.tooltip=Блоки, которые никогда не будут восприниматься как листва. Влияет на дополнительную листву, падающую листву. Листва будут рендериться с ID листвы в шейдер-программах.
|
||||
betterfoliage.blocks.cropsWhitelist.tooltip=Блоки, которые будут восприниматься как культуры. Культуры будут рендериться с ID высокой травы в шейдер-программах.
|
||||
betterfoliage.blocks.cropsBlacklist.tooltip= Блоки, которые никогда не будут восприниматься как культуры. Культуры будут рендериться с ID высокой травы в шейдер-программах.
|
||||
betterfoliage.blocks.logsWhitelist.tooltip=Блоки, которые будут восприниматься в качестве деревянных брёвен. Влияет на цилиндрические брёвна.
|
||||
betterfoliage.blocks.logsBlacklist.tooltip=Блоки, которые никогда не будут восприниматься в качестве деревянных брёвен. Влияет на цилиндрические брёвна.
|
||||
betterfoliage.blocks.sandWhitelist.tooltip=Блоки, которые будут восприниматься в качестве песка. Влияет на кораллы.
|
||||
betterfoliage.blocks.sandBlacklist.tooltip=Блоки, которые никогда не будут восприниматься в качестве песка. Влияет на кораллы.
|
||||
betterfoliage.blocks.lilypadWhitelist.tooltip=Блоки, которые будут восприниматься в качестве кувшинок. Влияет на улучшенные кувшинки.
|
||||
betterfoliage.blocks.lilypadBlacklist.tooltip=Блоки, которые никогда не будут восприниматься в качестве кувшинок. Влияет на улучшенные кувшинки.
|
||||
betterfoliage.blocks.cactusWhitelist.tooltip=Блоки, которые будут восприниматься в качестве кактусов. Влияет на улучшенные кактусы.
|
||||
betterfoliage.blocks.cactusBlacklist.tooltip=Блоки, которые никогда не будут восприниматься в качестве кувшинок. Влияет на улучшенные кактусы.
|
||||
|
||||
betterfoliage.leaves=Улучшенная листва
|
||||
betterfoliage.leaves.tooltip=Дополнительное округление листьев на блоках листвы.
|
||||
betterfoliage.leaves.dense=Плотный режим
|
||||
betterfoliage.leaves.dense.tooltip=Плотный режим имеет более округлые листья.
|
||||
|
||||
betterfoliage.shortGrass=Низкая трава и мицелий
|
||||
betterfoliage.shortGrass.tooltip=Пучки травы / мицелия на поверхности соответствующих блоков.
|
||||
betterfoliage.shortGrass.useGenerated=Использовать сгенерированные текстуры для травы.
|
||||
betterfoliage.shortGrass.useGenerated.tooltip=Сгенерированная текстура создается путём разрезания текстуры высокой травы с активного ресурс-пака пополам.
|
||||
betterfoliage.shortGrass.myceliumEnabled=Включить мицелий
|
||||
betterfoliage.shortGrass.myceliumEnabled.tooltip=Включить эту особенность для блоков мицелия?
|
||||
betterfoliage.shortGrass.grassEnabled=Включить траву
|
||||
betterfoliage.shortGrass.grassEnabled.tooltip=Включить эту особенность для блоков травы?
|
||||
betterfoliage.shortGrass.snowEnabled=Включить траву под снегом
|
||||
betterfoliage.shortGrass.snowEnabled.tooltip=Включить эту особенность для заснеженных блоков травы?
|
||||
betterfoliage.shortGrass.saturationThreshold=Порог насыщения
|
||||
betterfoliage.shortGrass.saturationThreshold.tooltip=Насыщенность цвета разделяется на: "обесцвеченные" блоки (используя цвет биома) и "цветные" блоки (используя их собственный цвет)
|
||||
|
||||
betterfoliage.hangingGrass=Висячая трава
|
||||
betterfoliage.hangingGrass.tooltip=Пучки травы свисают вниз с верхних краев блока травы.
|
||||
betterfoliage.hangingGrass.separation=Разделение
|
||||
betterfoliage.hangingGrass.separation.tooltip=Как долго подвесная трава выделяется из блока?
|
||||
|
||||
betterfoliage.cactus=Улучшенные кактусы
|
||||
betterfoliage.cactus.tooltip=Улучшить кактус с дополнительными частицами и плавными тенями.
|
||||
betterfoliage.cactus.sizeVariation=Вариации размера
|
||||
betterfoliage.cactus.sizeVariation.tooltip=Количество случайных изменений в размере кактусов.
|
||||
|
||||
betterfoliage.lilypad=Улучшенные кувшинки
|
||||
betterfoliage.lilypad.tooltip=Добавить кувшинкам корни и цветы.
|
||||
betterfoliage.lilypad.flowerChance=Шанс появления цветов
|
||||
betterfoliage.lilypad.flowerChance.tooltip=Шанс (N к 64) появления цветка на кувшинке.
|
||||
|
||||
betterfoliage.reed=Камыши
|
||||
betterfoliage.reed.tooltip=Мелководные камыши на блоках земли.
|
||||
betterfoliage.reed.biomes=Список биомов
|
||||
betterfoliage.reed.biomes.tooltip=Настройка биомов, в которых камышам разрешено появляться.
|
||||
betterfoliage.reed.biomes.tooltip.element=Должны ли камыши встречаться в %s биоме?
|
||||
|
||||
betterfoliage.algae=Морские водоросли
|
||||
betterfoliage.algae.tooltip=Глубоководные водоросли на блоках земли.
|
||||
betterfoliage.algae.biomes=Список биомов
|
||||
betterfoliage.algae.biomes.tooltip=Настройка биомов, в которых водорослям разрешено появляться.
|
||||
betterfoliage.algae.biomes.tooltip.element=Должны ли водоросли встречаться в %s биоме?
|
||||
|
||||
betterfoliage.coral=Кораллы
|
||||
betterfoliage.coral.tooltip=Кораллы на песчаных блоках в глубокой воде.
|
||||
betterfoliage.coral.size=Размер кораллов
|
||||
betterfoliage.coral.size.tooltip=Размер торчащих частичек кораллов.
|
||||
betterfoliage.coral.crustSize=Размер коры
|
||||
betterfoliage.coral.crustSize.tooltip=Размер плоской части кораллов.
|
||||
betterfoliage.coral.chance=Шанс кораллов
|
||||
betterfoliage.coral.chance.tooltip=Шанс (N in 64) появления кораллов на определенном блоке.
|
||||
betterfoliage.coral.biomes=Список биомов
|
||||
betterfoliage.coral.biomes.tooltip=Настройка биомов, в которых разрешено появляться кораллам.
|
||||
betterfoliage.coral.biomes.tooltip.element=Должны ли кораллы появляться в %s биоме?
|
||||
betterfoliage.coral.shallowWater=Мелководные кораллы
|
||||
betterfoliage.coral.shallowWater.tooltip=Должны ли появляться кораллы в воде, глубиной в 1 блок?
|
||||
|
||||
betterfoliage.netherrack=Адская лоза
|
||||
betterfoliage.netherrack.tooltip=Висячая лоза под адским камнем
|
||||
|
||||
betterfoliage.fallingLeaves=Падающие листья
|
||||
betterfoliage.fallingLeaves.tooltip=Падение FX частиц листвы исходящие из низа блоков листвы
|
||||
betterfoliage.fallingLeaves.speed=Скорость частиц
|
||||
betterfoliage.fallingLeaves.speed.tooltip=Общая скорость частиц
|
||||
betterfoliage.fallingLeaves.windStrength=Сила ветра
|
||||
betterfoliage.fallingLeaves.windStrength.tooltip=Величина воздействия ветра в хорошую погоду (распространение нормального распределения сосредоточено на 0)
|
||||
betterfoliage.fallingLeaves.stormStrength=Сила шторма
|
||||
betterfoliage.fallingLeaves.stormStrength.tooltip=Дополнительная величина воздействия ветра в ненастную погоду (распространение нормального распределения сосредоточено на 0)
|
||||
betterfoliage.fallingLeaves.size=Размер частиц
|
||||
betterfoliage.fallingLeaves.chance=Шанс частиц
|
||||
betterfoliage.fallingLeaves.chance.tooltip=Вероятность каждого случайного рендеринга в такт (1/20 секунды) опадения частицы блока листвы.
|
||||
betterfoliage.fallingLeaves.perturb=Возмущение
|
||||
betterfoliage.fallingLeaves.perturb.tooltip=Величина эффекта возмущений. Добавляет штопорообразное движение к частице синхронизированной с его вращением.
|
||||
betterfoliage.fallingLeaves.lifetime=Максимальное время жизни
|
||||
betterfoliage.fallingLeaves.lifetime.tooltip=Максимальное время жизни частиц. Минимальное время жизни - 60%% от этого значения.
|
||||
betterfoliage.fallingLeaves.opacityHack=Непрозрачные частицы
|
||||
betterfoliage.fallingLeaves.opacityHack.tooltip=Запретить прозрачным блокам затемнять частицы даже тогда, когда частицы впереди. ВНИМАНИЕ: может спровоцировать баги.
|
||||
|
||||
betterfoliage.risingSoul=Адские духи
|
||||
betterfoliage.risingSoul.tooltip=Количество душ-частиц FX, испускаемых из верхней части блоков песка душ.
|
||||
betterfoliage.risingSoul.chance=Шанс частиц
|
||||
betterfoliage.risingSoul.chance.tooltip=Частота генерации частиц на песке душ.
|
||||
betterfoliage.risingSoul.speed=Скорость частиц
|
||||
betterfoliage.risingSoul.speed.tooltip=Вертикальная скорость движения частиц духов.
|
||||
betterfoliage.risingSoul.perturb=Возмущение
|
||||
betterfoliage.risingSoul.perturb.tooltip=Магнитуда эффекта возмущений. Добавляет штопороподобное движение частиц.
|
||||
betterfoliage.risingSoul.headSize=Размер духа
|
||||
betterfoliage.risingSoul.headSize.tooltip=Размер частицы духа
|
||||
betterfoliage.risingSoul.trailSize=Размер следов
|
||||
betterfoliage.risingSoul.trailSize.tooltip=Начальный размер следа частиц
|
||||
betterfoliage.risingSoul.opacity=Прозрачность
|
||||
betterfoliage.risingSoul.opacity.tooltip=Непрозрачность эффекта частиц
|
||||
betterfoliage.risingSoul.sizeDecay=Размер распада
|
||||
betterfoliage.risingSoul.sizeDecay.tooltip=Следующий размер частицы соответствует их же размеру в предыдущем такте (1/20 секунды).
|
||||
betterfoliage.risingSoul.opacityDecay=Непрозрачность распада
|
||||
betterfoliage.risingSoul.opacityDecay.tooltip=Следующий уровень прозрачности частицы соответствует их же уровню прозрачности в предыдущем такте (1/20 секунды).
|
||||
betterfoliage.risingSoul.lifetime=Максимальное время жизни
|
||||
betterfoliage.risingSoul.lifetime.tooltip=Максимальное время жизни эффекта частиц. Минимальное время жизни равно 60%% от этого числа.
|
||||
betterfoliage.risingSoul.trailLength=Длина следов
|
||||
betterfoliage.risingSoul.trailLength.tooltip=Количество предыдущих позиций, которые запомнила частица в тактах (1/20 секунды).
|
||||
betterfoliage.risingSoul.trailDensity=Плотность следов
|
||||
betterfoliage.risingSoul.trailDensity.tooltip=Рендер каждой предыдущий N’ой позиции в следах частиц.
|
||||
|
||||
|
||||
betterfoliage.connectedGrass=Соединенные текстуры травы
|
||||
betterfoliage.connectedGrass.enabled=Включить
|
||||
betterfoliage.connectedGrass.enabled.tooltip=Если блок травы находится над блоком земли: прорисовать верхнюю текстуру травы на всех сторонах блока травы.
|
||||
|
||||
betterfoliage.roundLogs=Цилиндрические брёвна
|
||||
betterfoliage.roundLogs.tooltip=Соединить круглые блоки в сплошные, полные блоки?
|
||||
betterfoliage.roundLogs.connectSolids=Соединение в крупные брёвна
|
||||
betterfoliage.roundLogs.connectSolids.tooltip=Соединить круглые блоки в сплошные, полные блоки?
|
||||
betterfoliage.roundLogs.connectPerpendicular=Соединение в перпендикулярные брёвна
|
||||
betterfoliage.roundLogs.connectPerpendicular.tooltip=Соединить круглые брёвна к перпендикулярным брёвнам относительно их оси?
|
||||
betterfoliage.roundLogs.lenientConnect=Мягкое округление
|
||||
betterfoliage.roundLogs.lenientConnect.tooltip=Соединение в параллельные круглые брёвна L-формы, не только 2х2.
|
||||
betterfoliage.roundLogs.connectGrass=Соединенная трава
|
||||
betterfoliage.roundLogs.connectGrass.tooltip=Заменяет землю под деревьями на траву, если она есть поблизости.
|
||||
betterfoliage.roundLogs.radiusSmall=Радиус фаски
|
||||
betterfoliage.roundLogs.radiusSmall.tooltip=Радиус обрезки углов от бревна.
|
||||
betterfoliage.roundLogs.radiusLarge=Радиус соединенной фаски
|
||||
betterfoliage.roundLogs.radiusLarge.tooltip=Радиус среза внешнего угла соединённых брёвен.
|
||||
betterfoliage.roundLogs.dimming=Затемнение
|
||||
betterfoliage.roundLogs.dimming.tooltip=Затемнить неясные длинные грани.
|
||||
betterfoliage.roundLogs.zProtection=Z-Защита
|
||||
betterfoliage.roundLogs.zProtection.tooltip=Для масштабирования параллельных битов соединения бревен, чтобы остановить Z-бой (мерцание). Попробуйте установить его как можно выше, для устранения мерцания.
|
||||
@@ -0,0 +1,9 @@
|
||||
// Vanilla
|
||||
spruce=spruce
|
||||
jungle=jungle
|
||||
|
||||
// Biomes O' Plenty
|
||||
fir=spruce
|
||||
|
||||
// Forestry
|
||||
forestry:conifers=spruce
|
||||
@@ -0,0 +1,2 @@
|
||||
// Vanilla
|
||||
net.minecraft.block.LeavesBlock
|
||||
@@ -0,0 +1,4 @@
|
||||
minecraft:block/leaves,all
|
||||
minecraft:block/cube_all,all
|
||||
|
||||
biomesoplenty:block/leaves_overlay,under
|
||||
@@ -0,0 +1,8 @@
|
||||
// Vanilla
|
||||
net.minecraft.block.BlockLilyPad
|
||||
|
||||
// Biomes O'Plenty
|
||||
biomesoplenty.common.block.BlockBOPLilypad
|
||||
|
||||
// TerraFirmaCraft
|
||||
com.bioxx.tfc.Blocks.Vanilla.BlockCustomLilyPad
|
||||
@@ -0,0 +1,2 @@
|
||||
// Vanilla
|
||||
net.minecraft.block.LogBlock
|
||||
@@ -0,0 +1,12 @@
|
||||
// Vanilla
|
||||
block/column_side,end,end,side
|
||||
block/cube_column,end,end,side
|
||||
block/cube_all,all,all,all
|
||||
|
||||
// Agricultural Revolution
|
||||
agriculturalrevolution:block/palmlog,top,top,texture
|
||||
|
||||
// Lithos Core
|
||||
block/column_top,end,end,side_a,side_b
|
||||
block/column_side_x,end,end,side_a,side_b
|
||||
block/column_side_z,end,end,side_a,side_b
|
||||
@@ -0,0 +1,2 @@
|
||||
// Vanilla
|
||||
net.minecraft.block.MyceliumBlock
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 377 B |
Binary file not shown.
|
After Width: | Height: | Size: 414 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user