├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── com │ └── ishland │ └── earlyloadingscreen │ ├── EarlyLaunch.java │ ├── LanguageAdapterLaunch.java │ ├── Launch.java │ ├── LoadingProgressManager.java │ ├── LoadingScreenManager.java │ ├── MixinEarlyLaunch.java │ ├── PreLaunchHandler.java │ ├── SharedConstants.java │ ├── TheMixinConfig.java │ ├── TheMod.java │ ├── mixin │ ├── MixinMinecraftClient.java │ ├── MixinSplashOverlay.java │ ├── MixinWindow.java │ ├── access │ │ ├── ISimpleResourceReload.java │ │ └── ITextureStitcherHolder.java │ └── progress │ │ ├── MixinAtlasLoader.java │ │ ├── MixinBakedModelManager.java │ │ ├── MixinModelLoader.java │ │ ├── MixinSpriteLoader.java │ │ ├── MixinTextureManager.java │ │ └── MixinTextureStitcher.java │ ├── patch │ ├── BytecodeTransformer.java │ ├── FabricLoaderInvokePatch.java │ ├── PatchUtil.java │ └── SodiumOSDetectionPatch.java │ ├── platform_cl │ ├── AppLoaderAccessSupport.java │ ├── Config.java │ ├── LaunchPoint.java │ └── PlatformUtil.java │ ├── render │ ├── GLText.java │ └── Simple2DDraw.java │ └── util │ ├── AppLoaderUtil.java │ ├── OSDetectionUtil.java │ ├── ProgressUtil.java │ ├── RemapUtil.java │ └── WindowCreationUtil.java └── resources ├── assets └── earlyloadingscreen │ └── icon.png ├── earlyloadingscreen.accesswidener ├── earlyloadingscreen.mixins.json └── fabric.mod.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Automatically build the project and run any configured tests for every push 2 | # and submitted pull request. This can help catch issues that only occur on 3 | # certain platforms or Java versions, and provides a first line of defence 4 | # against bad commits. 5 | 6 | name: build 7 | on: [pull_request, push] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | # Use these Java versions 14 | java: [ 15 | 21, # Current Java LTS & minimum supported by Minecraft 16 | ] 17 | # and run on both Linux and Windows 18 | os: [ubuntu-22.04, windows-2022] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - name: checkout repository 22 | uses: actions/checkout@v3 23 | - name: validate gradle wrapper 24 | uses: gradle/wrapper-validation-action@v1 25 | - name: setup jdk ${{ matrix.java }} 26 | uses: actions/setup-java@v3 27 | with: 28 | java-version: ${{ matrix.java }} 29 | distribution: 'microsoft' 30 | - name: make gradle wrapper executable 31 | if: ${{ runner.os != 'Windows' }} 32 | run: chmod +x ./gradlew 33 | - name: build 34 | run: ./gradlew build 35 | - name: capture build artifacts 36 | if: ${{ runner.os == 'Linux' && matrix.java == '17' }} # Only upload artifacts built from latest java on one OS 37 | uses: actions/upload-artifact@v3 38 | with: 39 | name: Artifacts 40 | path: build/libs/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | 8 | # eclipse 9 | 10 | *.launch 11 | 12 | # idea 13 | 14 | .idea/ 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | # vscode 20 | 21 | .settings/ 22 | .vscode/ 23 | bin/ 24 | .classpath 25 | .project 26 | 27 | # macos 28 | 29 | *.DS_Store 30 | 31 | # fabric 32 | 33 | run*/ 34 | 35 | # java 36 | 37 | hs_err_*.log 38 | replay_*.log 39 | *.hprof 40 | *.jfr 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 ishland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Early Loading Screen 2 | 3 | A Fabric mod that shows an early loading screen and display information while the game is loading. 4 | 5 | ## Compatibility 6 | This mod has been tested with AOF6 with Prism Launcher and default settings without any issues. 7 | 8 | ## Configuration 9 | 10 | All configuration is done in the `early-loading-screen.properties` file in the config folder. 11 | 12 | ### `window_creation_point` 13 | Available options: `mixinEarly` `mixinLoad` `preLaunch` `mcEarly` `off` 14 | Default: `mixinEarly` (or `mixinLoad` if ImmediatelyFast is installed) 15 | 16 | This controls the point when the window is created. 17 | The available options above are sorted in order of earliest to latest. 18 | The option `mixinEarly` does some classloading hacks, use `mixinLoad` if you have issues with it. 19 | 20 | Use `off` to turn off early screen entirely. 21 | 22 | ### `enable_entrypoint_information` 23 | Available options: `true` `false` 24 | Default: `true` 25 | 26 | This controls whether the entrypoint information is shown on the early screen. 27 | When this is enabled, it will apply patches to the fabric-loader to show entrypoint invocation process. 28 | Disable if you have weird issues with the game launch. 29 | 30 | ### `reuse_early_window` 31 | Available options: `true` `false` 32 | Default: `true` 33 | Not available on Windows. 34 | 35 | This controls whether the early screen is reused as the game window. 36 | 37 | ### `enable_mixin_pretransform` 38 | Available options: `true` `false` 39 | Default: `false` 40 | 41 | This controls whether mixins apply runs early. 42 | May cause issues with mods that doesn't handle classloading properly. 43 | 44 | ### `allow_early_window_close` 45 | Available options: `true` `false` 46 | Default: `true` 47 | 48 | This controls whether the early screen can be closed. 49 | Closing the early screen will cause the game to exit immediately. 50 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'fabric-loom' version '1.7-SNAPSHOT' 3 | id 'maven-publish' 4 | id 'com.github.johnrengelman.shadow' version '7.1.0' 5 | } 6 | 7 | archivesBaseName = "${project.archives_base_name}-mc${project.minecraft_version}" 8 | version = project.mod_version 9 | group = project.maven_group 10 | 11 | loom { 12 | accessWidenerPath = file("src/main/resources/earlyloadingscreen.accesswidener") 13 | } 14 | 15 | configurations { 16 | shadowInclude 17 | } 18 | 19 | repositories { 20 | // Add repositories to retrieve artifacts from in here. 21 | // You should only use this when depending on other mods because 22 | // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. 23 | // See https://docs.gradle.org/current/userguide/declaring_repositories.html 24 | // for more information about repositories. 25 | 26 | maven { url 'https://jitpack.io' } 27 | } 28 | 29 | dependencies { 30 | // To change the versions see the gradle.properties file 31 | minecraft "com.mojang:minecraft:${project.minecraft_version}" 32 | mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" 33 | modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" 34 | 35 | // Fabric API. This is technically optional, but you probably want it anyway. 36 | // modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" 37 | 38 | // Uncomment the following line to enable the deprecated Fabric API modules. 39 | // These are included in the Fabric API production distribution and allow you to update your mod to the latest modules at a later more convenient time. 40 | 41 | // modImplementation "net.fabricmc.fabric-api:fabric-api-deprecated:${project.fabric_version}" 42 | 43 | shadowInclude implementation('net.bytebuddy:byte-buddy-agent:1.14.0') 44 | // include implementation("com.github.LlamaLad7:MixinExtras:${project.mixinextras_version}") 45 | // annotationProcessor "com.github.LlamaLad7:MixinExtras:${project.mixinextras_version}" 46 | } 47 | 48 | processResources { 49 | inputs.property "version", project.version 50 | 51 | filesMatching("fabric.mod.json") { 52 | expand "version": project.version 53 | } 54 | } 55 | 56 | tasks.withType(JavaCompile).configureEach { 57 | it.options.release = 17 58 | } 59 | 60 | java { 61 | // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task 62 | // if it is present. 63 | // If you remove this line, sources will not be generated. 64 | withSourcesJar() 65 | 66 | sourceCompatibility = JavaVersion.VERSION_17 67 | targetCompatibility = JavaVersion.VERSION_17 68 | } 69 | 70 | jar { 71 | from("LICENSE") { 72 | rename { "${it}_${project.archivesBaseName}"} 73 | } 74 | } 75 | 76 | shadowJar { 77 | archiveClassifier = "all-dev" 78 | configurations = [ project.configurations.shadowInclude ] 79 | from("LICENSE") { 80 | rename { "${it}_${project.archivesBaseName}"} 81 | } 82 | relocate "net.bytebuddy", "com.ishland.earlyloadingscreen.deps.bytebuddy" 83 | } 84 | 85 | //noinspection UnnecessaryQualifiedReference 86 | task("remapShadowJar", type: net.fabricmc.loom.task.RemapJarTask, dependsOn: shadowJar) { 87 | input = shadowJar.archiveFile 88 | archiveFileName = shadowJar.archiveFileName.get().replaceAll("-dev\\.jar\$", ".jar") 89 | addNestedDependencies = true 90 | } 91 | 92 | assemble.dependsOn(remapShadowJar) 93 | 94 | // configure the maven publication 95 | publishing { 96 | publications { 97 | mavenJava(MavenPublication) { 98 | from components.java 99 | } 100 | } 101 | 102 | // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. 103 | repositories { 104 | // Add repositories to publish to here. 105 | // Notice: This block does NOT have the same function as the block in the top level. 106 | // The repositories here will be used for publishing your artifact, not for 107 | // retrieving dependencies. 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx2G 3 | org.gradle.parallel=true 4 | 5 | # Fabric Properties 6 | # check these on https://fabricmc.net/develop 7 | minecraft_version=1.21 8 | yarn_mappings=1.21+build.2 9 | loader_version=0.15.11 10 | 11 | # Mod Properties 12 | mod_version=0.1.5 13 | maven_group=com.ishland 14 | archives_base_name=earlyloadingscreen 15 | 16 | # Dependencies 17 | fabric_version=0.83.0+1.20.1 18 | mixinextras_version=0.1.1 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishland/EarlyLoadingScreen/a54691da09c0489ddb49fe201f4f685fae72b549/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 134 | 135 | Please set the JAVA_HOME variable in your environment to match the 136 | location of your Java installation." 137 | fi 138 | 139 | # Increase the maximum file descriptors if we can. 140 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 141 | case $MAX_FD in #( 142 | max*) 143 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 144 | # shellcheck disable=SC3045 145 | MAX_FD=$( ulimit -H -n ) || 146 | warn "Could not query maximum file descriptor limit" 147 | esac 148 | case $MAX_FD in #( 149 | '' | soft) :;; #( 150 | *) 151 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 152 | # shellcheck disable=SC3045 153 | ulimit -n "$MAX_FD" || 154 | warn "Could not set maximum file descriptor limit to $MAX_FD" 155 | esac 156 | fi 157 | 158 | # Collect all arguments for the java command, stacking in reverse order: 159 | # * args from the command line 160 | # * the main class name 161 | # * -classpath 162 | # * -D...appname settings 163 | # * --module-path (only if needed) 164 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 165 | 166 | # For Cygwin or MSYS, switch paths to Windows format before running java 167 | if "$cygwin" || "$msys" ; then 168 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 169 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 170 | 171 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 172 | 173 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 174 | for arg do 175 | if 176 | case $arg in #( 177 | -*) false ;; # don't mess with options #( 178 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 179 | [ -e "$t" ] ;; #( 180 | *) false ;; 181 | esac 182 | then 183 | arg=$( cygpath --path --ignore --mixed "$arg" ) 184 | fi 185 | # Roll the args list around exactly as many times as the number of 186 | # args, so each arg winds up back in the position where it started, but 187 | # possibly modified. 188 | # 189 | # NB: a `for` loop captures its iteration list before it begins, so 190 | # changing the positional parameters here affects neither the number of 191 | # iterations, nor the values presented in `arg`. 192 | shift # remove old arg 193 | set -- "$@" "$arg" # push replacement arg 194 | done 195 | fi 196 | 197 | 198 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 199 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 200 | 201 | # Collect all arguments for the java command; 202 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 203 | # shell script including quotes and variable substitutions, so put them in 204 | # double quotes to make sure that they get re-expanded; and 205 | # * put everything else in single quotes, so that it's not re-expanded. 206 | 207 | set -- \ 208 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 209 | -classpath "$CLASSPATH" \ 210 | org.gradle.wrapper.GradleWrapperMain \ 211 | "$@" 212 | 213 | # Stop when "xargs" is not available. 214 | if ! command -v xargs >/dev/null 2>&1 215 | then 216 | die "xargs is not available" 217 | fi 218 | 219 | # Use "xargs" to parse quoted args. 220 | # 221 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 222 | # 223 | # In Bash we could simply go: 224 | # 225 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 226 | # set -- "${ARGS[@]}" "$@" 227 | # 228 | # but POSIX shell has neither arrays nor command substitution, so instead we 229 | # post-process each arg (as a line of input to sed) to backslash-escape any 230 | # character that might be a shell metacharacter, then use eval to reverse 231 | # that process (while maintaining the separation between arguments), and wrap 232 | # the whole thing up as a single "set" statement. 233 | # 234 | # This will of course break if any of these variables contains a newline or 235 | # an unmatched quote. 236 | # 237 | 238 | eval "set -- $( 239 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 240 | xargs -n1 | 241 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 242 | tr '\n' ' ' 243 | )" '"$@"' 244 | 245 | exec "$JAVACMD" "$@" 246 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | mavenCentral() 8 | gradlePluginPortal() 9 | } 10 | } 11 | 12 | rootProject.name = "EarlyLoadingScreen" // avoid weirdness when building the project using another directory name 13 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/EarlyLaunch.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen; 2 | 3 | import com.ishland.earlyloadingscreen.platform_cl.AppLoaderAccessSupport; 4 | import com.ishland.earlyloadingscreen.platform_cl.Config; 5 | import com.ishland.earlyloadingscreen.platform_cl.LaunchPoint; 6 | import com.ishland.earlyloadingscreen.util.AppLoaderUtil; 7 | import net.bytebuddy.agent.ByteBuddyAgent; 8 | import net.fabricmc.loader.impl.FabricLoaderImpl; 9 | import net.fabricmc.loader.impl.game.GameProvider; 10 | import net.fabricmc.loader.impl.game.minecraft.MinecraftGameProvider; 11 | 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.lang.instrument.Instrumentation; 15 | import java.lang.reflect.Field; 16 | import java.lang.reflect.InvocationTargetException; 17 | import java.lang.reflect.Method; 18 | import java.nio.file.Path; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.Set; 22 | 23 | public class EarlyLaunch { 24 | 25 | static void load0(LaunchPoint point) { 26 | final ClassLoader classLoader = EarlyLaunch.class.getClassLoader(); 27 | Config.init(); 28 | 29 | if (Config.WINDOW_CREATION_POINT.ordinal() > point.ordinal()) { 30 | return; 31 | } 32 | System.out.println(String.format("Loading EarlyLoadingScreen early on ClassLoader %s", classLoader.getClass().getName())); 33 | 34 | final String earlyLaunchProperty = "earlyloadingscreen.duringEarlyLaunch"; 35 | try { 36 | System.setProperty(earlyLaunchProperty, "true"); 37 | final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); 38 | unlockLibraryOnKnot(contextClassLoader); 39 | if (contextClassLoader.getClass().isInstance(classLoader)) { 40 | launch(contextClassLoader.loadClass("com.ishland.earlyloadingscreen.Launch")); 41 | } else { 42 | System.out.println("Relaunching on context classloader"); 43 | final Instrumentation inst = ByteBuddyAgent.install(); 44 | inst.redefineModule( 45 | ModuleLayer.boot().findModule("java.base").get(), 46 | Set.of(), 47 | Map.of(), 48 | Map.of("java.lang", Set.of(EarlyLaunch.class.getModule())), 49 | Set.of(), 50 | Map.of() 51 | ); 52 | AppLoaderAccessSupport.class.getName(); 53 | AppLoaderAccessSupport.LoadingScreenAccessor.class.getName(); 54 | AppLoaderAccessSupport.ProgressHolderAccessor.class.getName(); 55 | final Class launchClass = defineClass(contextClassLoader, "com.ishland.earlyloadingscreen.Launch"); 56 | launch(launchClass); 57 | System.out.println("[EarlyLoadingScreen] Relaunched on context classloader"); 58 | } 59 | } catch (Throwable t) { 60 | System.clearProperty(earlyLaunchProperty); 61 | System.out.println("[EarlyLoadingScreen] Failed to launch early"); 62 | t.printStackTrace(); 63 | } 64 | } 65 | 66 | private static void launch(Class launchClass) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 67 | launchClass.getMethod("initAndCreateWindow", boolean.class).invoke(null, true); 68 | } 69 | 70 | private static void unlockLibraryOnKnot(ClassLoader knotClassLoader) { 71 | try { 72 | final Method getDelegate = knotClassLoader.getClass().getDeclaredMethod("getDelegate"); 73 | getDelegate.setAccessible(true); 74 | final Object knotClassLoaderDelegate = getDelegate.invoke(knotClassLoader); 75 | final Class delegateClazz = Class.forName("net.fabricmc.loader.impl.launch.knot.KnotClassDelegate"); 76 | 77 | final MinecraftGameProvider gameProvider = (MinecraftGameProvider) FabricLoaderImpl.INSTANCE.getGameProvider(); 78 | // final Field getLogJars = MinecraftGameProvider.class.getDeclaredField("logJars"); 79 | // getLogJars.setAccessible(true); 80 | // Set logJars = (Set) getLogJars.get(gameProvider); 81 | final Field getMiscGameLibraries = MinecraftGameProvider.class.getDeclaredField("miscGameLibraries"); 82 | getMiscGameLibraries.setAccessible(true); 83 | List miscGameLibraries = (List) getMiscGameLibraries.get(gameProvider); 84 | final Method setAllowedPrefixes = delegateClazz.getDeclaredMethod("setAllowedPrefixes", Path.class, String[].class); 85 | setAllowedPrefixes.setAccessible(true); 86 | final Method addCodeSource = delegateClazz.getDeclaredMethod("addCodeSource", Path.class); 87 | addCodeSource.setAccessible(true); 88 | for (Path library : miscGameLibraries) { 89 | setAllowedPrefixes.invoke(knotClassLoaderDelegate, library, new String[0]); 90 | addCodeSource.invoke(knotClassLoaderDelegate, library); 91 | } 92 | } catch (Throwable t) { 93 | throw new RuntimeException("Failed to unlock library on knot", t); 94 | } 95 | } 96 | 97 | // have to duplicate code here because of classloader issues 98 | public static Class defineClass(ClassLoader classLoader, String name) throws IllegalAccessException, InvocationTargetException, IOException, NoSuchMethodException { 99 | final Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); 100 | defineClass.setAccessible(true); 101 | return (Class) defineClass.invoke( 102 | classLoader, 103 | name, 104 | getClassFile(name), 105 | 0, 106 | getClassFile(name).length 107 | ); 108 | } 109 | 110 | private static byte[] getClassFile(String name) throws IOException { 111 | try (InputStream in = AppLoaderUtil.class.getClassLoader().getResourceAsStream(name.replace('.', '/') + ".class")) { 112 | return in.readAllBytes(); 113 | } 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/LanguageAdapterLaunch.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen; 2 | 3 | import com.ishland.earlyloadingscreen.platform_cl.LaunchPoint; 4 | import net.fabricmc.loader.api.LanguageAdapter; 5 | import net.fabricmc.loader.api.LanguageAdapterException; 6 | import net.fabricmc.loader.api.ModContainer; 7 | 8 | public class LanguageAdapterLaunch implements LanguageAdapter { 9 | 10 | static { 11 | EarlyLaunch.load0(LaunchPoint.postModLoading); 12 | } 13 | 14 | @Override 15 | public T create(ModContainer mod, String value, Class type) throws LanguageAdapterException { 16 | throw new RuntimeException("This should not be called"); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/Launch.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen; 2 | 3 | import com.ishland.earlyloadingscreen.patch.FabricLoaderInvokePatch; 4 | import com.ishland.earlyloadingscreen.patch.SodiumOSDetectionPatch; 5 | import com.ishland.earlyloadingscreen.platform_cl.Config; 6 | 7 | public class Launch { 8 | 9 | static { 10 | final ClassLoader classLoader = Launch.class.getClassLoader(); 11 | SharedConstants.LOGGER.info(String.format("Loading EarlyLoadingScreen on ClassLoader %s", classLoader.getClass().getName())); 12 | 13 | Config.init(); 14 | if (Config.ENABLE_ENTRYPOINT_INFORMATION) { 15 | FabricLoaderInvokePatch.init(); 16 | } 17 | SodiumOSDetectionPatch.init(); 18 | LoadingScreenManager.init(); 19 | } 20 | 21 | public static void init() { 22 | } 23 | 24 | public static void initAndCreateWindow(boolean wait) { 25 | LoadingScreenManager.createWindow(); 26 | if (wait) { 27 | LoadingScreenManager.spinWaitInit(); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/LoadingProgressManager.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen; 2 | 3 | import com.ishland.earlyloadingscreen.platform_cl.AppLoaderAccessSupport; 4 | 5 | import java.util.concurrent.CopyOnWriteArrayList; 6 | import java.util.concurrent.ScheduledFuture; 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.function.Supplier; 9 | 10 | public class LoadingProgressManager { 11 | 12 | private static final CopyOnWriteArrayList activeProgress = new CopyOnWriteArrayList<>(); 13 | 14 | static { 15 | ProgressHolder.class.getName(); // load class 16 | Progress.class.getName(); // load class 17 | } 18 | 19 | static CopyOnWriteArrayList getActiveProgress() { 20 | return activeProgress; 21 | } 22 | 23 | public static ProgressHolder tryCreateProgressHolder() { 24 | return new ProgressHolder(); 25 | } 26 | 27 | public static void showMessageAsProgress(String message) { 28 | showMessageAsProgress(message, 10000L); 29 | } 30 | 31 | public static void showMessageAsProgress(String message, long timeMillis) { 32 | final ProgressHolder holder = tryCreateProgressHolder(); 33 | if (holder != null) { 34 | final ScheduledFuture future = LoadingScreenManager.SCHEDULER.schedule(holder::close, timeMillis, TimeUnit.MILLISECONDS); 35 | holder.update(() -> String.format("(%ds) %s", future.getDelay(TimeUnit.SECONDS), message)); 36 | holder.updateProgress(() -> 1f - (float) future.getDelay(TimeUnit.MILLISECONDS) / (float) timeMillis); 37 | } 38 | } 39 | 40 | static class Progress { 41 | private volatile Supplier supplier; 42 | private String text = ""; 43 | private volatile Supplier progressSupplier; 44 | 45 | public void update(Supplier text) { 46 | this.supplier = text; 47 | } 48 | 49 | public void updateProgress(Supplier progressSupplier) { 50 | this.progressSupplier = progressSupplier; 51 | } 52 | 53 | public String get() { 54 | final Supplier supplier = this.supplier; 55 | if (supplier == null) return ""; 56 | return supplier.get(); 57 | } 58 | 59 | public float getProgress() { 60 | final Supplier floatSupplier = this.progressSupplier; 61 | return floatSupplier != null ? floatSupplier.get() : 0.0f; 62 | } 63 | 64 | private String get0() { 65 | try { 66 | return supplier.get(); 67 | } catch (Throwable t) { 68 | return "Error: " + t.getMessage(); 69 | } 70 | } 71 | 72 | } 73 | 74 | public static class ProgressHolder implements AppLoaderAccessSupport.ProgressHolderAccessor { 75 | 76 | private final Progress impl; 77 | 78 | public ProgressHolder() { 79 | Progress progress = this.impl = new Progress(); 80 | LoadingScreenManager.CLEANER.register(this, () -> { 81 | activeProgress.remove(progress); 82 | }); 83 | activeProgress.add(impl); 84 | } 85 | 86 | public void update(Supplier text) { 87 | impl.update(text); 88 | } 89 | 90 | public void updateProgress(Supplier progress) { 91 | impl.updateProgress(progress); 92 | } 93 | 94 | @Override 95 | public void close() { 96 | activeProgress.remove(impl); 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/LoadingScreenManager.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen; 2 | 3 | import com.google.common.base.Joiner; 4 | import com.google.common.collect.Lists; 5 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 6 | import com.ishland.earlyloadingscreen.platform_cl.AppLoaderAccessSupport; 7 | import com.ishland.earlyloadingscreen.platform_cl.Config; 8 | import com.ishland.earlyloadingscreen.platform_cl.PlatformUtil; 9 | import com.ishland.earlyloadingscreen.render.GLText; 10 | import com.ishland.earlyloadingscreen.render.Simple2DDraw; 11 | import com.ishland.earlyloadingscreen.util.WindowCreationUtil; 12 | import net.fabricmc.loader.api.FabricLoader; 13 | import net.fabricmc.loader.impl.FabricLoaderImpl; 14 | import net.fabricmc.loader.impl.util.Arguments; 15 | import org.jetbrains.annotations.NotNull; 16 | import org.lwjgl.PointerBuffer; 17 | import org.lwjgl.glfw.Callbacks; 18 | import org.lwjgl.glfw.GLFW; 19 | import org.lwjgl.glfw.GLFWErrorCallback; 20 | import org.lwjgl.opengl.GL; 21 | import org.lwjgl.system.MemoryStack; 22 | import org.lwjgl.system.MemoryUtil; 23 | import org.lwjgl.util.tinyfd.TinyFileDialogs; 24 | 25 | import java.io.Closeable; 26 | import java.io.IOException; 27 | import java.lang.ref.Cleaner; 28 | import java.util.LinkedHashSet; 29 | import java.util.List; 30 | import java.util.ListIterator; 31 | import java.util.Locale; 32 | import java.util.Set; 33 | import java.util.concurrent.ConcurrentLinkedQueue; 34 | import java.util.concurrent.CopyOnWriteArrayList; 35 | import java.util.concurrent.Executor; 36 | import java.util.concurrent.Executors; 37 | import java.util.concurrent.ScheduledExecutorService; 38 | import java.util.concurrent.ScheduledFuture; 39 | import java.util.concurrent.TimeUnit; 40 | import java.util.concurrent.atomic.AtomicBoolean; 41 | import java.util.concurrent.locks.LockSupport; 42 | import java.util.function.Supplier; 43 | 44 | import static com.ishland.earlyloadingscreen.SharedConstants.LOGGER; 45 | import static com.ishland.earlyloadingscreen.render.GLText.GLT_BOTTOM; 46 | import static com.ishland.earlyloadingscreen.render.GLText.GLT_LEFT; 47 | import static com.ishland.earlyloadingscreen.render.GLText.GLT_RIGHT; 48 | import static com.ishland.earlyloadingscreen.render.GLText.GLT_TOP; 49 | import static com.ishland.earlyloadingscreen.render.GLText.gltCreateText; 50 | import static com.ishland.earlyloadingscreen.render.GLText.gltGetLineHeight; 51 | import static com.ishland.earlyloadingscreen.render.GLText.gltGetTextHeight; 52 | import static com.ishland.earlyloadingscreen.render.GLText.gltSetText; 53 | import static org.lwjgl.glfw.GLFW.glfwGetFramebufferSize; 54 | import static org.lwjgl.glfw.GLFW.glfwGetWindowSize; 55 | import static org.lwjgl.glfw.GLFW.glfwPollEvents; 56 | import static org.lwjgl.opengl.GL11.GL_BLEND; 57 | import static org.lwjgl.opengl.GL11.GL_CULL_FACE; 58 | import static org.lwjgl.opengl.GL11.GL_DEPTH; 59 | import static org.lwjgl.opengl.GL11.glDisable; 60 | import static org.lwjgl.opengl.GL11.glEnable; 61 | import static org.lwjgl.opengl.GL11.glViewport; 62 | import static org.lwjgl.opengl.GL32.GL_COLOR_BUFFER_BIT; 63 | import static org.lwjgl.opengl.GL32.GL_DEPTH_BUFFER_BIT; 64 | import static org.lwjgl.opengl.GL32.glClear; 65 | import static org.lwjgl.opengl.GL32.glClearColor; 66 | 67 | public class LoadingScreenManager { 68 | 69 | public static final Cleaner CLEANER = Cleaner.create(); 70 | public static final ScheduledExecutorService SCHEDULER = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setDaemon(true).setNameFormat("EarlyLoadingScreen Scheduler").build()); 71 | 72 | private static long handle; 73 | public static final WindowEventLoop windowEventLoop; 74 | public static boolean eventLoopStarted = false; 75 | private static final Object windowEventLoopSync = new Object(); 76 | 77 | static { 78 | LOGGER.info("Initializing LoadingScreenManager..."); 79 | windowEventLoop = new WindowEventLoop(PlatformUtil.IS_WINDOWS); 80 | AppLoaderAccessSupport.setAccess(LoadingProgressManager::tryCreateProgressHolder); 81 | } 82 | 83 | public static void init() { 84 | // intentionally empty 85 | } 86 | 87 | public static void createWindow() { 88 | synchronized (windowEventLoopSync) { 89 | if (handle != 0L || eventLoopStarted) { 90 | // LOGGER.warn("createWindow() called twice", new Throwable()); 91 | return; 92 | } 93 | LOGGER.info("Creating early window..."); 94 | try { 95 | initGLFW(); 96 | if (!PlatformUtil.IS_WINDOWS) { 97 | handle = initWindow(); 98 | } 99 | } catch (Throwable t) { 100 | LOGGER.error("Failed to create early window", t); 101 | return; 102 | } 103 | eventLoopStarted = true; 104 | windowEventLoop.start(); 105 | glfwPollEvents(); 106 | } 107 | } 108 | 109 | public static long takeContext() { 110 | synchronized (windowEventLoopSync) { 111 | if (handle == 0L) { 112 | return 0L; 113 | } 114 | LOGGER.info("Handing early window to Minecraft..."); 115 | windowEventLoop.running.set(false); 116 | eventLoopStarted = true; 117 | while (windowEventLoop.isAlive()) { 118 | LockSupport.parkNanos("Waiting for window event loop to exit", 100_000L); 119 | } 120 | final long handle1 = handle; 121 | handle = 0L; 122 | // if (SharedConstants.REUSE_WINDOW) { 123 | // return handle1; 124 | // } else { 125 | // LOGGER.info("Destroying early window..."); 126 | // windowEventLoop.renderLoop = null; 127 | // glfwDestroyWindow(handle1); 128 | // return -1; 129 | // } 130 | if (!Config.REUSE_EARLY_WINDOW) { 131 | windowEventLoop.renderLoop = null; 132 | } 133 | return handle1; 134 | } 135 | } 136 | 137 | public static void reInitLoop() { 138 | synchronized (windowEventLoopSync) { 139 | if (windowEventLoop.renderLoop != null) { 140 | LOGGER.info("Reinitializing screen rendering..."); 141 | windowEventLoop.renderLoop = new RenderLoop(); 142 | } 143 | } 144 | } 145 | 146 | public record WindowSettings( 147 | int framebufferWidth, 148 | int framebufferHeight, 149 | int windowWidth, 150 | int windowHeight 151 | ) { 152 | } 153 | 154 | public static WindowSettings getWindowSettings() { 155 | return new WindowSettings( 156 | windowEventLoop.framebufferWidth, 157 | windowEventLoop.framebufferHeight, 158 | windowEventLoop.windowWidth, 159 | windowEventLoop.windowHeight 160 | ); 161 | } 162 | 163 | private static void initGLFW() { 164 | try (MemoryStack memoryStack = MemoryStack.stackPush()) { 165 | PointerBuffer pointerBuffer = memoryStack.mallocPointer(1); 166 | int i = GLFW.glfwGetError(pointerBuffer); 167 | if (i != 0) { 168 | long l = pointerBuffer.get(); 169 | String string1 = l == 0L ? "" : MemoryUtil.memUTF8(l); 170 | throw new IllegalStateException(String.format(Locale.ROOT, "GLFW error before init: [0x%X]%s", i, string1)); 171 | } 172 | } 173 | List list = Lists.newArrayList(); 174 | GLFWErrorCallback gLFWErrorCallback = GLFW.glfwSetErrorCallback( 175 | (code, pointer) -> list.add(String.format(Locale.ROOT, "GLFW error during init: [0x%X]%s", code, pointer)) 176 | ); 177 | if (!GLFW.glfwInit()) { 178 | throw new IllegalStateException("Failed to initialize GLFW, errors: " + Joiner.on(",").join(list)); 179 | } else { 180 | for(String string : list) { 181 | LOGGER.error("GLFW error collected during initialization: {}", string); 182 | } 183 | 184 | _glfwSetErrorCallback(gLFWErrorCallback); 185 | } 186 | } 187 | 188 | private static void _glfwSetErrorCallback(GLFWErrorCallback gLFWErrorCallback) { 189 | GLFWErrorCallback old = GLFW.glfwSetErrorCallback(gLFWErrorCallback); 190 | if (old != null) { 191 | old.free(); 192 | } 193 | } 194 | 195 | private static void throwGlError(int error, long description) { 196 | String string = "GLFW error " + error + ": " + MemoryUtil.memUTF8(description); 197 | TinyFileDialogs.tinyfd_messageBox( 198 | "Minecraft", string + ".\n\nPlease make sure you have up-to-date drivers (see aka.ms/mcdriver for instructions).", "ok", "error", false 199 | ); 200 | throw new RuntimeException(string); 201 | } 202 | 203 | private static long initWindow() { 204 | GLFW.glfwSetErrorCallback(LoadingScreenManager::throwGlError); 205 | GLFW.glfwDefaultWindowHints(); 206 | GLFW.glfwWindowHint(GLFW.GLFW_CLIENT_API, GLFW.GLFW_OPENGL_API); 207 | GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_CREATION_API, GLFW.GLFW_NATIVE_CONTEXT_API); 208 | GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); 209 | GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 2); 210 | GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); 211 | GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, 1); 212 | int configuredWidth = 854; 213 | int configuredHeight = 480; 214 | try { 215 | final Arguments arguments = FabricLoaderImpl.INSTANCE.getGameProvider().getArguments(); 216 | configuredWidth = Integer.parseInt(arguments.getOrDefault("width", "854")); 217 | configuredHeight = Integer.parseInt(arguments.getOrDefault("height", "480")); 218 | } catch (Throwable t) { 219 | LOGGER.error("Failed to load window configuration", t); 220 | } 221 | String minecraftVersion = "Unknown"; 222 | try { 223 | minecraftVersion = FabricLoader.getInstance().getModContainer("minecraft").map(modContainer -> modContainer.getMetadata().getVersion().getFriendlyString()).get(); 224 | } catch (Throwable t) { 225 | LOGGER.error("Failed to load Minecraft version", t); 226 | } 227 | final long handle = WindowCreationUtil.warpGlfwCreateWindow(configuredWidth, configuredHeight, "Minecraft* %s".formatted(minecraftVersion), 0L, 0L); 228 | // Center window 229 | // final long monitor = glfwGetPrimaryMonitor(); 230 | // if (monitor != 0L) { 231 | // final GLFWVidMode vidmode = glfwGetVideoMode(monitor); 232 | // if (vidmode != null) { 233 | // glfwSetWindowPos( 234 | // handle, 235 | // (vidmode.width() - configuredWidth) / 2, 236 | // (vidmode.height() - configuredHeight) / 2 237 | // ); 238 | // } 239 | // } 240 | 241 | int[] width0 = new int[1]; 242 | int[] height0 = new int[1]; 243 | glfwGetFramebufferSize(handle, width0, height0); 244 | windowEventLoop.framebufferWidth = width0[0]; 245 | windowEventLoop.framebufferHeight = height0[0]; 246 | glfwGetWindowSize(handle, width0, height0); 247 | windowEventLoop.windowWidth = width0[0]; 248 | windowEventLoop.windowHeight = height0[0]; 249 | GLFW.glfwSetFramebufferSizeCallback(handle, (window, width, height) -> { 250 | windowEventLoop.framebufferWidth = width; 251 | windowEventLoop.framebufferHeight = height; 252 | }); 253 | GLFW.glfwSetWindowPosCallback(handle, (window, xpos, ypos) -> {}); 254 | GLFW.glfwSetWindowSizeCallback(handle, (window, width, height) -> { 255 | windowEventLoop.windowWidth = width; 256 | windowEventLoop.windowHeight = height; 257 | }); 258 | GLFW.glfwSetWindowFocusCallback(handle, (window, focused) -> {}); 259 | GLFW.glfwSetCursorEnterCallback(handle, (window, entered) -> {}); 260 | GLFW.glfwSetWindowContentScaleCallback(handle, (window, xscale, yscale) -> { 261 | windowEventLoop.scale = Math.max(xscale, yscale); 262 | }); 263 | 264 | return handle; 265 | } 266 | 267 | public static void spinWaitInit() { 268 | if (!LoadingScreenManager.windowEventLoop.initialized) { 269 | while (!LoadingScreenManager.windowEventLoop.initialized && LoadingScreenManager.windowEventLoop.isAlive()) { 270 | // spin wait, should come very soon 271 | Thread.onSpinWait(); 272 | } 273 | } 274 | } 275 | 276 | public static class RenderLoop { 277 | 278 | public GLText glt = new GLText(); 279 | public final GLText.GLTtext memoryUsage = gltCreateText(); 280 | public final GLText.GLTtext fpsText = gltCreateText(); 281 | private final GLText.GLTtext progressText = gltCreateText(); 282 | public Simple2DDraw draw = new Simple2DDraw(); 283 | public final Simple2DDraw.BufferBuilder progressBars = draw.new BufferBuilder(); 284 | 285 | public void render(int width, int height, float scale) { 286 | // glfwGetFramebufferSize(glfwGetCurrentContext(), width, height); 287 | // glViewport(0, 0, width[0], height[0]); 288 | glt.gltViewport(width, height); 289 | draw.viewport(width, height); 290 | 291 | glEnable(GL_BLEND); 292 | 293 | final CopyOnWriteArrayList activeProgress = LoadingProgressManager.getActiveProgress(); 294 | synchronized (activeProgress) { 295 | progressBars.begin(); 296 | final ListIterator iterator = activeProgress.listIterator(activeProgress.size()); 297 | float y = height; 298 | final float textHeight = gltGetLineHeight(scale * 1.0F); 299 | while (iterator.hasPrevious()) { 300 | final LoadingProgressManager.Progress progress = iterator.previous(); 301 | if (progress.get().isBlank()) continue; 302 | y -= textHeight; 303 | final float progress1 = progress.getProgress(); 304 | if (progress1 > 0.0f) { 305 | progressBars.rect(0, y, progress1 * width, textHeight, 1, 1, 1, 1); 306 | } 307 | // y -= 1; 308 | } 309 | progressBars.end(); 310 | progressBars.draw(); 311 | } 312 | 313 | try (final Closeable ignored = glt.gltBeginDraw()) { 314 | glt.gltColor(1.0f, 1.0f, 1.0f, 1.0f); 315 | 316 | gltSetText(this.memoryUsage, "Memory: %d/%d MiB".formatted( 317 | (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024L / 1024L, 318 | Runtime.getRuntime().maxMemory() / 1024L / 1024L 319 | )); 320 | glt.gltDrawText2DAligned( 321 | this.memoryUsage, 322 | width, 323 | 0, 324 | scale * 1.0f, 325 | GLT_RIGHT, GLT_TOP 326 | ); 327 | glt.gltDrawText2DAligned( 328 | this.fpsText, 329 | 0, 330 | 0, 331 | scale * 1.0f, 332 | GLT_LEFT, GLT_TOP 333 | ); 334 | 335 | StringBuilder sb = new StringBuilder(); 336 | synchronized (activeProgress) { 337 | for (LoadingProgressManager.Progress progress : activeProgress) { 338 | final String str = progress.get(); 339 | if (str.isBlank()) continue; 340 | sb.append(str).append('\n'); 341 | } 342 | } 343 | gltSetText(this.progressText, sb.toString().trim()); 344 | glt.gltDrawText2DAligned( 345 | this.progressText, 346 | 0, 347 | height, 348 | scale * 1.0f, 349 | GLT_LEFT, GLT_BOTTOM 350 | ); 351 | 352 | } catch (IOException e) { 353 | throw new RuntimeException(e); // shouldn't happen 354 | } 355 | 356 | glDisable(GL_BLEND); 357 | } 358 | 359 | private void terminate() { 360 | glt.gltTerminate(); 361 | } 362 | 363 | } 364 | 365 | public static class WindowEventLoop extends Thread implements Executor { 366 | 367 | private final AtomicBoolean running = new AtomicBoolean(true); 368 | private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); 369 | 370 | private volatile boolean needsCreateWindow; 371 | private volatile boolean initialized = false; 372 | public volatile RenderLoop renderLoop = null; 373 | private volatile int framebufferWidth = 0; 374 | private volatile int framebufferHeight = 0; 375 | private volatile int windowWidth = 0; 376 | private volatile int windowHeight = 0; 377 | private volatile float scale = 1.0f; 378 | 379 | private WindowEventLoop(boolean needsCreateWindow) { 380 | super("EarlyLoadingScreen - Render Thread"); 381 | this.needsCreateWindow = needsCreateWindow; 382 | } 383 | 384 | @Override 385 | public void run() { 386 | try { 387 | long handle; 388 | if (needsCreateWindow) { 389 | LoadingScreenManager.handle = handle = initWindow(); 390 | } else { 391 | handle = LoadingScreenManager.handle; 392 | } 393 | GLFW.glfwMakeContextCurrent(handle); 394 | GL.createCapabilities(); 395 | glClearColor(0.0F, 0.0F, 0.0F, 0.0F); 396 | GLFW.glfwSwapInterval(1); 397 | 398 | RenderLoop renderLoop = this.renderLoop = new RenderLoop(); 399 | 400 | initialized = true; 401 | 402 | long lastFpsTime = System.nanoTime(); 403 | int fpsCounter = 0; 404 | int fps = 0; 405 | 406 | long lastFrameTime = System.nanoTime(); 407 | 408 | final GLText glt = renderLoop.glt; 409 | while (running.get()) { 410 | { 411 | Runnable runnable; 412 | while ((runnable = queue.poll()) != null) { 413 | try { 414 | runnable.run(); 415 | } catch (Throwable t) { 416 | t.printStackTrace(); 417 | } 418 | } 419 | } 420 | if (GLFW.glfwWindowShouldClose(handle)) { 421 | if (Config.ALLOW_EARLY_WINDOW_CLOSE) { 422 | GLFW.glfwDestroyWindow(handle); 423 | LOGGER.info("Early window closed"); 424 | System.exit(0); 425 | } else { 426 | GLFW.glfwSetWindowShouldClose(handle, false); 427 | } 428 | } 429 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 430 | 431 | glViewport(0, 0, framebufferWidth, framebufferHeight); 432 | renderLoop.render(framebufferWidth, framebufferHeight, scale); 433 | 434 | if (!PlatformUtil.IS_OSX) { 435 | GLFW.glfwPollEvents(); 436 | } 437 | GLFW.glfwSwapBuffers(handle); 438 | fpsCounter ++; 439 | final long currentTime = System.nanoTime(); 440 | if (currentTime - lastFpsTime >= 1_000_000_000L) { 441 | fps = (int) (fpsCounter * 1000_000_000L / (currentTime - lastFpsTime)); 442 | fpsCounter = 0; 443 | lastFpsTime = currentTime; 444 | gltSetText(renderLoop.fpsText, "%d fps".formatted(fps)); 445 | } 446 | while ((System.nanoTime() - lastFrameTime) + 100_000L < 1_000_000_000L / 60L) { 447 | LockSupport.parkNanos(100_000L); 448 | } 449 | lastFrameTime = System.nanoTime(); 450 | } 451 | } catch (Throwable t) { 452 | LOGGER.error("Failed to render early window, exiting", t); 453 | this.renderLoop = null; 454 | } finally { 455 | final long handle1 = handle; 456 | if (handle1 != 0L) { 457 | Callbacks.glfwFreeCallbacks(handle1); 458 | } else { 459 | LOGGER.warn("Early exit"); 460 | } 461 | GLFW.glfwMakeContextCurrent(0L); 462 | needsCreateWindow = false; 463 | initialized = true; 464 | } 465 | } 466 | 467 | @Override 468 | public void execute(@NotNull Runnable command) { 469 | queue.add(command); 470 | } 471 | 472 | public void setWindowTitle(CharSequence title) { 473 | if (handle != 0L) { 474 | if (needsCreateWindow) { 475 | this.execute(() -> GLFW.glfwSetWindowTitle(handle, title)); 476 | } else { 477 | GLFW.glfwSetWindowTitle(handle, title); 478 | } 479 | } 480 | } 481 | } 482 | 483 | } 484 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/MixinEarlyLaunch.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen; 2 | 3 | import com.ishland.earlyloadingscreen.EarlyLaunch; 4 | import com.ishland.earlyloadingscreen.platform_cl.LaunchPoint; 5 | 6 | public class MixinEarlyLaunch { 7 | 8 | public static final String SMALL_REMINDER = "The following \"Unable to register injection point\" can be safely ignored. "; 9 | 10 | static { 11 | EarlyLaunch.load0(LaunchPoint.mixinEarly); 12 | System.out.println(SMALL_REMINDER); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/PreLaunchHandler.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen; 2 | 3 | import com.ishland.earlyloadingscreen.platform_cl.Config; 4 | import com.ishland.earlyloadingscreen.platform_cl.LaunchPoint; 5 | import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint; 6 | 7 | public class PreLaunchHandler implements PreLaunchEntrypoint { 8 | @Override 9 | public void onPreLaunch() { 10 | System.clearProperty("earlyloadingscreen.duringEarlyLaunch"); 11 | if (Config.WINDOW_CREATION_POINT.ordinal() <= LaunchPoint.preLaunch.ordinal()) { 12 | Launch.initAndCreateWindow(false); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/SharedConstants.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | public class SharedConstants { 7 | public static final Logger LOGGER = LoggerFactory.getLogger("EarlyLoadingScreen"); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/TheMixinConfig.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen; 2 | 3 | import com.ishland.earlyloadingscreen.platform_cl.Config; 4 | import com.ishland.earlyloadingscreen.platform_cl.LaunchPoint; 5 | import org.objectweb.asm.tree.ClassNode; 6 | import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; 7 | import org.spongepowered.asm.mixin.extensibility.IMixinInfo; 8 | 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | public class TheMixinConfig implements IMixinConfigPlugin { 13 | @Override 14 | public void onLoad(String mixinPackage) { 15 | // MixinExtrasBootstrap.init(); 16 | Config.init(); 17 | if (Config.WINDOW_CREATION_POINT.ordinal() <= LaunchPoint.mixinLoad.ordinal()) { 18 | Launch.initAndCreateWindow(false); 19 | } 20 | } 21 | 22 | @Override 23 | public String getRefMapperConfig() { 24 | return null; 25 | } 26 | 27 | @Override 28 | public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { 29 | final String packageName = "com.ishland.earlyloadingscreen.mixin."; 30 | if (mixinClassName.startsWith(packageName)) { 31 | final String name = mixinClassName.substring(packageName.length()); 32 | for (String disabledMixin : Config.DISABLED_MIXINS) { 33 | if (name.startsWith(disabledMixin + ".") || name.equals(disabledMixin)) { 34 | SharedConstants.LOGGER.info("Disabling mixin {} due to config", mixinClassName); 35 | return false; 36 | } 37 | } 38 | 39 | } 40 | return true; 41 | } 42 | 43 | @Override 44 | public void acceptTargets(Set myTargets, Set otherTargets) { 45 | 46 | } 47 | 48 | @Override 49 | public List getMixins() { 50 | return null; 51 | } 52 | 53 | @Override 54 | public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { 55 | 56 | } 57 | 58 | @Override 59 | public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/TheMod.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen; 2 | 3 | import com.ishland.earlyloadingscreen.platform_cl.Config; 4 | import net.fabricmc.api.ModInitializer; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.spongepowered.asm.mixin.MixinEnvironment; 8 | import org.spongepowered.asm.service.MixinService; 9 | 10 | import java.lang.reflect.Field; 11 | import java.lang.reflect.Method; 12 | import java.util.HashSet; 13 | import java.util.List; 14 | import java.util.Set; 15 | 16 | public class TheMod implements ModInitializer { 17 | 18 | @Override 19 | public void onInitialize() { 20 | if (Config.ENABLE_MIXIN_PRETRANSFORM) { 21 | auditMixins(); 22 | } 23 | } 24 | 25 | private static void auditMixins() { 26 | Logger auditLogger = LoggerFactory.getLogger("EarlyLoadingScreen Mixin Audit"); 27 | try { 28 | final Class transformerClazz = Class.forName("org.spongepowered.asm.mixin.transformer.MixinTransformer"); 29 | if (transformerClazz.isInstance(MixinEnvironment.getCurrentEnvironment().getActiveTransformer())) { 30 | final Field processorField = transformerClazz.getDeclaredField("processor"); 31 | processorField.setAccessible(true); 32 | final Object processor = processorField.get(MixinEnvironment.getCurrentEnvironment().getActiveTransformer()); 33 | final Class processorClazz = Class.forName("org.spongepowered.asm.mixin.transformer.MixinProcessor"); 34 | final Field configsField = processorClazz.getDeclaredField("configs"); 35 | configsField.setAccessible(true); 36 | final List configs = (List) configsField.get(processor); 37 | final Class configClazz = Class.forName("org.spongepowered.asm.mixin.transformer.MixinConfig"); 38 | final Method getUnhandledTargetsMethod = configClazz.getDeclaredMethod("getUnhandledTargets"); 39 | getUnhandledTargetsMethod.setAccessible(true); 40 | Set unhandled = new HashSet<>(); 41 | for (Object config : configs) { 42 | final Set unhandledTargets = (Set) getUnhandledTargetsMethod.invoke(config); 43 | unhandled.addAll(unhandledTargets); 44 | } 45 | try (LoadingProgressManager.ProgressHolder progressHolder = LoadingProgressManager.tryCreateProgressHolder()) { 46 | int index = 0; 47 | int total = unhandled.size(); 48 | for (String s : unhandled) { 49 | if (progressHolder != null) { 50 | int finalIndex = index; 51 | progressHolder.update(() -> String.format("Loading class (%d/%d): %s ", finalIndex, total, s)); 52 | } 53 | MixinService.getService().getClassProvider().findClass(s, false); 54 | index ++; 55 | } 56 | } 57 | for (Object config : configs) { 58 | for (String unhandledTarget : (Set) getUnhandledTargetsMethod.invoke(config)) { 59 | auditLogger.error("{} is already classloaded", unhandledTarget); 60 | } 61 | } 62 | } 63 | } catch (Throwable t) { 64 | throw new RuntimeException("Failed to audit mixins", t); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/mixin/MixinMinecraftClient.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.mixin; 2 | 3 | import com.ishland.earlyloadingscreen.Launch; 4 | import com.ishland.earlyloadingscreen.LoadingScreenManager; 5 | import com.ishland.earlyloadingscreen.SharedConstants; 6 | import com.ishland.earlyloadingscreen.platform_cl.Config; 7 | import com.ishland.earlyloadingscreen.platform_cl.LaunchPoint; 8 | import net.minecraft.client.MinecraftClient; 9 | import net.minecraft.util.ModStatus; 10 | import org.objectweb.asm.Opcodes; 11 | import org.spongepowered.asm.mixin.Mixin; 12 | import org.spongepowered.asm.mixin.Shadow; 13 | import org.spongepowered.asm.mixin.injection.At; 14 | import org.spongepowered.asm.mixin.injection.Inject; 15 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 16 | 17 | @Mixin(MinecraftClient.class) 18 | public abstract class MixinMinecraftClient { 19 | 20 | @Shadow protected abstract String getWindowTitle(); 21 | 22 | @Shadow 23 | public static ModStatus getModStatus() { 24 | throw new AbstractMethodError(); 25 | } 26 | 27 | @Inject(method = "", at = @At(value = "FIELD", target = "Lnet/minecraft/client/MinecraftClient;instance:Lnet/minecraft/client/MinecraftClient;", opcode = Opcodes.PUTSTATIC, shift = At.Shift.AFTER)) 28 | private void earlyInit(CallbackInfo ci) { 29 | String windowTitle; 30 | try { 31 | StringBuilder stringBuilder = new StringBuilder("Minecraft"); 32 | final ModStatus modStatus = getModStatus(); 33 | if (modStatus != null && modStatus.isModded()) { 34 | stringBuilder.append("*"); 35 | } 36 | 37 | stringBuilder.append(" "); 38 | stringBuilder.append(net.minecraft.SharedConstants.getGameVersion().getName()); 39 | windowTitle = stringBuilder.toString(); 40 | } catch (Throwable t) { 41 | SharedConstants.LOGGER.error("Failed to get window title", t); 42 | windowTitle = "Minecraft"; 43 | } 44 | if (Config.WINDOW_CREATION_POINT.ordinal() <= LaunchPoint.mcEarly.ordinal()) { 45 | Launch.initAndCreateWindow(false); 46 | LoadingScreenManager.windowEventLoop.setWindowTitle(windowTitle); 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/mixin/MixinSplashOverlay.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.mixin; 2 | 3 | import com.ishland.earlyloadingscreen.LoadingProgressManager; 4 | import com.ishland.earlyloadingscreen.LoadingScreenManager; 5 | import com.ishland.earlyloadingscreen.mixin.access.ISimpleResourceReload; 6 | import net.minecraft.client.MinecraftClient; 7 | import net.minecraft.client.gui.screen.SplashOverlay; 8 | import net.minecraft.client.util.Window; 9 | import net.minecraft.resource.ResourceReload; 10 | import net.minecraft.resource.SimpleResourceReload; 11 | import org.spongepowered.asm.mixin.Final; 12 | import org.spongepowered.asm.mixin.Mixin; 13 | import org.spongepowered.asm.mixin.Shadow; 14 | import org.spongepowered.asm.mixin.injection.At; 15 | import org.spongepowered.asm.mixin.injection.Inject; 16 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 17 | 18 | import java.util.Arrays; 19 | 20 | import static com.ishland.earlyloadingscreen.render.GLText.gltSetText; 21 | 22 | @Mixin(value = SplashOverlay.class, priority = 1010) 23 | public class MixinSplashOverlay { 24 | 25 | @Shadow @Final private ResourceReload reload; 26 | 27 | private LoadingProgressManager.ProgressHolder progressHolder; 28 | 29 | @Inject(method = "", at = @At("RETURN")) 30 | private void onInit(CallbackInfo ci) { 31 | if (this.reload instanceof SimpleResourceReload) { 32 | LoadingProgressManager.ProgressHolder progressHolder = LoadingProgressManager.tryCreateProgressHolder(); 33 | if (progressHolder != null) { 34 | this.progressHolder = progressHolder; 35 | this.reload.whenComplete().thenRun(progressHolder::close); 36 | } 37 | } 38 | } 39 | 40 | @Inject(method = "render", at = @At(value = "RETURN")) 41 | private void postRender(CallbackInfo ci) { 42 | final LoadingScreenManager.RenderLoop renderLoop = LoadingScreenManager.windowEventLoop.renderLoop; 43 | if (renderLoop != null) { 44 | if (this.progressHolder != null && this.reload instanceof SimpleResourceReload simpleResourceReload) { 45 | this.progressHolder.update(() -> "Pending reloads: " + Arrays.toString(((ISimpleResourceReload) simpleResourceReload).getWaitingReloaders().toArray())); 46 | } 47 | final MinecraftClient client = MinecraftClient.getInstance(); 48 | if (client != null) { 49 | gltSetText(renderLoop.fpsText, "%d fps".formatted(client.getCurrentFps())); 50 | } else { 51 | gltSetText(renderLoop.fpsText, ""); 52 | } 53 | final Window window = MinecraftClient.getInstance().getWindow(); 54 | renderLoop.render(window.getFramebufferWidth(), window.getFramebufferHeight(), (float) window.getScaleFactor() / 2.0f); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/mixin/MixinWindow.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.mixin; 2 | 3 | import com.ishland.earlyloadingscreen.Launch; 4 | import com.ishland.earlyloadingscreen.platform_cl.Config; 5 | import com.ishland.earlyloadingscreen.LoadingScreenManager; 6 | import com.ishland.earlyloadingscreen.SharedConstants; 7 | import com.ishland.earlyloadingscreen.platform_cl.LaunchPoint; 8 | import com.ishland.earlyloadingscreen.util.WindowCreationUtil; 9 | import net.minecraft.client.util.Window; 10 | import org.lwjgl.glfw.GLFW; 11 | import org.lwjgl.opengl.GL; 12 | import org.spongepowered.asm.mixin.Final; 13 | import org.spongepowered.asm.mixin.Mixin; 14 | import org.spongepowered.asm.mixin.Shadow; 15 | import org.spongepowered.asm.mixin.Unique; 16 | import org.spongepowered.asm.mixin.injection.At; 17 | import org.spongepowered.asm.mixin.injection.Inject; 18 | import org.spongepowered.asm.mixin.injection.Redirect; 19 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 20 | 21 | @Mixin(Window.class) 22 | public class MixinWindow { 23 | 24 | @Shadow private int width; 25 | 26 | @Shadow private int height; 27 | 28 | @Shadow private int windowedHeight; 29 | 30 | @Shadow private int windowedWidth; 31 | 32 | @Redirect(method = "", at = @At(value = "INVOKE", target = "Lorg/lwjgl/glfw/GLFW;glfwCreateWindow(IILjava/lang/CharSequence;JJ)J")) 33 | private long redirectCreateWindow(int width, int height, CharSequence title, long monitor, long share) { 34 | // if (true) { 35 | // while (true) { 36 | // LockSupport.park(); 37 | // } 38 | // } 39 | if (Config.WINDOW_CREATION_POINT.ordinal() <= LaunchPoint.off.ordinal()) { 40 | Launch.init(); 41 | } 42 | if (Config.WINDOW_CREATION_POINT == LaunchPoint.off) { 43 | final long newHandle = WindowCreationUtil.warpGlfwCreateWindow(width, height, title, monitor, share); 44 | initGLFWHandle(newHandle); 45 | return newHandle; 46 | } 47 | final long context = LoadingScreenManager.takeContext(); 48 | if (context != 0L) { 49 | if (Config.REUSE_EARLY_WINDOW) { 50 | // GLFW.glfwSetWindowSize(context, width, height); 51 | GLFW.glfwSetWindowTitle(context, title); 52 | return context; 53 | } else { 54 | final long newHandle = WindowCreationUtil.warpGlfwCreateWindow(width, height, title, monitor, share); 55 | initGLFWHandle(newHandle); 56 | SharedConstants.LOGGER.info("Destroying early window"); 57 | GLFW.glfwDestroyWindow(context); 58 | return newHandle; 59 | } 60 | } else { 61 | return WindowCreationUtil.warpGlfwCreateWindow(width, height, title, monitor, share); 62 | } 63 | } 64 | 65 | @Unique 66 | private static void initGLFWHandle(long newHandle) { 67 | GLFW.glfwMakeContextCurrent(newHandle); 68 | GL.createCapabilities(); 69 | GLFW.glfwSwapBuffers(newHandle); 70 | LoadingScreenManager.reInitLoop(); 71 | GLFW.glfwMakeContextCurrent(0L); 72 | } 73 | 74 | @Inject(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/Window;updateWindowRegion()V")) 75 | private void syncSettingsFromEarlyWindow(CallbackInfo ci) { 76 | if (Config.REUSE_EARLY_WINDOW) { 77 | final LoadingScreenManager.WindowSettings settings = LoadingScreenManager.getWindowSettings(); 78 | this.windowedWidth = this.width = settings.windowWidth(); 79 | this.windowedHeight = this.height = settings.windowHeight(); 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/mixin/access/ISimpleResourceReload.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.mixin.access; 2 | 3 | import net.minecraft.resource.ResourceReloader; 4 | import net.minecraft.resource.SimpleResourceReload; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.gen.Accessor; 7 | 8 | import java.util.Set; 9 | 10 | @Mixin(SimpleResourceReload.class) 11 | public interface ISimpleResourceReload { 12 | 13 | @Accessor 14 | Set getWaitingReloaders(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/mixin/access/ITextureStitcherHolder.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.mixin.access; 2 | 3 | import net.minecraft.client.texture.TextureStitcher; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | import org.spongepowered.asm.mixin.gen.Accessor; 6 | 7 | @Mixin(TextureStitcher.Holder.class) 8 | public interface ITextureStitcherHolder { 9 | 10 | @Accessor 11 | T getSprite(); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/mixin/progress/MixinAtlasLoader.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.mixin.progress; 2 | 3 | import net.minecraft.client.texture.atlas.AtlasLoader; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | 6 | @Mixin(AtlasLoader.class) 7 | public class MixinAtlasLoader { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/mixin/progress/MixinBakedModelManager.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.mixin.progress; 2 | 3 | import com.ishland.earlyloadingscreen.util.ProgressUtil; 4 | import net.minecraft.client.render.model.BakedModelManager; 5 | import net.minecraft.util.Util; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.injection.At; 8 | import org.spongepowered.asm.mixin.injection.Inject; 9 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 10 | import org.spongepowered.asm.mixin.injection.callback.LocalCapture; 11 | 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.concurrent.CompletableFuture; 15 | import java.util.concurrent.CompletionStage; 16 | import java.util.concurrent.Executor; 17 | 18 | @Mixin(BakedModelManager.class) 19 | public class MixinBakedModelManager { 20 | 21 | @Inject(method = "method_45899", at = @At(value = "RETURN"), locals = LocalCapture.CAPTURE_FAILSOFT) 22 | private static void captureLoadModels(Executor unused, Map unused1, CallbackInfoReturnable> cir, List> list) { 23 | ProgressUtil.createProgress(list, cir.getReturnValue(), "models"); 24 | } 25 | 26 | @Inject(method = "method_45893", at = @At(value = "RETURN"), locals = LocalCapture.CAPTURE_FAILSOFT) 27 | private static void captureLoadBlockStates(Executor unused, Map unused1, CallbackInfoReturnable> cir, List> list) { 28 | ProgressUtil.createProgress(list, cir.getReturnValue(), "block states"); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/mixin/progress/MixinModelLoader.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.mixin.progress; 2 | 3 | import com.ishland.earlyloadingscreen.LoadingProgressManager; 4 | import net.minecraft.block.Block; 5 | import net.minecraft.block.BlockState; 6 | import net.minecraft.client.render.model.ModelLoader; 7 | import net.minecraft.client.render.model.UnbakedModel; 8 | import net.minecraft.client.util.ModelIdentifier; 9 | import net.minecraft.registry.Registries; 10 | import net.minecraft.state.StateManager; 11 | import net.minecraft.util.Identifier; 12 | import org.spongepowered.asm.mixin.Final; 13 | import org.spongepowered.asm.mixin.Mixin; 14 | import org.spongepowered.asm.mixin.Shadow; 15 | import org.spongepowered.asm.mixin.injection.At; 16 | import org.spongepowered.asm.mixin.injection.Inject; 17 | import org.spongepowered.asm.mixin.injection.Redirect; 18 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 19 | 20 | import java.util.Map; 21 | import java.util.function.BiConsumer; 22 | 23 | @Mixin(ModelLoader.class) 24 | public abstract class MixinModelLoader { 25 | 26 | @Shadow @Final private Map modelsToBake; 27 | private LoadingProgressManager.ProgressHolder modelLoadProgressHolder; 28 | private LoadingProgressManager.ProgressHolder modelAdditionalLoadProgressHolder; 29 | private int modelLoadProgress = 0; 30 | private int modelLoadTotalEstimate; 31 | private int modelDependencyResolveProgress = 0; 32 | 33 | @Inject(method = "", at = @At(value = "INVOKE", target = "Ljava/lang/Object;()V", shift = At.Shift.AFTER)) 34 | private void earlyInit(CallbackInfo ci) { 35 | modelLoadProgressHolder = LoadingProgressManager.tryCreateProgressHolder(); 36 | modelAdditionalLoadProgressHolder = LoadingProgressManager.tryCreateProgressHolder(); 37 | if (modelLoadProgressHolder != null) { 38 | modelLoadProgressHolder.update(() -> "Preparing models..."); 39 | } 40 | for(Block block : Registries.BLOCK) { 41 | modelLoadTotalEstimate += block.getStateManager().getStates().size(); 42 | } 43 | modelLoadTotalEstimate += Registries.ITEM.getIds().size(); 44 | modelLoadTotalEstimate += 6; 45 | } 46 | 47 | @Inject(method = "", at = @At("RETURN")) 48 | private void postInit(CallbackInfo ci) { 49 | if (modelLoadProgressHolder != null) { 50 | modelLoadProgressHolder.close(); 51 | modelLoadProgressHolder = null; 52 | } 53 | if (modelAdditionalLoadProgressHolder != null) { 54 | modelAdditionalLoadProgressHolder.close(); 55 | modelAdditionalLoadProgressHolder = null; 56 | } 57 | } 58 | 59 | @Inject(method = "add", at = @At("HEAD")) 60 | private void progressAddModel(ModelIdentifier id, UnbakedModel model, CallbackInfo ci) { 61 | this.modelLoadProgress ++; 62 | if (modelLoadProgressHolder != null) { 63 | modelLoadProgressHolder.update(() -> String.format("Loading model (%d/~%d): %s", this.modelLoadProgress, this.modelLoadTotalEstimate, id)); 64 | modelLoadProgressHolder.updateProgress(() -> (float) this.modelLoadProgress / (float) this.modelLoadTotalEstimate); 65 | } 66 | } 67 | 68 | @Inject(method = "method_45875", at = @At("HEAD")) 69 | private void progressModelResolution(UnbakedModel model, CallbackInfo ci) { 70 | this.modelDependencyResolveProgress ++; 71 | if (modelLoadProgressHolder != null) { 72 | final int size = this.modelsToBake.size(); 73 | modelLoadProgressHolder.update(() -> String.format("Resolving model dependencies (%d/%d): %s", this.modelDependencyResolveProgress, size, model)); 74 | modelLoadProgressHolder.updateProgress(() -> (float) this.modelDependencyResolveProgress / (float) size); 75 | } 76 | } 77 | 78 | @Redirect(method = "bake", at = @At(value = "INVOKE", target = "Ljava/util/Map;forEach(Ljava/util/function/BiConsumer;)V")) 79 | private void redirectIteration(Map instance, BiConsumer consumer) { 80 | try (LoadingProgressManager.ProgressHolder progressHolder = LoadingProgressManager.tryCreateProgressHolder()) { 81 | int index = 0; 82 | int size = instance.size(); 83 | for (Map.Entry entry : instance.entrySet()) { 84 | final ModelIdentifier identifier = entry.getKey(); 85 | final UnbakedModel model = entry.getValue(); 86 | if (progressHolder != null) { 87 | int finalIndex = index; 88 | progressHolder.update(() -> String.format("Baking model (%d/%d): %s", finalIndex, size, identifier)); 89 | progressHolder.updateProgress(() -> (float) finalIndex / (float) size); 90 | } 91 | index++; 92 | consumer.accept(identifier, model); 93 | } 94 | } 95 | 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/mixin/progress/MixinSpriteLoader.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.mixin.progress; 2 | 3 | import com.ishland.earlyloadingscreen.util.ProgressUtil; 4 | import net.minecraft.client.texture.SpriteContents; 5 | import net.minecraft.client.texture.SpriteLoader; 6 | import net.minecraft.client.texture.SpriteOpener; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.Redirect; 11 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 12 | import org.spongepowered.asm.mixin.injection.callback.LocalCapture; 13 | 14 | import java.util.Iterator; 15 | import java.util.List; 16 | import java.util.concurrent.CompletableFuture; 17 | import java.util.concurrent.Executor; 18 | import java.util.function.Function; 19 | import java.util.function.Supplier; 20 | 21 | @Mixin(SpriteLoader.class) 22 | public class MixinSpriteLoader { 23 | 24 | @Inject(method = "loadAll", at = @At("RETURN"), locals = LocalCapture.CAPTURE_FAILSOFT) 25 | private static void captureSpriteLoad(SpriteOpener opener, List> sources, Executor executor, CallbackInfoReturnable>> cir, List> list) { 26 | ProgressUtil.createProgress(list, cir.getReturnValue(), "sprites"); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/mixin/progress/MixinTextureManager.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.mixin.progress; 2 | 3 | import net.minecraft.client.texture.TextureManager; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | 6 | @Mixin(TextureManager.class) 7 | public class MixinTextureManager { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/mixin/progress/MixinTextureStitcher.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.mixin.progress; 2 | 3 | import com.ishland.earlyloadingscreen.LoadingProgressManager; 4 | import com.ishland.earlyloadingscreen.mixin.access.ITextureStitcherHolder; 5 | import net.minecraft.client.texture.TextureStitcher; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.injection.At; 8 | import org.spongepowered.asm.mixin.injection.Inject; 9 | import org.spongepowered.asm.mixin.injection.Redirect; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 11 | import org.spongepowered.asm.mixin.injection.callback.LocalCapture; 12 | 13 | import java.util.Iterator; 14 | import java.util.List; 15 | import java.util.ListIterator; 16 | 17 | @Mixin(TextureStitcher.class) 18 | public class MixinTextureStitcher { 19 | 20 | private LoadingProgressManager.ProgressHolder progressHolder; 21 | 22 | @Inject(method = "stitch", at = @At("HEAD")) 23 | private void preStitch(CallbackInfo ci) { 24 | progressHolder = LoadingProgressManager.tryCreateProgressHolder(); 25 | if (progressHolder != null) { 26 | progressHolder.update(() -> "Stitiching textures..."); 27 | } 28 | } 29 | 30 | @Redirect(method = "stitch", at = @At(value = "INVOKE", target = "Ljava/util/List;iterator()Ljava/util/Iterator;")) 31 | private Iterator redirectStitchIterator(List instance) { 32 | return instance.listIterator(); 33 | } 34 | 35 | @Inject(method = "stitch", at = @At(value = "INVOKE", target = "Ljava/util/Iterator;next()Ljava/lang/Object;", shift = At.Shift.BY, by = 3), locals = LocalCapture.CAPTURE_FAILSOFT) 36 | private void monitorStitch(CallbackInfo ci, List> list, Iterator> var2, TextureStitcher.Holder holder) { 37 | if (progressHolder != null && var2 instanceof ListIterator> iterator) { 38 | final int idx = iterator.previousIndex(); 39 | final int size = list.size(); 40 | progressHolder.update(() -> "Stitiching textures (%d/%d): %s".formatted(idx, size, ((ITextureStitcherHolder) holder).getSprite().getId())); 41 | progressHolder.updateProgress(() -> idx / (float) size); 42 | } 43 | } 44 | 45 | @Inject(method = "stitch", at = @At("RETURN")) 46 | private void postStitch(CallbackInfo ci) { 47 | if (progressHolder != null) progressHolder.close(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/patch/BytecodeTransformer.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.patch; 2 | 3 | import org.objectweb.asm.tree.ClassNode; 4 | 5 | public interface BytecodeTransformer { 6 | 7 | boolean transform(String className, ClassNode node); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/patch/FabricLoaderInvokePatch.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.patch; 2 | 3 | import com.ishland.earlyloadingscreen.LoadingProgressManager; 4 | import com.ishland.earlyloadingscreen.LoadingScreenManager; 5 | import com.ishland.earlyloadingscreen.SharedConstants; 6 | import com.ishland.earlyloadingscreen.platform_cl.AppLoaderAccessSupport; 7 | import com.ishland.earlyloadingscreen.util.AppLoaderUtil; 8 | import net.fabricmc.loader.impl.FabricLoaderImpl; 9 | import org.objectweb.asm.Opcodes; 10 | import org.objectweb.asm.Type; 11 | import org.objectweb.asm.tree.AbstractInsnNode; 12 | import org.objectweb.asm.tree.ClassNode; 13 | import org.objectweb.asm.tree.FieldInsnNode; 14 | import org.objectweb.asm.tree.InsnNode; 15 | import org.objectweb.asm.tree.JumpInsnNode; 16 | import org.objectweb.asm.tree.LabelNode; 17 | import org.objectweb.asm.tree.LdcInsnNode; 18 | import org.objectweb.asm.tree.LocalVariableNode; 19 | import org.objectweb.asm.tree.MethodInsnNode; 20 | import org.objectweb.asm.tree.MethodNode; 21 | import org.objectweb.asm.tree.TypeInsnNode; 22 | import org.objectweb.asm.tree.VarInsnNode; 23 | 24 | import java.lang.instrument.Instrumentation; 25 | import java.nio.file.Files; 26 | import java.nio.file.Path; 27 | import java.util.Comparator; 28 | import java.util.Iterator; 29 | import java.util.ListIterator; 30 | import java.util.Map; 31 | import java.util.Set; 32 | 33 | public class FabricLoaderInvokePatch implements BytecodeTransformer { 34 | 35 | private static final FabricLoaderInvokePatch INSTANCE = new FabricLoaderInvokePatch(); 36 | 37 | private FabricLoaderInvokePatch() { 38 | } 39 | 40 | static { 41 | initTransformer(); 42 | } 43 | 44 | public static void init() { 45 | // intentionally empty, just to trigger static initialization 46 | } 47 | 48 | private static void initTransformer() { 49 | Instrumentation inst = PatchUtil.instrumentation; 50 | if (inst == null) { 51 | SharedConstants.LOGGER.warn("Instrumentation unavailable, entrypoint information will not be available"); 52 | LoadingProgressManager.showMessageAsProgress("Instrumentation unavailable, entrypoint information will not be available"); 53 | return; 54 | } 55 | try { 56 | updateAppLoaderAccess(inst); 57 | } catch (Throwable t) { 58 | SharedConstants.LOGGER.warn("Failed to update AppLoader access", t); 59 | } 60 | try { 61 | AppLoaderUtil.init(); 62 | } catch (Throwable t) { 63 | SharedConstants.LOGGER.warn("Failed to define classes on AppClassLoader, entrypoint information will not be available", t); 64 | LoadingProgressManager.showMessageAsProgress("Failed to define classes on AppClassLoader, entrypoint information will not be available"); 65 | return; 66 | } 67 | PatchUtil.transformers.add(INSTANCE); 68 | try { 69 | inst.retransformClasses(Class.forName("net.fabricmc.loader.impl.launch.knot.KnotClassDelegate")); 70 | inst.retransformClasses(FabricLoaderImpl.class); 71 | } catch (Throwable t) { 72 | SharedConstants.LOGGER.warn("Failed to retransform FabricLoaderImpl, attempting to revert changes", t); 73 | LoadingProgressManager.showMessageAsProgress("Failed to retransform EntrypointUtils, entrypoint information will not be available"); 74 | PatchUtil.transformers.remove(INSTANCE); 75 | try { 76 | inst.retransformClasses(Class.forName("net.fabricmc.loader.impl.launch.knot.KnotClassDelegate")); 77 | } catch (Throwable t2) { 78 | SharedConstants.LOGGER.warn("Failed to revert changes to EntrypointUtils", t2); 79 | } 80 | try { 81 | inst.retransformClasses(FabricLoaderImpl.class); 82 | } catch (Throwable t2) { 83 | SharedConstants.LOGGER.warn("Failed to revert changes to FabricLoaderImpl", t2); 84 | } 85 | } 86 | } 87 | 88 | private static void updateAppLoaderAccess(Instrumentation inst) { 89 | inst.redefineModule( 90 | ModuleLayer.boot().findModule("java.base").get(), 91 | Set.of(), 92 | Map.of(), 93 | Map.of("java.lang", Set.of(PatchUtil.class.getModule())), 94 | Set.of(), 95 | Map.of() 96 | ); 97 | } 98 | 99 | @Override 100 | public boolean transform(String className, ClassNode node) { 101 | 102 | // patch net/fabricmc/loader/impl/FabricLoaderImpl 103 | if (className.equals("net/fabricmc/loader/impl/FabricLoaderImpl")) { 104 | SharedConstants.LOGGER.info("Patching FabricLoaderImpl for entrypoint information"); 105 | 106 | for (MethodNode method : node.methods) { 107 | // Lnet/fabricmc/loader/impl/FabricLoaderImpl;invokeEntrypoints(Ljava/lang/String;Ljava/lang/Class;Ljava/util/function/Consumer;)V 108 | if (method.name.equals("invokeEntrypoints") && method.desc.equals("(Ljava/lang/String;Ljava/lang/Class;Ljava/util/function/Consumer;)V")) { 109 | SharedConstants.LOGGER.info("Patching FabricLoaderImpl.invokeEntrypoints"); 110 | 111 | // add local var for progress tracker 112 | // use the same range as the first local var 113 | final LocalVariableNode firstLocalVar = method.localVariables.get(0); // var0 is `this` 114 | final int progressTrackerIndex = method.maxLocals++; 115 | method.visitLocalVariable( 116 | "early_loading_screen$progressTracker", 117 | Type.getDescriptor(AppLoaderAccessSupport.ProgressHolderAccessor.class), 118 | null, 119 | firstLocalVar.start.getLabel(), 120 | firstLocalVar.end.getLabel(), 121 | progressTrackerIndex 122 | ); 123 | 124 | int iteratorVarIndex = -1; 125 | int listVarIndex = -1; 126 | 127 | final ListIterator iterator = method.instructions.iterator(); 128 | while (iterator.hasNext()) { 129 | final AbstractInsnNode insn = iterator.next(); 130 | 131 | if (insn instanceof MethodInsnNode methodInsnNode) { 132 | 133 | // wrap getEntryPointContainers return value with ArrayList 134 | // target insn: INVOKEVIRTUAL net/fabricmc/loader/impl/FabricLoaderImpl.getEntrypointContainers (Ljava/lang/String;Ljava/lang/Class;)Ljava/util/List; 135 | if (methodInsnNode.owner.equals("net/fabricmc/loader/impl/FabricLoaderImpl") && methodInsnNode.name.equals("getEntrypointContainers") && methodInsnNode.desc.equals("(Ljava/lang/String;Ljava/lang/Class;)Ljava/util/List;")) { 136 | // add invoke after getEntrypointContainers 137 | // Lcom/google/common/collect/Lists;newArrayList(Ljava/lang/Iterable;)Ljava/util/ArrayList; 138 | 139 | iterator.add(new MethodInsnNode( 140 | Opcodes.INVOKESTATIC, 141 | "com/google/common/collect/Lists", 142 | "newArrayList", 143 | "(Ljava/lang/Iterable;)Ljava/util/ArrayList;", 144 | false 145 | )); 146 | 147 | // store the list 148 | final AbstractInsnNode next = iterator.next(); 149 | if (next instanceof VarInsnNode varInsnNode && varInsnNode.getOpcode() == Opcodes.ASTORE) { 150 | listVarIndex = varInsnNode.var; 151 | } else { 152 | throw new IllegalStateException("Expected VarInsnNode, got %s".formatted(next.getClass().getName())); 153 | } 154 | iterator.previous(); 155 | } 156 | 157 | // replace iterator call with listIterator 158 | // target insn: INVOKEINTERFACE java/util/Collection.iterator ()Ljava/util/Iterator; (itf) 159 | if (methodInsnNode.owner.equals("java/util/Collection") && methodInsnNode.name.equals("iterator") && methodInsnNode.desc.equals("()Ljava/util/Iterator;")) { 160 | // replace iterator with listIterator 161 | // need to check cast to java/util/List first 162 | // INVOKEINTERFACE java/util/List.listIterator ()Ljava/util/ListIterator; (itf) 163 | 164 | iterator.previous(); 165 | iterator.add(new TypeInsnNode(Opcodes.CHECKCAST, "java/util/List")); 166 | iterator.next(); 167 | iterator.set(new MethodInsnNode( 168 | Opcodes.INVOKEINTERFACE, 169 | "java/util/List", 170 | "listIterator", 171 | "()Ljava/util/ListIterator;", 172 | true 173 | )); 174 | final AbstractInsnNode next = iterator.next(); 175 | if (next instanceof VarInsnNode varInsnNode) { 176 | iteratorVarIndex = varInsnNode.var; 177 | } else { 178 | throw new IllegalStateException("Expected VarInsnNode after iterator call, but got %s".formatted(next.getClass().getName())); 179 | } 180 | iterator.previous(); 181 | } 182 | 183 | // call handler with progress tracker 184 | // target insn: INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object; (itf) 185 | if (methodInsnNode.owner.equals("java/util/Iterator") && methodInsnNode.name.equals("next") && methodInsnNode.desc.equals("()Ljava/lang/Object;")) { 186 | 187 | final AbstractInsnNode next = iterator.next(); 188 | if (next.getOpcode() != Opcodes.CHECKCAST) { 189 | throw new IllegalStateException("Expected CHECKCAST after iterator.next() call, but got %s".formatted(next.getClass().getName())); 190 | } 191 | if (listVarIndex == -1) { 192 | throw new IllegalStateException("listVarIndex not found"); 193 | } 194 | if (iteratorVarIndex == -1) { 195 | throw new IllegalStateException("iteratorVarIndex not found"); 196 | } 197 | // Lcom/ishland/earlyloadingscreen/platform_cl/AppLoaderAccessSupport;onEntrypointInvoke(Lnet/fabricmc/loader/api/entrypoint/EntrypointContainer;Lcom/ishland/earlyloadingscreen/platform_cl/AppLoaderAccessSupport$ProgressHolderAccessor;Ljava/util/List;Ljava/util/ListIterator;Ljava/lang/String;)V 198 | iterator.add(new InsnNode(Opcodes.DUP)); 199 | iterator.add(new VarInsnNode(Opcodes.ALOAD, progressTrackerIndex)); 200 | iterator.add(new VarInsnNode(Opcodes.ALOAD, listVarIndex)); 201 | iterator.add(new VarInsnNode(Opcodes.ALOAD, iteratorVarIndex)); 202 | iterator.add(new VarInsnNode(Opcodes.ALOAD, 1)); // arg1 203 | iterator.add(new MethodInsnNode( 204 | Opcodes.INVOKESTATIC, 205 | "com/ishland/earlyloadingscreen/platform_cl/AppLoaderAccessSupport", 206 | "onEntrypointInvoke", 207 | "(Lnet/fabricmc/loader/api/entrypoint/EntrypointContainer;Lcom/ishland/earlyloadingscreen/platform_cl/AppLoaderAccessSupport$ProgressHolderAccessor;Ljava/util/List;Ljava/util/ListIterator;Ljava/lang/String;)V", 208 | false 209 | )); 210 | 211 | iterator.previous(); 212 | } 213 | } 214 | 215 | if (insn instanceof LabelNode labelNode) { 216 | if (labelNode == firstLocalVar.start) { 217 | // initialize progress tracker 218 | // use Lcom/ishland/earlyloadingscreen/platform_cl/AppLoaderAccessSupport;tryCreateProgressHolder()Lcom/ishland/earlyloadingscreen/platform_cl/AppLoaderAccessSupport$ProgressHolderAccessor; 219 | iterator.add( 220 | new MethodInsnNode( 221 | Opcodes.INVOKESTATIC, 222 | "com/ishland/earlyloadingscreen/platform_cl/AppLoaderAccessSupport", 223 | "tryCreateProgressHolder", 224 | "()Lcom/ishland/earlyloadingscreen/platform_cl/AppLoaderAccessSupport$ProgressHolderAccessor;", 225 | false 226 | ) 227 | ); 228 | iterator.add(new VarInsnNode(Opcodes.ASTORE, progressTrackerIndex)); 229 | } 230 | } 231 | 232 | if (insn.getOpcode() == Opcodes.RETURN) { 233 | // call close() on progress tracker 234 | 235 | LabelNode continueLabel = new LabelNode(); 236 | 237 | iterator.previous(); 238 | iterator.add(new VarInsnNode(Opcodes.ALOAD, progressTrackerIndex)); 239 | iterator.add(new JumpInsnNode(Opcodes.IFNULL, continueLabel)); 240 | iterator.add(new VarInsnNode(Opcodes.ALOAD, progressTrackerIndex)); 241 | // Ljava/io/Closeable;close()V 242 | iterator.add(new MethodInsnNode( 243 | Opcodes.INVOKEINTERFACE, 244 | "java/io/Closeable", 245 | "close", 246 | "()V", 247 | true 248 | )); 249 | iterator.add(continueLabel); 250 | iterator.next(); 251 | } 252 | } 253 | 254 | } 255 | } 256 | return true; 257 | } 258 | 259 | // patch net/fabricmc/loader/impl/launch/knot/KnotClassDelegate 260 | if (className.equals("net/fabricmc/loader/impl/launch/knot/KnotClassDelegate")) { 261 | SharedConstants.LOGGER.info("Patching KnotClassDelegate for class loader issues"); 262 | 263 | for (MethodNode method : node.methods) { 264 | 265 | // Lnet/fabricmc/loader/impl/launch/knot/KnotClassDelegate;loadClass(Ljava/lang/String;Z)Ljava/lang/Class; 266 | if (method.name.equals("loadClass") && method.desc.equals("(Ljava/lang/String;Z)Ljava/lang/Class;")) { 267 | SharedConstants.LOGGER.info("Patching KnotClassDelegate.loadClass"); 268 | 269 | final LocalVariableNode firstLocalVar = method.localVariables.get(0); 270 | final ListIterator iterator = method.instructions.iterator(); 271 | while (iterator.hasNext()) { 272 | final AbstractInsnNode insn = iterator.next(); 273 | 274 | if (insn instanceof LabelNode labelNode) { 275 | if (labelNode == firstLocalVar.start) { 276 | 277 | LabelNode continueLabel = new LabelNode(); 278 | // add special override to delegate to parent 279 | iterator.add(new VarInsnNode(Opcodes.ALOAD, 1)); 280 | iterator.add(new LdcInsnNode("com.ishland.earlyloadingscreen.platform_cl.")); 281 | // do String.startsWith 282 | iterator.add(new MethodInsnNode( 283 | Opcodes.INVOKEVIRTUAL, 284 | "java/lang/String", 285 | "startsWith", 286 | "(Ljava/lang/String;)Z", 287 | false 288 | )); 289 | iterator.add(new JumpInsnNode(Opcodes.IFEQ, continueLabel)); 290 | 291 | // parent loader at field Lnet/fabricmc/loader/impl/launch/knot/KnotClassDelegate;parentClassLoader:Ljava/lang/ClassLoader; 292 | // invoke INVOKEVIRTUAL java/lang/ClassLoader.loadClass (Ljava/lang/String;)Ljava/lang/Class; 293 | iterator.add(new VarInsnNode(Opcodes.ALOAD, 0)); 294 | iterator.add(new VarInsnNode(Opcodes.ALOAD, 0)); 295 | iterator.add(new FieldInsnNode( 296 | Opcodes.GETFIELD, 297 | "net/fabricmc/loader/impl/launch/knot/KnotClassDelegate", 298 | "parentClassLoader", 299 | "Ljava/lang/ClassLoader;" 300 | )); 301 | iterator.add(new VarInsnNode(Opcodes.ALOAD, 1)); 302 | iterator.add(new MethodInsnNode( 303 | Opcodes.INVOKEVIRTUAL, 304 | "java/lang/ClassLoader", 305 | "loadClass", 306 | "(Ljava/lang/String;)Ljava/lang/Class;", 307 | false 308 | )); 309 | 310 | iterator.add(new InsnNode(Opcodes.ARETURN)); 311 | 312 | iterator.add(continueLabel); 313 | 314 | } 315 | } 316 | } 317 | } 318 | } 319 | 320 | return true; 321 | } 322 | 323 | return false; 324 | 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/patch/PatchUtil.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.patch; 2 | 3 | import com.ishland.earlyloadingscreen.SharedConstants; 4 | import net.bytebuddy.agent.ByteBuddyAgent; 5 | import org.apache.commons.io.file.PathUtils; 6 | import org.objectweb.asm.ClassReader; 7 | import org.objectweb.asm.ClassWriter; 8 | import org.objectweb.asm.tree.ClassNode; 9 | 10 | import java.io.IOException; 11 | import java.lang.instrument.ClassFileTransformer; 12 | import java.lang.instrument.IllegalClassFormatException; 13 | import java.lang.instrument.Instrumentation; 14 | import java.nio.file.Files; 15 | import java.nio.file.Path; 16 | import java.security.ProtectionDomain; 17 | import java.util.Comparator; 18 | import java.util.Iterator; 19 | import java.util.concurrent.CopyOnWriteArrayList; 20 | 21 | public class PatchUtil { 22 | 23 | private static final Path transformerOutputPath = Path.of(".", ".earlyloadingscreen-transformer-output"); 24 | 25 | static final CopyOnWriteArrayList transformers = new CopyOnWriteArrayList<>(); 26 | static final Instrumentation instrumentation; 27 | 28 | static { 29 | if (Files.isDirectory(transformerOutputPath)) { 30 | try { 31 | deleteDirectory(transformerOutputPath); 32 | } catch (IOException e) { 33 | SharedConstants.LOGGER.warn("Failed to delete transformer output directory", e); 34 | } 35 | } 36 | try { 37 | Files.createDirectories(transformerOutputPath); 38 | } catch (IOException e) { 39 | SharedConstants.LOGGER.warn("Failed to create transformer output directory", e); 40 | } 41 | 42 | Instrumentation inst = null; 43 | try { 44 | inst = ByteBuddyAgent.install(); 45 | } catch (Throwable t) { 46 | SharedConstants.LOGGER.warn("Failed to install ByteBuddyAgent, patching will not work", t); 47 | } 48 | instrumentation = inst; 49 | if (inst != null) { 50 | inst.addTransformer(new ClassFileTransformer() { 51 | @Override 52 | public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 53 | try { 54 | ClassNode node = new ClassNode(); 55 | new ClassReader(classfileBuffer).accept(node, 0); 56 | boolean transformed = false; 57 | for (BytecodeTransformer transformer : transformers) { 58 | transformed |= transformer.transform(className, node); 59 | } 60 | if (transformed) { 61 | final ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); 62 | node.accept(writer); 63 | final byte[] buf = writer.toByteArray(); 64 | try { 65 | final Path path = transformerOutputPath.resolve(className + ".class"); 66 | Files.createDirectories(path.getParent()); 67 | Files.write(path, buf); 68 | } catch (Throwable t) { 69 | SharedConstants.LOGGER.warn("Failed to write transformed class %s to disk".formatted(className), t); 70 | } 71 | return buf; 72 | } else { 73 | return null; 74 | } 75 | } catch (Throwable t) { 76 | t.printStackTrace(); 77 | SharedConstants.LOGGER.warn("Failed to transform class " + className, t); 78 | return null; 79 | } 80 | } 81 | }, true); 82 | } 83 | } 84 | 85 | private static void deleteDirectory(Path path) throws IOException { 86 | final Iterator iterator = Files.walk(path) 87 | .sorted(Comparator.reverseOrder()).iterator(); 88 | while (iterator.hasNext()) { 89 | Files.delete(iterator.next()); 90 | } 91 | } 92 | 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/patch/SodiumOSDetectionPatch.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.patch; 2 | 3 | import com.ishland.earlyloadingscreen.LoadingProgressManager; 4 | import com.ishland.earlyloadingscreen.SharedConstants; 5 | import com.ishland.earlyloadingscreen.util.RemapUtil; 6 | import net.fabricmc.loader.api.FabricLoader; 7 | import net.fabricmc.loader.api.MappingResolver; 8 | import org.objectweb.asm.Opcodes; 9 | import org.objectweb.asm.Type; 10 | import org.objectweb.asm.tree.AbstractInsnNode; 11 | import org.objectweb.asm.tree.ClassNode; 12 | import org.objectweb.asm.tree.FieldInsnNode; 13 | import org.objectweb.asm.tree.LocalVariableNode; 14 | import org.objectweb.asm.tree.MethodInsnNode; 15 | import org.objectweb.asm.tree.MethodNode; 16 | import org.objectweb.asm.tree.ParameterNode; 17 | 18 | import java.lang.instrument.Instrumentation; 19 | import java.util.Arrays; 20 | import java.util.Iterator; 21 | import java.util.ListIterator; 22 | 23 | public class SodiumOSDetectionPatch implements BytecodeTransformer { 24 | 25 | private static final SodiumOSDetectionPatch INSTANCE = new SodiumOSDetectionPatch(); 26 | 27 | private static final String[] SODIUM_WORKAROUNDS_CLASSES = new String[] { 28 | "me/jellysquid/mods/sodium/client/util/workarounds/Workarounds", 29 | "me/jellysquid/mods/sodium/client/compatibility/workarounds/Workarounds", 30 | "me/jellysquid/mods/sodium/client/util/workarounds/driver/nvidia/NvidiaWorkarounds", 31 | "me/jellysquid/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaWorkarounds", 32 | "me/jellysquid/mods/sodium/client/util/workarounds/driver/nvidia/NvidiaWorkarounds$1", 33 | "me/jellysquid/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaWorkarounds$1", 34 | "me/jellysquid/mods/sodium/client/util/workarounds/probe/GraphicsAdapterProbe", 35 | "me/jellysquid/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe", 36 | }; 37 | 38 | public static final boolean INITIALIZED; 39 | 40 | private SodiumOSDetectionPatch() { 41 | } 42 | 43 | static { 44 | INITIALIZED = initTransformer(); 45 | } 46 | 47 | public static void init() { 48 | // intentionally empty, just to trigger static initialization 49 | } 50 | 51 | private static boolean initTransformer() { 52 | Instrumentation inst = PatchUtil.instrumentation; 53 | if (inst == null) { 54 | SharedConstants.LOGGER.warn("Instrumentation unavailable, sodium workarounds will not be available"); 55 | LoadingProgressManager.showMessageAsProgress("Instrumentation unavailable, sodium workarounds may not be available"); 56 | return false; 57 | } 58 | PatchUtil.transformers.add(INSTANCE); 59 | if (!Arrays.stream(SODIUM_WORKAROUNDS_CLASSES).allMatch(name -> tryRetransform(inst, name))) { 60 | SharedConstants.LOGGER.warn("Failed to retransform sodium workarounds, sodium workarounds will not be available"); 61 | LoadingProgressManager.showMessageAsProgress("Failed to retransform sodium workarounds, sodium workarounds may not be available"); 62 | PatchUtil.transformers.remove(INSTANCE); 63 | for (String s : SODIUM_WORKAROUNDS_CLASSES) { 64 | tryRetransform(inst, s); 65 | } 66 | return false; 67 | } 68 | return true; 69 | } 70 | 71 | private static boolean tryRetransform(Instrumentation inst, String name) { 72 | try { 73 | inst.retransformClasses(Class.forName(name.replace('/', '.'))); 74 | return true; 75 | } catch (ClassNotFoundException e) { 76 | return true; 77 | } catch (Throwable t) { 78 | SharedConstants.LOGGER.warn("Failed to retransform class %s".formatted(name), t); 79 | return false; 80 | } 81 | } 82 | 83 | @Override 84 | public boolean transform(String className, ClassNode node) { 85 | if (Arrays.asList(SODIUM_WORKAROUNDS_CLASSES).contains(className)) { 86 | SharedConstants.LOGGER.info("Patching %s to allow early usage".formatted(className)); 87 | 88 | final MappingResolver resolver = FabricLoader.getInstance().getMappingResolver(); 89 | final String intermediary = "intermediary"; 90 | 91 | for (MethodNode method : node.methods) { 92 | 93 | final String remappedMCOS = resolver.mapClassName(intermediary, "net.minecraft.class_156$class_158").replace('.', '/'); 94 | { 95 | final Iterator iterator = method.localVariables.iterator(); 96 | 97 | while (iterator.hasNext()) { 98 | final LocalVariableNode localVariableNode = iterator.next(); 99 | 100 | if (localVariableNode.desc.equals(String.format("L%s;", remappedMCOS))) { 101 | localVariableNode.desc = "Lcom/ishland/earlyloadingscreen/util/OSDetectionUtil$OperatingSystem;"; 102 | } 103 | } 104 | } 105 | 106 | method.desc = method.desc.replace(String.format("L%s;", remappedMCOS), "Lcom/ishland/earlyloadingscreen/util/OSDetectionUtil$OperatingSystem;"); 107 | if (method.signature != null) { 108 | method.signature = method.signature.replace(String.format("L%s;", remappedMCOS), "Lcom/ishland/earlyloadingscreen/util/OSDetectionUtil$OperatingSystem;"); 109 | } 110 | 111 | { 112 | final ListIterator iterator = method.instructions.iterator(); 113 | 114 | while (iterator.hasNext()) { 115 | final AbstractInsnNode insn = iterator.next(); 116 | 117 | if (insn instanceof MethodInsnNode methodInsnNode) { 118 | // replace Util.getOperatingSystem() with OSDetectionUtil.getOperatingSystem() 119 | if (methodInsnNode.getOpcode() == Opcodes.INVOKESTATIC && 120 | methodInsnNode.owner.equals(resolver.mapClassName(intermediary, "net.minecraft.class_156").replace('.', '/')) && 121 | methodInsnNode.name.equals(resolver.mapMethodName(intermediary, "net.minecraft.class_156", "method_668", "()Lnet/minecraft/class_156$class_158;")) && 122 | methodInsnNode.desc.equals(RemapUtil.remapMethodDescriptor("()Lnet/minecraft/class_156$class_158;"))) { 123 | methodInsnNode.owner = "com/ishland/earlyloadingscreen/util/OSDetectionUtil"; 124 | methodInsnNode.name = "getOperatingSystem"; 125 | methodInsnNode.desc = "()Lcom/ishland/earlyloadingscreen/util/OSDetectionUtil$OperatingSystem;"; 126 | } 127 | 128 | // replace Util$OperatingSystem.ordinal() with OSDetectionUtil$OperatingSystem.ordinal() 129 | if (methodInsnNode.getOpcode() == Opcodes.INVOKEVIRTUAL && 130 | methodInsnNode.owner.equals(remappedMCOS) && 131 | methodInsnNode.name.equals(resolver.mapMethodName(intermediary, "net.minecraft.class_156$class_158", "ordinal", "()I")) && 132 | methodInsnNode.desc.equals(RemapUtil.remapMethodDescriptor("()I"))) { 133 | methodInsnNode.owner = "com/ishland/earlyloadingscreen/util/OSDetectionUtil$OperatingSystem"; 134 | } 135 | 136 | // replace Util$OperatingSystem.values() with OSDetectionUtil$OperatingSystem.values() 137 | if (methodInsnNode.getOpcode() == Opcodes.INVOKESTATIC && 138 | methodInsnNode.owner.equals(remappedMCOS) && 139 | methodInsnNode.name.equals(resolver.mapMethodName(intermediary, "net.minecraft.class_156$class_158", "values", "()[Lnet/minecraft/class_156$class_158;")) && 140 | methodInsnNode.desc.equals(RemapUtil.remapMethodDescriptor("()[Lnet/minecraft/class_156$class_158;"))) { 141 | methodInsnNode.owner = "com/ishland/earlyloadingscreen/util/OSDetectionUtil$OperatingSystem"; 142 | methodInsnNode.desc = "()[Lcom/ishland/earlyloadingscreen/util/OSDetectionUtil$OperatingSystem;"; 143 | } 144 | 145 | methodInsnNode.desc = methodInsnNode.desc.replace(String.format("L%s;", remappedMCOS), "Lcom/ishland/earlyloadingscreen/util/OSDetectionUtil$OperatingSystem;"); 146 | } 147 | 148 | if (insn instanceof FieldInsnNode fieldInsnNode) { 149 | // replace Util$OperatingSystem with OSDetectionUtil$OperatingSystem 150 | if (fieldInsnNode.getOpcode() == Opcodes.GETSTATIC && 151 | fieldInsnNode.owner.equals(remappedMCOS) && 152 | fieldInsnNode.desc.equals(RemapUtil.remapFieldDescriptor("Lnet/minecraft/class_156$class_158;"))) { 153 | fieldInsnNode.owner = "com/ishland/earlyloadingscreen/util/OSDetectionUtil$OperatingSystem"; 154 | fieldInsnNode.desc = "Lcom/ishland/earlyloadingscreen/util/OSDetectionUtil$OperatingSystem;"; 155 | if (fieldInsnNode.name.equals(resolver.mapFieldName(intermediary, "net.minecraft.class_156$class_158", "field_1135", "Lnet/minecraft/class_156$class_158;"))) { 156 | fieldInsnNode.name = "LINUX"; 157 | } else if (fieldInsnNode.name.equals(resolver.mapFieldName(intermediary, "net.minecraft.class_156$class_158", "field_1134", "Lnet/minecraft/class_156$class_158;"))) { 158 | fieldInsnNode.name = "SOLARIS"; 159 | } else if (fieldInsnNode.name.equals(resolver.mapFieldName(intermediary, "net.minecraft.class_156$class_158", "field_1133", "Lnet/minecraft/class_156$class_158;"))) { 160 | fieldInsnNode.name = "WINDOWS"; 161 | } else if (fieldInsnNode.name.equals(resolver.mapFieldName(intermediary, "net.minecraft.class_156$class_158", "field_1132", "Lnet/minecraft/class_156$class_158;"))) { 162 | fieldInsnNode.name = "OSX"; 163 | } else if (fieldInsnNode.name.equals(resolver.mapFieldName(intermediary, "net.minecraft.class_156$class_158", "field_1131", "Lnet/minecraft/class_156$class_158;"))) { 164 | fieldInsnNode.name = "UNKNOWN"; 165 | } else { 166 | throw new RuntimeException("Unknown field name: " + fieldInsnNode.name); 167 | } 168 | } 169 | } 170 | } 171 | } 172 | 173 | } 174 | 175 | 176 | return true; 177 | } 178 | 179 | 180 | return false; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/platform_cl/AppLoaderAccessSupport.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.platform_cl; 2 | 3 | import net.fabricmc.loader.api.FabricLoader; 4 | import net.fabricmc.loader.api.entrypoint.EntrypointContainer; 5 | 6 | import java.io.Closeable; 7 | import java.util.List; 8 | import java.util.ListIterator; 9 | import java.util.function.Supplier; 10 | 11 | public class AppLoaderAccessSupport { 12 | 13 | private static final String shareKey = "early-loadingscreen:accessor"; 14 | 15 | static { 16 | if (AppLoaderAccessSupport.class.getClassLoader() != FabricLoader.class.getClassLoader()) { 17 | System.err.println(String.format("[EarlyLoadingScreen] AppLoaderAccessSupport is loaded by %s but expected to be loaded by %s! Entrypoint information may not be available. ", AppLoaderAccessSupport.class.getClassLoader(), FabricLoader.class.getClassLoader())); 18 | } 19 | LoadingScreenAccessor.class.getName(); 20 | ProgressHolderAccessor.class.getName(); 21 | } 22 | 23 | public static void setAccess(LoadingScreenAccessor access) { 24 | FabricLoader.getInstance().getObjectShare().put(shareKey, access); 25 | } 26 | 27 | // called by generated code 28 | @SuppressWarnings("unused") 29 | public static ProgressHolderAccessor tryCreateProgressHolder() { 30 | final Object o = FabricLoader.getInstance().getObjectShare().get(shareKey); 31 | if (o instanceof LoadingScreenAccessor accessor) { 32 | return accessor.tryCreateProgressHolder(); 33 | } else if (o != null) { 34 | System.err.println(String.format("[EarlyLoadingScreen] Share %s exists but is not a LoadingScreenAccessor (found %s), entrypoint information will not be available.", shareKey, o)); 35 | } else { 36 | System.err.println(String.format("[EarlyLoadingScreen] Share %s does not exist, entrypoint information will not be available.", shareKey)); 37 | } 38 | return null; 39 | } 40 | 41 | // called by generated code 42 | @SuppressWarnings("unused") 43 | public static void onEntrypointInvoke(EntrypointContainer container, ProgressHolderAccessor progressHolder, List list, ListIterator listIterator, String entrypointName) { 44 | if (progressHolder != null) { 45 | final int prevIdx = listIterator.previousIndex(); 46 | final int size = list.size(); 47 | progressHolder.update(() -> String.format("Running entrypoint %s (%d/%d) for mod %s", entrypointName, prevIdx, size, container.getProvider().getMetadata().getId())); 48 | progressHolder.updateProgress(() -> prevIdx / (float) size); 49 | } 50 | } 51 | 52 | public interface LoadingScreenAccessor { 53 | public ProgressHolderAccessor tryCreateProgressHolder(); 54 | } 55 | 56 | public interface ProgressHolderAccessor extends Closeable { 57 | public void update(Supplier text); 58 | public void updateProgress(Supplier progress); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/platform_cl/Config.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.platform_cl; 2 | 3 | import net.fabricmc.loader.api.FabricLoader; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.nio.file.StandardOpenOption; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Properties; 15 | 16 | public class Config { 17 | 18 | private static final String MIXIN_PREFIX = "mixin."; 19 | 20 | public static final boolean REUSE_EARLY_WINDOW; 21 | public static final boolean ENABLE_ENTRYPOINT_INFORMATION; 22 | public static final boolean ENABLE_MIXIN_PRETRANSFORM; 23 | public static final boolean ALLOW_EARLY_WINDOW_CLOSE; 24 | public static final LaunchPoint WINDOW_CREATION_POINT; 25 | public static final List DISABLED_MIXINS = new ArrayList<>(); 26 | 27 | static { 28 | final Properties properties = new Properties(); 29 | final Properties newProperties = new Properties(); 30 | final Path path = FabricLoader.getInstance().getConfigDir().resolve("early-loading-screen.properties"); 31 | if (Files.isRegularFile(path)) { 32 | try (InputStream in = Files.newInputStream(path, StandardOpenOption.CREATE)) { 33 | properties.load(in); 34 | } catch (IOException e) { 35 | throw new RuntimeException(e); 36 | } 37 | } 38 | 39 | StringBuilder sb = new StringBuilder(); 40 | sb.append("Early loading screen configuration file\n"); 41 | 42 | REUSE_EARLY_WINDOW = !PlatformUtil.IS_WINDOWS && getBoolean(properties, newProperties, "reuse_early_window", true, sb); 43 | ENABLE_ENTRYPOINT_INFORMATION = getBoolean(properties, newProperties, "enable_entrypoint_information", true, sb); 44 | ENABLE_MIXIN_PRETRANSFORM = getBoolean(properties, newProperties, "enable_mixin_pretransform", false, sb); 45 | ALLOW_EARLY_WINDOW_CLOSE = getBoolean(properties, newProperties, "allow_early_window_close", true, sb); 46 | final LaunchPoint defaultPoint = FabricLoader.getInstance().isModLoaded("immediatelyfast") ? LaunchPoint.mixinLoad : LaunchPoint.postModLoading; 47 | WINDOW_CREATION_POINT = getEnum(LaunchPoint.class, properties, newProperties, "window_creation_point", defaultPoint, sb); 48 | for (Map.Entry entry : properties.entrySet()) { 49 | final String key = (String) entry.getKey(); 50 | if (key.startsWith(MIXIN_PREFIX)) { 51 | final String mixin = key.substring(MIXIN_PREFIX.length()); 52 | if (mixin.isBlank()) continue; 53 | if (parseBoolean((String) entry.getValue())) { 54 | newProperties.setProperty(key, "true"); 55 | } else { 56 | newProperties.setProperty(key, "false"); 57 | DISABLED_MIXINS.add(mixin); 58 | } 59 | } 60 | } 61 | 62 | 63 | try (OutputStream out = Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { 64 | newProperties.store(out, sb.toString().trim().indent(1)); 65 | } catch (IOException e) { 66 | throw new RuntimeException(e); 67 | } 68 | } 69 | 70 | public static void init() { 71 | } 72 | 73 | private static int getInt(Properties properties, Properties newProperties, String key, int def, StringBuilder comment) { 74 | comment.append(key).append(" default: ").append(def).append("\n"); 75 | try { 76 | final int i = Integer.parseInt(properties.getProperty(key)); 77 | newProperties.setProperty(key, String.valueOf(i)); 78 | return i; 79 | } catch (NumberFormatException e) { 80 | newProperties.setProperty(key, "default"); 81 | return def; 82 | } 83 | } 84 | 85 | private static boolean getBoolean(Properties properties, Properties newProperties, String key, boolean def, StringBuilder comment) { 86 | comment.append(key).append(" default: ").append(def).append("\n"); 87 | try { 88 | final boolean b = parseBoolean(properties.getProperty(key)); 89 | newProperties.setProperty(key, String.valueOf(b)); 90 | return b; 91 | } catch (NumberFormatException e) { 92 | newProperties.setProperty(key, "default"); 93 | return def; 94 | } 95 | } 96 | 97 | private static > T getEnum(Class clazz, Properties properties, Properties newProperties, String key, T def, StringBuilder comment) { 98 | comment.append(key).append(" available [default] options: "); 99 | for (T constant : clazz.getEnumConstants()) { 100 | if (constant == def) comment.append('['); 101 | comment.append(constant.name()); 102 | if (constant == def) comment.append(']'); 103 | comment.append(' '); 104 | } 105 | 106 | try { 107 | final T configured = Enum.valueOf(clazz, String.valueOf(properties.getProperty(key))); 108 | newProperties.setProperty(key, String.valueOf(configured)); 109 | return configured; 110 | } catch (IllegalArgumentException e) { 111 | newProperties.setProperty(key, "default"); 112 | return def; 113 | } 114 | } 115 | 116 | private static boolean parseBoolean(String string) { 117 | if (string == null) throw new NumberFormatException("null"); 118 | if (string.trim().equalsIgnoreCase("true")) return true; 119 | if (string.trim().equalsIgnoreCase("false")) return false; 120 | throw new NumberFormatException(string); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/platform_cl/LaunchPoint.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.platform_cl; 2 | 3 | public enum LaunchPoint { 4 | 5 | postModLoading, 6 | mixinEarly, 7 | mixinLoad, 8 | preLaunch, 9 | mcEarly, 10 | off 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/platform_cl/PlatformUtil.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.platform_cl; 2 | 3 | import java.util.Locale; 4 | 5 | public class PlatformUtil { 6 | 7 | private static final String NORMALIZED_OS = normalizeOs(System.getProperty("os.name", "")); 8 | 9 | public static final boolean IS_WINDOWS = "windows".equals(NORMALIZED_OS); 10 | public static final boolean IS_OSX = "osx".equals(NORMALIZED_OS); 11 | 12 | 13 | private static String normalize(String value) { 14 | return value.toLowerCase(Locale.US).replaceAll("[^a-z0-9]+", ""); 15 | } 16 | 17 | private static String normalizeOs(String value) { 18 | value = normalize(value); 19 | if (value.startsWith("aix")) { 20 | return "aix"; 21 | } else if (value.startsWith("hpux")) { 22 | return "hpux"; 23 | } else if (!value.startsWith("os400") || value.length() > 5 && Character.isDigit(value.charAt(5))) { 24 | if (value.startsWith("linux")) { 25 | return "linux"; 26 | } else if (!value.startsWith("macosx") && !value.startsWith("osx") && !value.startsWith("darwin")) { 27 | if (value.startsWith("freebsd")) { 28 | return "freebsd"; 29 | } else if (value.startsWith("openbsd")) { 30 | return "openbsd"; 31 | } else if (value.startsWith("netbsd")) { 32 | return "netbsd"; 33 | } else if (!value.startsWith("solaris") && !value.startsWith("sunos")) { 34 | return value.startsWith("windows") ? "windows" : "unknown"; 35 | } else { 36 | return "sunos"; 37 | } 38 | } else { 39 | return "osx"; 40 | } 41 | } else { 42 | return "os400"; 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/render/GLText.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.render; 2 | 3 | import org.joml.Vector4f; 4 | import org.lwjgl.opengl.GL32; 5 | 6 | import java.io.Closeable; 7 | import java.nio.ByteBuffer; 8 | import java.util.Arrays; 9 | 10 | import static org.lwjgl.opengl.GL32.*; 11 | 12 | /* 13 | 14 | GLText - OpenGL Text Rendering Library 15 | https://github.com/vallentin/glText 16 | Adapted for use with LWJGL 3 17 | 18 | Original License: 19 | 20 | Copyright (c) 2013-2018 Christian Vallentin 21 | 22 | This software is provided 'as-is', without any express or implied 23 | warranty. In no event will the authors be held liable for any damages 24 | arising from the use of this software. 25 | 26 | Permission is granted to anyone to use this software for any purpose, 27 | including commercial applications, and to alter it and redistribute it 28 | freely, subject to the following restrictions: 29 | 30 | 1. The origin of this software must not be misrepresented; you must not 31 | claim that you wrote the original software. If you use this software 32 | in a product, an acknowledgment in the product documentation would 33 | be appreciated but is not required. 34 | 35 | 2. Altered source versions must be plainly marked as such, and must not 36 | be misrepresented as being the original software. 37 | 38 | 3. This notice may not be removed or altered from any source 39 | distribution. 40 | 41 | */ 42 | public class GLText { 43 | 44 | // #define _gltDrawText() \ 45 | // glUniformMatrix4fv(_gltText2DShaderMVPUniformLocation, 1, GL_FALSE, mvp); \ 46 | // \ 47 | // glBindVertexArray(text->_vao); \ 48 | // glDrawArrays(GL_TRIANGLES, 0, text->vertexCount); 49 | 50 | public static final String GLT_NAME = "glText"; 51 | public static final int GLT_VERSION_MAJOR = 1; 52 | public static final int GLT_VERSION_MINOR = 1; 53 | public static final int GLT_VERSION_PATCH = 6; 54 | public static final String GLT_VERSION = GLT_VERSION_MAJOR + "." + GLT_VERSION_MINOR + "." + GLT_VERSION_PATCH; 55 | public static final String GLT_NAME_VERSION = GLT_NAME + " " + GLT_VERSION; 56 | public static final int GLT_NULL = 0; 57 | public static final int GLT_NULL_HANDLE = 0; 58 | public static final int GLT_LEFT = 0; 59 | public static final int GLT_TOP = 0; 60 | public static final int GLT_CENTER = 1; 61 | public static final int GLT_RIGHT = 2; 62 | public static final int GLT_BOTTOM = 2; 63 | 64 | // impl start 65 | 66 | private static final int NULL = 0; 67 | private static final int _GLT_TEXT2D_POSITION_LOCATION = 0; 68 | private static final int _GLT_TEXT2D_TEXCOORD_LOCATION = 1; 69 | private static final int _GLT_TEXT2D_POSITION_SIZE = 2; 70 | private static final int _GLT_TEXT2D_TEXCOORD_SIZE = 2; 71 | private static final int _GLT_TEXT2D_VERTEX_SIZE = (_GLT_TEXT2D_POSITION_SIZE + _GLT_TEXT2D_TEXCOORD_SIZE); 72 | private static final int _GLT_TEXT2D_POSITION_OFFSET = 0; 73 | private static final int _GLT_TEXT2D_TEXCOORD_OFFSET = _GLT_TEXT2D_POSITION_SIZE; 74 | private static int _GLT_MAT4_INDEX(int row, int column) { 75 | return (row) + (column) * 4; 76 | } 77 | private static final String _gltFontGlyphCharacters = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+/():;%&`*#=[]\""; 78 | private static final int _gltFontGlyphCount = 83; 79 | private static final char _gltFontGlyphMinChar = ' '; 80 | private static final char _gltFontGlyphMaxChar = 'z'; 81 | private static final int _gltFontGlyphHeight = 17; // Line height 82 | 83 | private static class _GLTglyph { 84 | public char c; 85 | public int x, y; 86 | public int w, h; 87 | public float u1, v1; 88 | public float u2, v2; 89 | public boolean drawable; 90 | } 91 | 92 | private static class _GLTglyphdata { 93 | public int x, y; 94 | public int w, h; 95 | public int marginLeft, marginTop; 96 | public int marginRight, marginBottom; 97 | public int dataWidth, dataHeight; 98 | } 99 | 100 | private _GLTglyph[] _gltFontGlyphs = new _GLTglyph[_gltFontGlyphCount]; 101 | private _GLTglyph[] _gltFontGlyphs2 = new _GLTglyph[_gltFontGlyphMaxChar - _gltFontGlyphMinChar + 1]; 102 | private int _gltText2DShader = GLT_NULL_HANDLE; 103 | private int _gltText2DFontTexture = GLT_NULL_HANDLE; 104 | private int _gltText2DShaderMVPUniformLocation = -1; 105 | private int _gltText2DShaderColorUniformLocation = -1; 106 | 107 | private float[] _gltText2DProjectionMatrix = new float[16]; 108 | 109 | public static class GLTtext { 110 | public String _text; 111 | public int _textLength; 112 | public boolean _dirty; 113 | public int vertexCount; 114 | public float[] _vertices; 115 | public int _vao; 116 | public int _vbo; 117 | } 118 | 119 | public static GLTtext gltCreateText() { 120 | GLTtext text = new GLTtext(); 121 | text._vao = glGenVertexArrays(); 122 | text._vbo = glGenBuffers(); 123 | 124 | assert text._vao != NULL; 125 | assert text._vbo != NULL; 126 | 127 | if (text._vao == NULL || text._vbo == NULL) { 128 | gltDeleteText(text); 129 | return null; 130 | } 131 | 132 | glBindVertexArray(text._vao); 133 | 134 | glBindBuffer(GL_ARRAY_BUFFER, text._vbo); 135 | 136 | glEnableVertexAttribArray(_GLT_TEXT2D_POSITION_LOCATION); 137 | 138 | glVertexAttribPointer(_GLT_TEXT2D_POSITION_LOCATION, _GLT_TEXT2D_POSITION_SIZE, GL_FLOAT, false, (_GLT_TEXT2D_VERTEX_SIZE * Float.BYTES), (_GLT_TEXT2D_POSITION_OFFSET * Float.BYTES)); 139 | 140 | glEnableVertexAttribArray(_GLT_TEXT2D_TEXCOORD_LOCATION); 141 | glVertexAttribPointer(_GLT_TEXT2D_TEXCOORD_LOCATION, _GLT_TEXT2D_TEXCOORD_SIZE, GL_FLOAT, false, (_GLT_TEXT2D_VERTEX_SIZE * Float.BYTES), (_GLT_TEXT2D_TEXCOORD_OFFSET * Float.BYTES)); 142 | 143 | glBindBuffer(GL_ARRAY_BUFFER, 0); 144 | glBindVertexArray(0); 145 | 146 | return text; 147 | } 148 | 149 | public static void gltDeleteText(GLTtext text) { 150 | if (text == null) { 151 | return; 152 | } 153 | 154 | if (text._vao != NULL) { 155 | glDeleteVertexArrays(text._vao); 156 | text._vao = NULL; 157 | } 158 | 159 | if (text._vbo != NULL) { 160 | glDeleteBuffers(text._vbo); 161 | text._vbo = NULL; 162 | } 163 | 164 | if (text._text != null) { 165 | text._text = null; 166 | } 167 | 168 | if (text._vertices != null) { 169 | text._vertices = null; 170 | } 171 | 172 | text = null; 173 | } 174 | 175 | public static boolean gltSetText(GLTtext text, String string) { 176 | if (text == null) { 177 | return false; 178 | } 179 | 180 | int strLength = 0; 181 | 182 | if (string != null) { 183 | strLength = string.length(); 184 | } 185 | 186 | if (strLength > 0) { 187 | if (text._text != null) { 188 | if (string.equals(text._text)) { 189 | return true; 190 | } 191 | 192 | text._text = null; 193 | } 194 | 195 | text._text = string; 196 | 197 | text._textLength = strLength; 198 | text._dirty = true; 199 | 200 | return true; 201 | } else { 202 | if (text._text != null) { 203 | text._text = null; 204 | } else { 205 | return true; 206 | } 207 | 208 | text._textLength = 0; 209 | text._dirty = true; 210 | 211 | return true; 212 | } 213 | } 214 | 215 | public static String gltGetText(GLTtext text) { 216 | if (text != null && text._text != null) { 217 | return text._text; 218 | } 219 | 220 | return ""; 221 | } 222 | 223 | public void gltViewport(int width, int height) { 224 | assert width > 0; 225 | assert height > 0; 226 | 227 | final float left = 0.0f; 228 | final float right = (float) width; 229 | final float bottom = (float) height; 230 | final float top = 0.0f; 231 | final float zNear = -1.0f; 232 | final float zFar = 1.0f; 233 | 234 | final float[] projection = { 235 | (2.0f / (right - left)), 0.0f, 0.0f, 0.0f, 236 | 0.0f, (2.0f / (top - bottom)), 0.0f, 0.0f, 237 | 0.0f, 0.0f, (-2.0f / (zFar - zNear)), 0.0f, 238 | -((right + left) / (right - left)), 239 | -((top + bottom) / (top - bottom)), 240 | -((zFar + zNear) / (zFar - zNear)), 241 | 1.0f, 242 | }; 243 | 244 | System.arraycopy(projection, 0, _gltText2DProjectionMatrix, 0, 16); 245 | } 246 | 247 | public Closeable gltBeginDraw() { 248 | glUseProgram(_gltText2DShader); 249 | 250 | glActiveTexture(GL_TEXTURE0); 251 | glBindTexture(GL_TEXTURE_2D, _gltText2DFontTexture); 252 | return () -> { 253 | glUseProgram(0); 254 | glBindTexture(GL_TEXTURE_2D, 0); 255 | }; 256 | } 257 | 258 | public void gltDrawText(GLTtext text, float[] mvp) { 259 | if (text == null) { 260 | return; 261 | } 262 | 263 | if (text._dirty) { 264 | _gltUpdateBuffers(text); 265 | } 266 | 267 | if (text.vertexCount == 0) { 268 | return; 269 | } 270 | 271 | glUniformMatrix4fv(_gltText2DShaderMVPUniformLocation, false, mvp); 272 | 273 | glBindVertexArray(text._vao); 274 | glDrawArrays(GL_TRIANGLES, 0, text.vertexCount); 275 | glBindVertexArray(0); 276 | } 277 | 278 | // #define _gltDrawText() \ 279 | // glUniformMatrix4fv(_gltText2DShaderMVPUniformLocation, 1, GL_FALSE, mvp); \ 280 | // \ 281 | // glBindVertexArray(text->_vao); \ 282 | // glDrawArrays(GL_TRIANGLES, 0, text->vertexCount); 283 | // 284 | public void gltDrawText2D(GLTtext text, float x, float y, float scale) { 285 | if (text == null) { 286 | return; 287 | } 288 | 289 | if (text._dirty) { 290 | _gltUpdateBuffers(text); 291 | } 292 | 293 | if (text.vertexCount == 0) { 294 | return; 295 | } 296 | 297 | // manual viewpoint 298 | 299 | final float[] model = { 300 | scale, 0.0f, 0.0f, 0.0f, 301 | 0.0f, scale, 0.0f, 0.0f, 302 | 0.0f, 0.0f, scale, 0.0f, 303 | x, y, 0.0f, 1.0f, 304 | }; 305 | 306 | final float[] mvp = new float[16]; 307 | _gltMat4Mult(_gltText2DProjectionMatrix, model, mvp); 308 | 309 | glUniformMatrix4fv(_gltText2DShaderMVPUniformLocation, false, mvp); 310 | 311 | glBindVertexArray(text._vao); 312 | glDrawArrays(GL_TRIANGLES, 0, text.vertexCount); 313 | glBindVertexArray(0); 314 | } 315 | 316 | public void gltDrawText2DAligned(GLTtext text, float x, float y, float scale, int horizontalAlignment, int verticalAlignment) { 317 | if (text == null) { 318 | return; 319 | } 320 | 321 | if (text._dirty) { 322 | _gltUpdateBuffers(text); 323 | } 324 | 325 | if (text.vertexCount == 0) { 326 | return; 327 | } 328 | 329 | if (horizontalAlignment == GLT_CENTER) { 330 | x -= gltGetTextWidth(text, scale) * 0.5f; 331 | } else if (horizontalAlignment == GLT_RIGHT) { 332 | x -= gltGetTextWidth(text, scale); 333 | } 334 | 335 | if (verticalAlignment == GLT_CENTER) { 336 | y -= gltGetTextHeight(text, scale) * 0.5f; 337 | } else if (verticalAlignment == GLT_BOTTOM) { 338 | y -= gltGetTextHeight(text, scale); 339 | } 340 | 341 | gltDrawText2D(text, x, y, scale); 342 | } 343 | 344 | // GLT_API void gltDrawText3D(GLTtext *text, GLfloat x, GLfloat y, GLfloat z, GLfloat scale, GLfloat view[16], GLfloat projection[16]) 345 | //{ 346 | // if (!text) 347 | // return; 348 | // 349 | // if (text->_dirty) 350 | // _gltUpdateBuffers(text); 351 | // 352 | // if (!text->vertexCount) 353 | // return; 354 | // 355 | // const GLfloat model[16] = { 356 | // scale, 0.0f, 0.0f, 0.0f, 357 | // 0.0f, -scale, 0.0f, 0.0f, 358 | // 0.0f, 0.0f, scale, 0.0f, 359 | // x, y + (GLfloat)_gltFontGlyphHeight * scale, z, 1.0f, 360 | // }; 361 | // 362 | // GLfloat mvp[16]; 363 | // GLfloat vp[16]; 364 | // 365 | // _gltMat4Mult(projection, view, vp); 366 | // _gltMat4Mult(vp, model, mvp); 367 | // 368 | // _gltDrawText(); 369 | //} 370 | public void gltDrawText3D(GLTtext text, float x, float y, float z, float scale, float[] view, float[] projection) { 371 | if (text == null) { 372 | return; 373 | } 374 | 375 | if (text._dirty) { 376 | _gltUpdateBuffers(text); 377 | } 378 | 379 | if (text.vertexCount == 0) { 380 | return; 381 | } 382 | 383 | final float[] model = { 384 | scale, 0.0f, 0.0f, 0.0f, 385 | 0.0f, -scale, 0.0f, 0.0f, 386 | 0.0f, 0.0f, scale, 0.0f, 387 | x, y + (float) _gltFontGlyphHeight * scale, z, 1.0f, 388 | }; 389 | 390 | final float[] mvp = new float[16]; 391 | final float[] vp = new float[16]; 392 | _gltMat4Mult(projection, view, vp); 393 | _gltMat4Mult(vp, model, mvp); 394 | 395 | glUniformMatrix4fv(_gltText2DShaderMVPUniformLocation, false, mvp); 396 | 397 | glBindVertexArray(text._vao); 398 | glDrawArrays(GL_TRIANGLES, 0, text.vertexCount); 399 | glBindVertexArray(0); 400 | } 401 | 402 | // GLT_API void gltColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a) 403 | //{ 404 | // glUniform4f(_gltText2DShaderColorUniformLocation, r, g, b, a); 405 | //} 406 | public void gltColor(float r, float g, float b, float a) { 407 | glUniform4f(_gltText2DShaderColorUniformLocation, r, g, b, a); 408 | } 409 | 410 | // GLT_API void gltGetColor(GLfloat *r, GLfloat *g, GLfloat *b, GLfloat *a) 411 | //{ 412 | // GLfloat color[4]; 413 | // glGetUniformfv(_gltText2DShader, _gltText2DShaderColorUniformLocation, color); 414 | // 415 | // if (r) (*r) = color[0]; 416 | // if (g) (*g) = color[1]; 417 | // if (b) (*b) = color[2]; 418 | // if (a) (*a) = color[3]; 419 | //} 420 | public Vector4f gltGetColor() { 421 | final float[] color = new float[4]; 422 | glGetUniformfv(_gltText2DShader, _gltText2DShaderColorUniformLocation, color); 423 | 424 | return new Vector4f(color[0], color[1], color[2], color[3]); 425 | } 426 | 427 | // GLT_API GLfloat gltGetLineHeight(GLfloat scale) 428 | //{ 429 | // return (GLfloat)_gltFontGlyphHeight * scale; 430 | //} 431 | public static float gltGetLineHeight(float scale) { 432 | return (float) _gltFontGlyphHeight * scale; 433 | } 434 | 435 | // GLT_API GLfloat gltGetTextWidth(const GLTtext *text, GLfloat scale) 436 | //{ 437 | // if (!text || !text->_text) 438 | // return 0.0f; 439 | // 440 | // GLfloat maxWidth = 0.0f; 441 | // GLfloat width = 0.0f; 442 | // 443 | // _GLTglyph glyph; 444 | // 445 | // char c; 446 | // int i; 447 | // for (i = 0; i < text->_textLength; i++) 448 | // { 449 | // c = text->_text[i]; 450 | // 451 | // if ((c == '\n') || (c == '\r')) 452 | // { 453 | // if (width > maxWidth) 454 | // maxWidth = width; 455 | // 456 | // width = 0.0f; 457 | // 458 | // continue; 459 | // } 460 | // 461 | // if (!gltIsCharacterSupported(c)) 462 | // { 463 | //#ifdef GLT_UNKNOWN_CHARACTER 464 | // c = GLT_UNKNOWN_CHARACTER; 465 | // if (!gltIsCharacterSupported(c)) 466 | // continue; 467 | //#else 468 | // continue; 469 | //#endif 470 | // } 471 | // 472 | // glyph = _gltFontGlyphs2[c - _gltFontGlyphMinChar]; 473 | // 474 | // width += (GLfloat)glyph.w; 475 | // } 476 | // 477 | // if (width > maxWidth) 478 | // maxWidth = width; 479 | // 480 | // return maxWidth * scale; 481 | //} 482 | public float gltGetTextWidth(GLTtext text, float scale) { 483 | if (text == null) { 484 | return 0.0f; 485 | } 486 | 487 | float maxWidth = 0.0f; 488 | float width = 0.0f; 489 | 490 | for (int i = 0; i < text._textLength; i++) { 491 | char c = text._text.charAt(i); 492 | 493 | if ((c == '\n') || (c == '\r')) { 494 | if (width > maxWidth) { 495 | maxWidth = width; 496 | } 497 | 498 | width = 0.0f; 499 | 500 | continue; 501 | } 502 | 503 | if (!gltIsCharacterSupported(c)) { 504 | //#ifdef GLT_UNKNOWN_CHARACTER 505 | // c = GLT_UNKNOWN_CHARACTER; 506 | // if (!gltIsCharacterSupported(c)) 507 | // continue; 508 | //#else 509 | // continue; 510 | //#endif 511 | c = ' '; 512 | if (!gltIsCharacterSupported(c)) { 513 | continue; 514 | } 515 | } 516 | 517 | _GLTglyph glyph = _gltFontGlyphs2[c - _gltFontGlyphMinChar]; 518 | 519 | width += (float) glyph.w; 520 | } 521 | 522 | if (width > maxWidth) { 523 | maxWidth = width; 524 | } 525 | 526 | return maxWidth * scale; 527 | } 528 | 529 | // GLT_API GLfloat gltGetTextHeight(const GLTtext *text, GLfloat scale) 530 | //{ 531 | // if (!text || !text->_text) 532 | // return 0.0f; 533 | // 534 | // return (GLfloat)(gltCountNewLines(text->_text) + 1) * gltGetLineHeight(scale); 535 | //} 536 | public static float gltGetTextHeight(GLTtext text, float scale) { 537 | if (text == null || text._text == null) { 538 | return 0.0f; 539 | } 540 | 541 | return (float) (gltCountNewLines(text) + 1) * gltGetLineHeight(scale); 542 | } 543 | 544 | // GLT_API GLboolean gltIsCharacterSupported(const char c) 545 | //{ 546 | // if (c == '\t') return GL_TRUE; 547 | // if (c == '\n') return GL_TRUE; 548 | // if (c == '\r') return GL_TRUE; 549 | // 550 | // int i; 551 | // for (i = 0; i < _gltFontGlyphCount; i++) 552 | // { 553 | // if (_gltFontGlyphCharacters[i] == c) 554 | // return GL_TRUE; 555 | // } 556 | // 557 | // return GL_FALSE; 558 | //} 559 | public static boolean gltIsCharacterSupported(char c) { 560 | if (c == '\t') { 561 | return true; 562 | } 563 | if (c == '\n') { 564 | return true; 565 | } 566 | if (c == '\r') { 567 | return true; 568 | } 569 | 570 | for (int i = 0; i < _gltFontGlyphCount; i++) { 571 | if (_gltFontGlyphCharacters.charAt(i) == c) { 572 | return true; 573 | } 574 | } 575 | 576 | return false; 577 | } 578 | 579 | // GLT_API GLint gltCountSupportedCharacters(const char *str) 580 | //{ 581 | // if (!str) 582 | // return 0; 583 | // 584 | // GLint count = 0; 585 | // 586 | // while ((*str) != '\0') 587 | // { 588 | // if (gltIsCharacterSupported(*str)) 589 | // count++; 590 | // 591 | // str++; 592 | // } 593 | // 594 | // return count; 595 | //} 596 | public static int gltCountSupportedCharacters(String str) { 597 | if (str == null) { 598 | return 0; 599 | } 600 | 601 | int count = 0; 602 | 603 | for (int i = 0; i < str.length(); i++) { 604 | if (gltIsCharacterSupported(str.charAt(i))) { 605 | count++; 606 | } 607 | } 608 | 609 | return count; 610 | } 611 | 612 | // GLT_API GLboolean gltIsCharacterDrawable(const char c) 613 | //{ 614 | // if (c < _gltFontGlyphMinChar) return GL_FALSE; 615 | // if (c > _gltFontGlyphMaxChar) return GL_FALSE; 616 | // 617 | // if (_gltFontGlyphs2[c - _gltFontGlyphMinChar].drawable) 618 | // return GL_TRUE; 619 | // 620 | // return GL_FALSE; 621 | //} 622 | public boolean gltIsCharacterDrawable(char c) { 623 | if (c < _gltFontGlyphMinChar) { 624 | return false; 625 | } 626 | if (c > _gltFontGlyphMaxChar) { 627 | return false; 628 | } 629 | 630 | if (_gltFontGlyphs2[c - _gltFontGlyphMinChar].drawable) { 631 | return true; 632 | } 633 | 634 | return false; 635 | } 636 | 637 | // GLT_API GLint gltCountDrawableCharacters(const char *str) 638 | //{ 639 | // if (!str) 640 | // return 0; 641 | // 642 | // GLint count = 0; 643 | // 644 | // while ((*str) != '\0') 645 | // { 646 | // if (gltIsCharacterDrawable(*str)) 647 | // count++; 648 | // 649 | // str++; 650 | // } 651 | // 652 | // return count; 653 | //} 654 | public int gltCountDrawableCharacters(String str) { 655 | if (str == null) { 656 | return 0; 657 | } 658 | 659 | int count = 0; 660 | 661 | for (int i = 0; i < str.length(); i++) { 662 | if (gltIsCharacterDrawable(str.charAt(i))) { 663 | count++; 664 | } 665 | } 666 | 667 | return count; 668 | } 669 | 670 | // GLT_API GLint gltCountNewLines(const char *str) 671 | //{ 672 | // GLint count = 0; 673 | // 674 | // while ((str = strchr(str, '\n')) != NULL) 675 | // { 676 | // count++; 677 | // str++; 678 | // } 679 | // 680 | // return count; 681 | //} 682 | public static int gltCountNewLines(GLTtext text) { 683 | int count = 0; 684 | 685 | for (int i = 0; i < text._text.length(); i++) { 686 | if (text._text.charAt(i) == '\n') { 687 | count++; 688 | } 689 | } 690 | 691 | return count; 692 | } 693 | 694 | // GLT_API void _gltGetViewportSize(GLint *width, GLint *height) 695 | //{ 696 | // GLint dimensions[4]; 697 | // glGetIntegerv(GL_VIEWPORT, dimensions); 698 | // 699 | // if (width) (*width) = dimensions[2]; 700 | // if (height) (*height) = dimensions[3]; 701 | //} 702 | public static void _gltGetViewportSize(int[] width, int[] height) { 703 | int[] dimensions = new int[4]; 704 | glGetIntegerv(GL_VIEWPORT, dimensions); 705 | 706 | if (width != null) { 707 | width[0] = dimensions[2]; 708 | } 709 | if (height != null) { 710 | height[0] = dimensions[3]; 711 | } 712 | } 713 | 714 | // GLT_API void _gltMat4Mult(const GLfloat lhs[16], const GLfloat rhs[16], GLfloat result[16]) 715 | //{ 716 | // int c, r, i; 717 | // 718 | // for (c = 0; c < 4; c++) 719 | // { 720 | // for (r = 0; r < 4; r++) 721 | // { 722 | // result[_GLT_MAT4_INDEX(r, c)] = 0.0f; 723 | // 724 | // for (i = 0; i < 4; i++) 725 | // result[_GLT_MAT4_INDEX(r, c)] += lhs[_GLT_MAT4_INDEX(r, i)] * rhs[_GLT_MAT4_INDEX(i, c)]; 726 | // } 727 | // } 728 | //} 729 | public static void _gltMat4Mult(float[] lhs, float[] rhs, float[] result) { 730 | int c, r, i; 731 | 732 | for (c = 0; c < 4; c++) { 733 | for (r = 0; r < 4; r++) { 734 | result[_GLT_MAT4_INDEX(r, c)] = 0.0f; 735 | 736 | for (i = 0; i < 4; i++) { 737 | result[_GLT_MAT4_INDEX(r, c)] += lhs[_GLT_MAT4_INDEX(r, i)] * rhs[_GLT_MAT4_INDEX(i, c)]; 738 | } 739 | } 740 | } 741 | } 742 | 743 | // GLT_API void _gltUpdateBuffers(GLTtext *text) 744 | //{ 745 | // if (!text || !text->_dirty) 746 | // return; 747 | // 748 | // if (text->_vertices) 749 | // { 750 | // text->vertexCount = 0; 751 | // 752 | // free(text->_vertices); 753 | // text->_vertices = GLT_NULL; 754 | // } 755 | // 756 | // if (!text->_text || !text->_textLength) 757 | // { 758 | // text->_dirty = GL_FALSE; 759 | // return; 760 | // } 761 | // 762 | // const GLsizei countDrawable = gltCountDrawableCharacters(text->_text); 763 | // 764 | // if (!countDrawable) 765 | // { 766 | // text->_dirty = GL_FALSE; 767 | // return; 768 | // } 769 | // 770 | // const GLsizei vertexCount = countDrawable * 2 * 3; // 3 vertices in a triangle, 2 triangles in a quad 771 | // 772 | // const GLsizei vertexSize = _GLT_TEXT2D_VERTEX_SIZE; 773 | // GLfloat *vertices = (GLfloat*)malloc(vertexCount * vertexSize * sizeof(GLfloat)); 774 | // 775 | // if (!vertices) 776 | // return; 777 | // 778 | // GLsizei vertexElementIndex = 0; 779 | // 780 | // GLfloat glyphX = 0.0f; 781 | // GLfloat glyphY = 0.0f; 782 | // 783 | // GLfloat glyphWidth; 784 | // const GLfloat glyphHeight = (GLfloat)_gltFontGlyphHeight; 785 | // 786 | // const GLfloat glyphAdvanceX = 0.0f; 787 | // const GLfloat glyphAdvanceY = 0.0f; 788 | // 789 | // _GLTglyph glyph; 790 | // 791 | // char c; 792 | // int i; 793 | // for (i = 0; i < text->_textLength; i++) 794 | // { 795 | // c = text->_text[i]; 796 | // 797 | // if (c == '\n') 798 | // { 799 | // glyphX = 0.0f; 800 | // glyphY += glyphHeight + glyphAdvanceY; 801 | // 802 | // continue; 803 | // } 804 | // else if (c == '\r') 805 | // { 806 | // glyphX = 0.0f; 807 | // 808 | // continue; 809 | // } 810 | // 811 | // if (!gltIsCharacterSupported(c)) 812 | // { 813 | //#ifdef GLT_UNKNOWN_CHARACTER 814 | // c = GLT_UNKNOWN_CHARACTER; 815 | // if (!gltIsCharacterSupported(c)) 816 | // continue; 817 | //#else 818 | // continue; 819 | //#endif 820 | // } 821 | // 822 | // glyph = _gltFontGlyphs2[c - _gltFontGlyphMinChar]; 823 | // 824 | // glyphWidth = (GLfloat)glyph.w; 825 | // 826 | // if (glyph.drawable) 827 | // { 828 | // vertices[vertexElementIndex++] = glyphX; 829 | // vertices[vertexElementIndex++] = glyphY; 830 | // vertices[vertexElementIndex++] = glyph.u1; 831 | // vertices[vertexElementIndex++] = glyph.v1; 832 | // 833 | // vertices[vertexElementIndex++] = glyphX + glyphWidth; 834 | // vertices[vertexElementIndex++] = glyphY + glyphHeight; 835 | // vertices[vertexElementIndex++] = glyph.u2; 836 | // vertices[vertexElementIndex++] = glyph.v2; 837 | // 838 | // vertices[vertexElementIndex++] = glyphX + glyphWidth; 839 | // vertices[vertexElementIndex++] = glyphY; 840 | // vertices[vertexElementIndex++] = glyph.u2; 841 | // vertices[vertexElementIndex++] = glyph.v1; 842 | // 843 | // vertices[vertexElementIndex++] = glyphX; 844 | // vertices[vertexElementIndex++] = glyphY; 845 | // vertices[vertexElementIndex++] = glyph.u1; 846 | // vertices[vertexElementIndex++] = glyph.v1; 847 | // 848 | // vertices[vertexElementIndex++] = glyphX; 849 | // vertices[vertexElementIndex++] = glyphY + glyphHeight; 850 | // vertices[vertexElementIndex++] = glyph.u1; 851 | // vertices[vertexElementIndex++] = glyph.v2; 852 | // 853 | // vertices[vertexElementIndex++] = glyphX + glyphWidth; 854 | // vertices[vertexElementIndex++] = glyphY + glyphHeight; 855 | // vertices[vertexElementIndex++] = glyph.u2; 856 | // vertices[vertexElementIndex++] = glyph.v2; 857 | // } 858 | // 859 | // glyphX += glyphWidth + glyphAdvanceX; 860 | // } 861 | // 862 | // text->vertexCount = vertexCount; 863 | // text->_vertices = vertices; 864 | // 865 | // glBindBuffer(GL_ARRAY_BUFFER, text->_vbo); 866 | // glBufferData(GL_ARRAY_BUFFER, vertexCount * _GLT_TEXT2D_VERTEX_SIZE * sizeof(GLfloat), vertices, GL_DYNAMIC_DRAW); 867 | // 868 | // text->_dirty = GL_FALSE; 869 | //} 870 | public void _gltUpdateBuffers(GLTtext text) { 871 | if (text == null || !text._dirty) 872 | return; 873 | 874 | if (text._vertices != null) { 875 | text.vertexCount = 0; 876 | text._vertices = null; 877 | } 878 | 879 | if (text._text == null || text._textLength == 0) { 880 | text._dirty = false; 881 | return; 882 | } 883 | 884 | int countDrawable = gltCountDrawableCharacters(text._text); 885 | 886 | if (countDrawable == 0) { 887 | text._dirty = false; 888 | return; 889 | } 890 | 891 | int vertexCount = countDrawable * 2 * 3; // 3 vertices in a triangle, 2 triangles in a quad 892 | 893 | int vertexSize = _GLT_TEXT2D_VERTEX_SIZE; 894 | float[] vertices = new float[vertexCount * vertexSize]; 895 | 896 | if (vertices == null) 897 | return; 898 | 899 | int vertexElementIndex = 0; 900 | 901 | float glyphX = 0.0f; 902 | float glyphY = 0.0f; 903 | 904 | float glyphWidth; 905 | float glyphHeight = (float) _gltFontGlyphHeight; 906 | 907 | float glyphAdvanceX = 0.0f; 908 | float glyphAdvanceY = 0.0f; 909 | 910 | _GLTglyph glyph; 911 | 912 | for (int i = 0; i < text._textLength; i++) { 913 | char c = text._text.charAt(i); 914 | 915 | if (c == '\n') { 916 | glyphX = 0.0f; 917 | glyphY += glyphHeight + glyphAdvanceY; 918 | continue; 919 | } else if (c == '\r') { 920 | glyphX = 0.0f; 921 | continue; 922 | } 923 | 924 | if (!gltIsCharacterSupported(c)) { 925 | // Handle unsupported character 926 | continue; 927 | } 928 | 929 | glyph = _gltFontGlyphs2[c - _gltFontGlyphMinChar]; 930 | 931 | glyphWidth = (float) glyph.w; 932 | 933 | if (glyph.drawable) { 934 | vertices[vertexElementIndex++] = glyphX; 935 | vertices[vertexElementIndex++] = glyphY; 936 | vertices[vertexElementIndex++] = glyph.u1; 937 | vertices[vertexElementIndex++] = glyph.v1; 938 | 939 | vertices[vertexElementIndex++] = glyphX + glyphWidth; 940 | vertices[vertexElementIndex++] = glyphY + glyphHeight; 941 | vertices[vertexElementIndex++] = glyph.u2; 942 | vertices[vertexElementIndex++] = glyph.v2; 943 | 944 | vertices[vertexElementIndex++] = glyphX + glyphWidth; 945 | vertices[vertexElementIndex++] = glyphY; 946 | vertices[vertexElementIndex++] = glyph.u2; 947 | vertices[vertexElementIndex++] = glyph.v1; 948 | 949 | vertices[vertexElementIndex++] = glyphX; 950 | vertices[vertexElementIndex++] = glyphY; 951 | vertices[vertexElementIndex++] = glyph.u1; 952 | vertices[vertexElementIndex++] = glyph.v1; 953 | 954 | vertices[vertexElementIndex++] = glyphX; 955 | vertices[vertexElementIndex++] = glyphY + glyphHeight; 956 | vertices[vertexElementIndex++] = glyph.u1; 957 | vertices[vertexElementIndex++] = glyph.v2; 958 | 959 | vertices[vertexElementIndex++] = glyphX + glyphWidth; 960 | vertices[vertexElementIndex++] = glyphY + glyphHeight; 961 | vertices[vertexElementIndex++] = glyph.u2; 962 | vertices[vertexElementIndex++] = glyph.v2; 963 | } 964 | 965 | glyphX += glyphWidth + glyphAdvanceX; 966 | } 967 | 968 | text.vertexCount = vertexCount; 969 | text._vertices = vertices; 970 | 971 | glBindBuffer(GL_ARRAY_BUFFER, text._vbo); 972 | glBufferData(GL_ARRAY_BUFFER, vertices, GL_DYNAMIC_DRAW); 973 | glBindBuffer(GL_ARRAY_BUFFER, 0); 974 | 975 | text._dirty = false; 976 | } 977 | 978 | // GLT_API GLboolean gltInit(void) 979 | //{ 980 | // if (gltInitialized) 981 | // return GL_TRUE; 982 | // 983 | // if (!_gltCreateText2DShader()) 984 | // return GL_FALSE; 985 | // 986 | // if (!_gltCreateText2DFontTexture()) 987 | // return GL_FALSE; 988 | // 989 | // gltInitialized = GL_TRUE; 990 | // return GL_TRUE; 991 | //} 992 | { 993 | gltInit(); 994 | }; 995 | private void gltInit() { 996 | _gltCreateText2DShader(); 997 | _gltCreateText2DFontTexture(); 998 | } 999 | 1000 | // GLT_API void gltTerminate(void) 1001 | //{ 1002 | // if (_gltText2DShader != GLT_NULL_HANDLE) 1003 | // { 1004 | // glDeleteProgram(_gltText2DShader); 1005 | // _gltText2DShader = GLT_NULL_HANDLE; 1006 | // } 1007 | // 1008 | // if (_gltText2DFontTexture != GLT_NULL_HANDLE) 1009 | // { 1010 | // glDeleteTextures(1, &_gltText2DFontTexture); 1011 | // _gltText2DFontTexture = GLT_NULL_HANDLE; 1012 | // } 1013 | // 1014 | // gltInitialized = GL_FALSE; 1015 | //} 1016 | public void gltTerminate() { 1017 | if (_gltText2DShader != GLT_NULL_HANDLE) { 1018 | glDeleteProgram(_gltText2DShader); 1019 | _gltText2DShader = GLT_NULL_HANDLE; 1020 | } 1021 | 1022 | if (_gltText2DFontTexture != GLT_NULL_HANDLE) { 1023 | glDeleteTextures(_gltText2DFontTexture); 1024 | _gltText2DFontTexture = GLT_NULL_HANDLE; 1025 | } 1026 | } 1027 | 1028 | // static const GLchar* _gltText2DVertexShaderSource = 1029 | //"#version 330 core\n" 1030 | //"\n" 1031 | //"in vec2 position;\n" 1032 | //"in vec2 texCoord;\n" 1033 | //"\n" 1034 | //"uniform mat4 mvp;\n" 1035 | //"\n" 1036 | //"out vec2 fTexCoord;\n" 1037 | //"\n" 1038 | //"void main()\n" 1039 | //"{\n" 1040 | //" fTexCoord = texCoord;\n" 1041 | //" \n" 1042 | //" gl_Position = mvp * vec4(position, 0.0, 1.0);\n" 1043 | //"}\n"; 1044 | private static final String _gltText2DVertexShaderSource = 1045 | """ 1046 | #version 150 core 1047 | 1048 | in vec2 position; 1049 | in vec2 texCoord; 1050 | 1051 | uniform mat4 mvp; 1052 | 1053 | out vec2 fTexCoord; 1054 | 1055 | void main() 1056 | { 1057 | fTexCoord = texCoord; 1058 | 1059 | gl_Position = mvp * vec4(position, 0.0, 1.0); 1060 | } 1061 | """; 1062 | 1063 | // static const GLchar* _gltText2DFragmentShaderSource = 1064 | //"#version 330 core\n" 1065 | //"\n" 1066 | //"out vec4 fragColor;\n" 1067 | //"\n" 1068 | //"uniform sampler2D diffuse;\n" 1069 | //"\n" 1070 | //"uniform vec4 color = vec4(1.0, 1.0, 1.0, 1.0);\n" 1071 | //"\n" 1072 | //"in vec2 fTexCoord;\n" 1073 | //"\n" 1074 | //"void main()\n" 1075 | //"{\n" 1076 | //" fragColor = texture(diffuse, fTexCoord) * color;\n" 1077 | //"}\n"; 1078 | private static final String _gltText2DFragmentShaderSource = 1079 | """ 1080 | #version 150 core 1081 | 1082 | out vec4 fragColor; 1083 | 1084 | uniform sampler2D diffuse; 1085 | 1086 | uniform vec4 color; 1087 | 1088 | in vec2 fTexCoord; 1089 | 1090 | void main() 1091 | { 1092 | vec4 outColor = texture(diffuse, fTexCoord) * color; 1093 | if (outColor.a < 0.01) 1094 | discard; 1095 | fragColor = outColor; 1096 | } 1097 | """; 1098 | 1099 | public void _gltCreateText2DShader() { 1100 | int vertexShader, fragmentShader; 1101 | int compileStatus, linkStatus; 1102 | 1103 | vertexShader = glCreateShader(GL_VERTEX_SHADER); 1104 | glShaderSource(vertexShader, _gltText2DVertexShaderSource); 1105 | glCompileShader(vertexShader); 1106 | 1107 | compileStatus = glGetShaderi(vertexShader, GL_COMPILE_STATUS); 1108 | 1109 | if (compileStatus != GL_TRUE) { 1110 | int infoLogLength = glGetShaderi(vertexShader, GL_INFO_LOG_LENGTH); 1111 | 1112 | RuntimeException e; 1113 | if (infoLogLength > 1) { 1114 | final String infoLog = glGetShaderInfoLog(vertexShader, infoLogLength); 1115 | 1116 | System.out.printf("Vertex Shader #%d :\n%s\n", vertexShader, infoLog); 1117 | e = new RuntimeException("Failed to compile vertex shader: " + infoLog); 1118 | } else { 1119 | e = new RuntimeException("Failed to compile vertex shader"); 1120 | } 1121 | 1122 | glDeleteShader(vertexShader); 1123 | gltTerminate(); 1124 | 1125 | throw e; 1126 | } 1127 | 1128 | fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); 1129 | glShaderSource(fragmentShader, _gltText2DFragmentShaderSource); 1130 | glCompileShader(fragmentShader); 1131 | 1132 | compileStatus = glGetShaderi(fragmentShader, GL_COMPILE_STATUS); 1133 | 1134 | if (compileStatus != GL_TRUE) { 1135 | int infoLogLength = glGetShaderi(fragmentShader, GL_INFO_LOG_LENGTH); 1136 | 1137 | RuntimeException e; 1138 | if (infoLogLength > 1) { 1139 | final String infoLog = glGetShaderInfoLog(fragmentShader, infoLogLength); 1140 | 1141 | System.out.printf("Fragment Shader #%d :\n%s\n", fragmentShader, infoLog); 1142 | e = new RuntimeException("Failed to compile fragment shader: " + infoLog); 1143 | } else { 1144 | e = new RuntimeException("Failed to compile fragment shader"); 1145 | } 1146 | 1147 | glDeleteShader(vertexShader); 1148 | glDeleteShader(fragmentShader); 1149 | gltTerminate(); 1150 | 1151 | throw e; 1152 | } 1153 | 1154 | _gltText2DShader = glCreateProgram(); 1155 | 1156 | glAttachShader(_gltText2DShader, vertexShader); 1157 | glAttachShader(_gltText2DShader, fragmentShader); 1158 | 1159 | glBindAttribLocation(_gltText2DShader, _GLT_TEXT2D_POSITION_LOCATION, "position"); 1160 | glBindAttribLocation(_gltText2DShader, _GLT_TEXT2D_TEXCOORD_LOCATION, "texCoord"); 1161 | 1162 | glBindFragDataLocation(_gltText2DShader, 0, "fragColor"); 1163 | 1164 | glLinkProgram(_gltText2DShader); 1165 | 1166 | glDetachShader(_gltText2DShader, vertexShader); 1167 | glDeleteShader(vertexShader); 1168 | 1169 | glDetachShader(_gltText2DShader, fragmentShader); 1170 | glDeleteShader(fragmentShader); 1171 | 1172 | linkStatus = glGetProgrami(_gltText2DShader, GL_LINK_STATUS); 1173 | 1174 | if (linkStatus != GL_TRUE) { 1175 | int infoLogLength = glGetProgrami(_gltText2DShader, GL_INFO_LOG_LENGTH); 1176 | 1177 | RuntimeException e; 1178 | if (infoLogLength > 1) { 1179 | final String infoLog = glGetProgramInfoLog(_gltText2DShader, infoLogLength); 1180 | 1181 | System.out.printf("Shader Program #%d :\n%s\n", _gltText2DShader, infoLog); 1182 | e = new RuntimeException("Failed to link shader program: " + infoLog); 1183 | } else { 1184 | e = new RuntimeException("Failed to link shader program"); 1185 | } 1186 | 1187 | gltTerminate(); 1188 | 1189 | throw e; 1190 | } 1191 | 1192 | glUseProgram(_gltText2DShader); 1193 | 1194 | _gltText2DShaderMVPUniformLocation = glGetUniformLocation(_gltText2DShader, "mvp"); 1195 | _gltText2DShaderColorUniformLocation = glGetUniformLocation(_gltText2DShader, "color"); 1196 | 1197 | gltColor(1.0F, 1.0F, 1.0F, 1.0F); 1198 | 1199 | glUniform1i(glGetUniformLocation(_gltText2DShader, "diffuse"), 0); 1200 | 1201 | glUseProgram(0); 1202 | } 1203 | 1204 | private static final long[] _gltFontGlyphRects = new long[] { 1205 | 0x1100040000L, 0x304090004L, 0x30209000DL, 0x304090016L, 0x30209001FL, 0x304090028L, 0x302090031L, 0x409003AL, 1206 | 0x302090043L, 0x30109004CL, 0x1080055L, 0x30209005DL, 0x302090066L, 0x3040A006FL, 0x304090079L, 0x304090082L, 1207 | 0x409008BL, 0x4090094L, 0x30409009DL, 0x3040900A6L, 0x3020900AFL, 0x3040900B8L, 0x3040900C1L, 0x3040A00CAL, 1208 | 0x3040900D4L, 0x40A00DD, 0x3040900E7L, 0x3020900F0L, 0x3020900F9L, 0x302090102L, 0x30209010BL, 0x302090114L, 1209 | 0x30209011DL, 0x302090126L, 0x30209012FL, 0x302070138L, 0x30209013FL, 0x302090148L, 0x302090151L, 0x3020A015AL, 1210 | 0x3020A0164L, 0x30209016EL, 0x302090177L, 0x102090180L, 0x302090189L, 0x302090192L, 0x30209019BL, 0x3020901A4L, 1211 | 0x3020901ADL, 0x3020A01B6L, 0x3020901C0L, 0x3020901C9L, 0x3020901D2L, 0x3020901DBL, 0x3020901E4L, 0x3020901EDL, 1212 | 0x3020901F6L, 0x3020A01FFL, 0x302090209L, 0x302090212L, 0x30209021BL, 0x302090224L, 0x30209022DL, 0x309060236L, 1213 | 0x10906023CL, 0x302070242L, 0x302090249L, 0x706090252L, 0x50409025BL, 0x202090264L, 0x10207026DL, 0x102070274L, 1214 | 0x30406027BL, 0x104060281L, 0x2010B0287L, 0x3020A0292L, 0xB0007029CL, 0x5040A02AAL, 0x3020A02B4L, 0x6050902BEL, 1215 | 0x20702C7L, 0x20702CEL, 0xB010902D5L, 1216 | }; 1217 | 1218 | private static final int _GLT_FONT_PIXEL_SIZE_BITS = 2; 1219 | private static final int _gltFontGlyphDataCount = 387; 1220 | 1221 | private static final long[] _gltFontGlyphData = Arrays.copyOf(new long[] { 1222 | 0x551695416A901554L, 0x569695A5A56AA55AL, 0x0555554155545AA9L, 0x916AA41569005A40L, 0xA5A569695A5A5696L, 0x51555556AA569695L, 0x696916A941554155L, 0x69155A55569555A5L, 1223 | 0x15541555456A9569L, 0xA9569545A4005500L, 0x569695A5A569695AL, 0x5545AA9569695A5AL, 0x916A941554555415L, 0x55A56AA95A5A5696L, 0x40555416A9555695L, 0x55A45AA505550155L, 1224 | 0xA55AAA455A555691L, 0x0169005A45569155L, 0xA945554015400554L, 0x569695A5A569695AL, 0x9545AA9569695A5AL, 0x4555555AA95A5556L, 0x55A4016900154555L, 0xA569695A5A45AA90L, 1225 | 0x69695A5A569695A5L, 0x9001541555455555L, 0x05AA4155505A4016L, 0xA40169005A501695L, 0x155555AAA4569505L, 0x5A405A4015405555L, 0x5A505A545AA45554L, 0x5A405A405A405A40L, 1226 | 0x555556A95A555A40L, 0x569005A400551554L, 0x9569A569695A5A45L, 0xA5A56969169A556AL, 0xA405555555155555L, 0x169005A50169505AL, 0x9505A40169005A40L, 0x5555155555AAA456L, 1227 | 0x95A66916AA905555L, 0x695A6695A6695A66L, 0x154555555A5695A6L, 0x5696916AA4155555L, 0x9695A5A569695A5AL, 0x45555155555A5A56L, 0xA5A5696916A94155L, 0x9569695A5A569695L, 1228 | 0x155515541555456AL, 0x695A5A5696916AA4L, 0x56AA569695A5A569L, 0x5540169155A55569L, 0x56AA515550015400L, 0x9695A5A569695A5AL, 0x05A5516AA55A5A56L, 0x500155005A401695L, 1229 | 0xA56A695A5A455555L, 0x0169015A55569556L, 0x54005500155005A4L, 0x555A555695AA9455L, 0xAA569555A5505AA5L, 0x0015415551555556L, 0x5A55AAA455A50169L, 0x40169005A4556915L, 1230 | 0x550155505AA5055AL, 0xA569695A5A455555L, 0x69695A5A569695A5L, 0x555554155545AA95L, 0x5A5A569695A5A455L, 0xA515AA55A5A56969L, 0x1545505500555055L, 0x95A6695A6695A569L, 1231 | 0xA4569A55A6695A66L, 0x5551555015554569L, 0x456A9569695A5A45L, 0xA5A5696916A95569L, 0x4155545555155555L, 0xA45A5A45A5A45A5AL, 0xA945A5A45A5A45A5L, 0x56A915A555695056L, 1232 | 0x4555501554055551L, 0x6945695169555AAAL, 0x55AAA45569156955L, 0x5A50055055551555L, 0xA569695A5A45AA50L, 0x69695A5A56AA95A5L, 0x555555155555A5A5L, 0x5A5A5696916AA415L, 1233 | 0x5A5696956AA56969L, 0x1555556AA569695AL, 0x96916A9415541555L, 0x9555A555695A5A56L, 0x6A9569695A5A4556L, 0xA405551554155545L, 0x69695A5A45A6905AL, 0x695A5A569695A5A5L, 1234 | 0x05550555555AA55AL, 0x5A555695AAA45555L, 0x4556916AA4156955L, 0x5555AAA45569155AL, 0x95AAA45555555515L, 0x6AA41569555A5556L, 0xA40169155A455691L, 0x1554005500155005L, 1235 | 0x695A5A5696916A94L, 0x5A5A56A69555A555L, 0x54155545AA956969L, 0x569695A5A4555555L, 0x9695AAA569695A5AL, 0x55A5A569695A5A56L, 0x55AA455555551555L, 0x5A416905A456915AL, 1236 | 0x515555AA45A51690L, 0x169005A400550055L, 0x9555A40169005A40L, 0x456A9569695A5A56L, 0xA5A4555515541555L, 0xA55A69569A569695L, 0x6969169A45A6915AL, 0x555555155555A5A5L, 1237 | 0x005A40169005A400L, 0x5A40169005A40169L, 0x155555AAA4556900L, 0x695A569154555555L, 0x6695A6695A9A95A5L, 0xA5695A5695A6695AL, 0x55154555555A5695L, 0x95A5695A56915455L, 1238 | 0x695AA695A6A95A5AL, 0x5695A5695A5695A9L, 0x155455154555555AL, 0x695A5A5696916A94L, 0x5A5A569695A5A569L, 0x541555456A956969L, 0x5696916AA4155515L, 0x56956AA569695A5AL, 1239 | 0x5005A40169155A55L, 0x6A94155400550015L, 0xA569695A5A569691L, 0x69695A5A569695A5L, 0x005A5415A5456A95L, 0x16AA415555500155L, 0xAA569695A5A56969L, 0x569695A5A55A6956L, 1240 | 0x5545555155555A5AL, 0x5555A5696916A941L, 0xA5545A5005A5155AL, 0x41555456A9569695L, 0x56955AAA45555155L, 0x9005A40169055A51L, 0x05A40169005A4016L, 0x5A45555055001550L, 1241 | 0x569695A5A569695AL, 0x9695A5A569695A5AL, 0x515541555456A956L, 0xA5A569695A5A4555L, 0xA569695A5A569695L, 0x555055A515AA55A5L, 0x95A5691545505500L, 0x695A6695A5695A56L, 1242 | 0x9A4569A55A6695A6L, 0x555015554169A456L, 0x9569695A5A455551L, 0x5A6515A515694566L, 0x555A5A569695A5A4L, 0x5A5A455555555155L, 0xA9569695A5A56969L, 0x0169015A41569456L, 1243 | 0x55505500155005A4L, 0x05A55169555AAA45L, 0x55A455A555A515A5L, 0x5155555AAA455690L, 0x696916A941554555L, 0xA95A5A56A695A9A5L, 0x56A9569695A6A569L, 0x9401540155415554L, 1244 | 0x05A5516AA45A9516L, 0xA40169005A401695L, 0x4154005540169005L, 0xA5A5696916A94155L, 0x9556945695169555L, 0x55555AAA45569156L, 0x6916A94155455551L, 0x56A5169555A5A569L, 1245 | 0xA9569695A5A56955L, 0x0015415541555456L, 0x4169A4055A4005A4L, 0xA916969169A5169AL, 0x50056954569555AAL, 0x5AAA455551540015L, 0xAA41569555A55569L, 0x55A555A551695516L, 1246 | 0x55005550555555AAL, 0x915694569416A401L, 0xA5A569695A5A45AAL, 0x41555456A9569695L, 0x69555AAA45555155L, 0x9415A415A5056951L, 0x0169015A40569056L, 0xA941554015400554L, 1247 | 0x569A95A5A5696916L, 0x9695A5A56A6956A9L, 0x415541555456A956L, 0xA5A5696916A94155L, 0x516AA55A5A569695L, 0x155415A915694569L, 0x555A95A915505540L, 0x5A55A95A91555545L, 1248 | 0x1694154154555569L, 0xA456956A95AA56A9L, 0x055416905A415515L, 0x696916A941554154L, 0x9055A515A555A5A5L, 0x05A4016900554056L, 0xAA45555055001550L, 0x005505555155555AL, 1249 | 0x6955AAA4569505A4L, 0x005500155055A515L, 0x690169405A400550L, 0x90569415A415A505L, 0x0569015A415A5056L, 0x6941540015400554L, 0xA456915A55A55691L, 0x16905A505A416905L, 1250 | 0x6901555405541694L, 0x16905A505A416941L, 0x6955A45A516905A4L, 0xA455415415545695L, 0x6A45555515556A56L, 0x56A45555515556A5L, 0xA56A45555515556AL, 0x5505515555A56956L, 1251 | 0x690569A4016A5001L, 0x4056954169A9459AL, 0x416A690156941569L, 0x15A9505A695169A6L, 0x4015505540055540L, 0x94169A4169A405A9L, 0x5A56A9A4555A415AL, 0x555169A955A5A55AL, 1252 | 0x6945A90555555415L, 0x1555154055416941L, 0x56AAA456A545A690L, 0x40555515A69156A5L, 0x6945A69015550555L, 0xA6915A6956AAA45AL, 0x5A6956AAA45A6955L, 0x455540555515A691L, 1253 | 0xAA9555556AA91555L, 0xA915555554555556L, 0x416905A556955A56L, 0x5A416905A416905AL, 0x555515555AA45690L, 0x6905A516955AA455L, 0x416905A416905A41L, 0x55556A95A556905AL, 1254 | 0xA5A5696915555554L, 0x5555155555L, 1255 | }, _gltFontGlyphDataCount); 1256 | 1257 | private void _gltCreateText2DFontTexture() { 1258 | for (int i = 0; i < _gltFontGlyphs.length; i ++) { 1259 | _gltFontGlyphs[i] = new _GLTglyph(); 1260 | } 1261 | for (int i = 0; i < _gltFontGlyphs2.length; i ++) { 1262 | _gltFontGlyphs2[i] = new _GLTglyph(); 1263 | } 1264 | 1265 | 1266 | int texWidth = 0; 1267 | int texHeight = 0; 1268 | 1269 | int drawableGlyphCount = 0; 1270 | 1271 | _GLTglyphdata[] glyphsData = new _GLTglyphdata[_gltFontGlyphCount]; 1272 | for (int i = 0; i < _gltFontGlyphCount; i ++) { 1273 | glyphsData[i] = new _GLTglyphdata(); 1274 | } 1275 | 1276 | long glyphPacked; 1277 | int glyphMarginPacked; 1278 | 1279 | int glyphX, glyphY, glyphWidth, glyphHeight; 1280 | int glyphMarginLeft, glyphMarginTop, glyphMarginRight, glyphMarginBottom; 1281 | 1282 | int glyphDataWidth, glyphDataHeight; 1283 | 1284 | glyphMarginLeft = 0; 1285 | glyphMarginRight = 0; 1286 | 1287 | _GLTglyph glyph; 1288 | _GLTglyphdata glyphData; 1289 | 1290 | char c; 1291 | int i; 1292 | int x, y; 1293 | 1294 | for (i = 0; i < _gltFontGlyphCount; i++) { 1295 | c = _gltFontGlyphCharacters.charAt(i); 1296 | 1297 | glyphPacked = _gltFontGlyphRects[i]; 1298 | 1299 | glyphX = (int) ((glyphPacked >>> (8 * 0)) & 0xFFFF); 1300 | glyphWidth = (int) ((glyphPacked >>> (8 * 2)) & 0xFF); 1301 | 1302 | glyphY = 0; 1303 | glyphHeight = _gltFontGlyphHeight; 1304 | 1305 | glyphMarginPacked = (int) ((glyphPacked >>> (8 * 3)) & 0xFFFF); 1306 | 1307 | glyphMarginTop = (glyphMarginPacked >>> (0)) & 0xFF; 1308 | glyphMarginBottom = (glyphMarginPacked >>> (8)) & 0xFF; 1309 | 1310 | glyphDataWidth = glyphWidth; 1311 | glyphDataHeight = glyphHeight - (glyphMarginTop + glyphMarginBottom); 1312 | 1313 | glyph = _gltFontGlyphs[i]; 1314 | 1315 | glyph.c = c; 1316 | 1317 | glyph.x = glyphX; 1318 | glyph.y = glyphY; 1319 | glyph.w = glyphWidth; 1320 | glyph.h = glyphHeight; 1321 | 1322 | glyphData = glyphsData[i]; 1323 | 1324 | glyphData.x = glyphX; 1325 | glyphData.y = glyphY; 1326 | glyphData.w = glyphWidth; 1327 | glyphData.h = glyphHeight; 1328 | 1329 | glyphData.marginLeft = glyphMarginLeft; 1330 | glyphData.marginTop = glyphMarginTop; 1331 | glyphData.marginRight = glyphMarginRight; 1332 | glyphData.marginBottom = glyphMarginBottom; 1333 | 1334 | glyphData.dataWidth = glyphDataWidth; 1335 | glyphData.dataHeight = glyphDataHeight; 1336 | 1337 | glyph.drawable = false; 1338 | 1339 | if (glyphDataWidth > 0 && glyphDataHeight > 0) { 1340 | glyph.drawable = true; 1341 | } 1342 | 1343 | if (glyph.drawable) { 1344 | drawableGlyphCount++; 1345 | 1346 | texWidth += glyphWidth; 1347 | 1348 | if (texHeight < glyphHeight) { 1349 | texHeight = glyphHeight; 1350 | } 1351 | } 1352 | } 1353 | 1354 | final int textureGlyphPadding = 1; // amount of pixels added around the whole bitmap texture 1355 | final int textureGlyphSpacing = 1; // amount of pixels added between each glyph on the final bitmap texture 1356 | 1357 | texWidth += textureGlyphSpacing * (drawableGlyphCount - 1); 1358 | 1359 | texWidth += textureGlyphPadding * 2; 1360 | texHeight += textureGlyphPadding * 2; 1361 | 1362 | final int texAreaSize = texWidth * texHeight; 1363 | 1364 | final int texPixelComponents = 4; // R, G, B, A 1365 | byte[] texData = new byte[texAreaSize * texPixelComponents]; 1366 | 1367 | int texPixelIndex; 1368 | 1369 | for (texPixelIndex = 0; texPixelIndex < (texAreaSize * texPixelComponents); texPixelIndex++) { 1370 | texData[texPixelIndex] = 0; 1371 | } 1372 | 1373 | final int glyphDataTypeSizeBits = Long.SIZE; 1374 | 1375 | int data0Index = 0; 1376 | int data1Index = 0; 1377 | 1378 | int bit0Index = 0; 1379 | int bit1Index = 1; 1380 | 1381 | int r, g, b, a; 1382 | 1383 | float u1, v1; 1384 | float u2, v2; 1385 | 1386 | int texX = 0; 1387 | int texY = 0; 1388 | 1389 | texX += textureGlyphPadding; 1390 | 1391 | for (i = 0; i < _gltFontGlyphCount; i++) { 1392 | glyph = _gltFontGlyphs[i]; 1393 | glyphData = glyphsData[i]; 1394 | 1395 | if (!glyph.drawable) { 1396 | continue; 1397 | } 1398 | 1399 | c = glyph.c; 1400 | 1401 | glyphX = glyph.x; 1402 | glyphY = glyph.y; 1403 | glyphWidth = glyph.w; 1404 | glyphHeight = glyph.h; 1405 | 1406 | glyphMarginLeft = glyphData.marginLeft; 1407 | glyphMarginTop = glyphData.marginTop; 1408 | glyphMarginRight = glyphData.marginRight; 1409 | glyphMarginBottom = glyphData.marginBottom; 1410 | 1411 | glyphDataWidth = glyphData.dataWidth; 1412 | glyphDataHeight = glyphData.dataHeight; 1413 | 1414 | texY = textureGlyphPadding; 1415 | 1416 | u1 = (float) texX / (float) texWidth; 1417 | v1 = (float) texY / (float) texHeight; 1418 | 1419 | u2 = (float) glyphWidth / (float) texWidth; 1420 | v2 = (float) glyphHeight / (float) texHeight; 1421 | 1422 | glyph.u1 = u1; 1423 | glyph.v1 = v1; 1424 | 1425 | glyph.u2 = u1 + u2; 1426 | glyph.v2 = v1 + v2; 1427 | 1428 | texX += glyphMarginLeft; 1429 | texY += glyphMarginTop; 1430 | 1431 | for (y = 0; y < glyphDataHeight; y++) { 1432 | for (x = 0; x < glyphDataWidth; x++) { 1433 | long c0 = ((_gltFontGlyphData[data0Index] >>> bit0Index) & 1); 1434 | long c1 = ((_gltFontGlyphData[data1Index] >>> bit1Index) & 1); 1435 | 1436 | if (c0 == 0 && c1 == 0) { 1437 | r = 0; 1438 | g = 0; 1439 | b = 0; 1440 | a = 0; 1441 | } else if (c0 == 0 && c1 == 1) { 1442 | r = 255; 1443 | g = 255; 1444 | b = 255; 1445 | a = 255; 1446 | } else if (c0 == 1 && c1 == 0) { 1447 | r = 0; 1448 | g = 0; 1449 | b = 0; 1450 | a = 255; 1451 | } else { 1452 | throw new RuntimeException("Invalid glyph data"); 1453 | } 1454 | 1455 | texPixelIndex = ((texY + y) * texWidth * texPixelComponents + (texX + x) * texPixelComponents); 1456 | texData[texPixelIndex + 0] = (byte) r; 1457 | texData[texPixelIndex + 1] = (byte) g; 1458 | texData[texPixelIndex + 2] = (byte) b; 1459 | texData[texPixelIndex + 3] = (byte) a; 1460 | 1461 | bit0Index += _GLT_FONT_PIXEL_SIZE_BITS; 1462 | bit1Index += _GLT_FONT_PIXEL_SIZE_BITS; 1463 | 1464 | if (bit0Index >= glyphDataTypeSizeBits) { 1465 | bit0Index = bit0Index % glyphDataTypeSizeBits; 1466 | data0Index++; 1467 | } 1468 | 1469 | if (bit1Index >= glyphDataTypeSizeBits) { 1470 | bit1Index = bit1Index % glyphDataTypeSizeBits; 1471 | data1Index++; 1472 | } 1473 | } 1474 | } 1475 | 1476 | texX += glyphDataWidth; 1477 | texY += glyphDataHeight; 1478 | 1479 | texX += glyphMarginRight; 1480 | texX += textureGlyphSpacing; 1481 | } 1482 | 1483 | for (i = 0; i < _gltFontGlyphCount; i++) { 1484 | glyph = _gltFontGlyphs[i]; 1485 | 1486 | _gltFontGlyphs2[glyph.c - _gltFontGlyphMinChar] = glyph; 1487 | } 1488 | 1489 | _gltText2DFontTexture = glGenTextures(); 1490 | glBindTexture(GL_TEXTURE_2D, _gltText2DFontTexture); 1491 | 1492 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texWidth, texHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, ByteBuffer.allocateDirect(texData.length).put(texData).position(0)); 1493 | 1494 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 1495 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 1496 | 1497 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 1498 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 1499 | 1500 | glBindTexture(GL_TEXTURE_2D, 0); 1501 | 1502 | } 1503 | 1504 | 1505 | 1506 | 1507 | } 1508 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/render/Simple2DDraw.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.render; 2 | 3 | import static org.lwjgl.opengl.GL32.*; 4 | 5 | public class Simple2DDraw { 6 | 7 | private final int shaderProgram; 8 | private final int projectionUniformLocation; 9 | 10 | public Simple2DDraw() { 11 | 12 | // format: pos, color 13 | 14 | final int vertexShader = glCreateShader(GL_VERTEX_SHADER); 15 | glShaderSource(vertexShader, """ 16 | #version 150 core 17 | in vec2 position; 18 | in vec4 color; 19 | 20 | uniform mat4 projection; 21 | 22 | out vec4 fColor; 23 | 24 | void main() { 25 | gl_Position = projection * vec4(position, 0.0, 1.0); 26 | fColor = color; 27 | } 28 | """); 29 | glCompileShader(vertexShader); 30 | final int[] success = new int[1]; 31 | glGetShaderiv(vertexShader, GL_COMPILE_STATUS, success); 32 | if (success[0] == 0) { 33 | RuntimeException e = new RuntimeException("Failed to compile vertex shader: " + glGetShaderInfoLog(vertexShader)); 34 | glDeleteShader(vertexShader); 35 | throw e; 36 | } 37 | 38 | final int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); 39 | glShaderSource(fragmentShader, """ 40 | #version 150 core 41 | in vec4 fColor; 42 | 43 | out vec4 FragColor; 44 | 45 | void main() { 46 | if (fColor.a < 0.01) 47 | discard; 48 | FragColor = fColor; 49 | } 50 | """); 51 | glCompileShader(fragmentShader); 52 | glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, success); 53 | if (success[0] == 0) { 54 | RuntimeException e = new RuntimeException("Failed to compile fragment shader: " + glGetShaderInfoLog(fragmentShader)); 55 | glDeleteShader(vertexShader); 56 | glDeleteShader(fragmentShader); 57 | throw e; 58 | } 59 | 60 | shaderProgram = glCreateProgram(); 61 | glAttachShader(shaderProgram, vertexShader); 62 | glAttachShader(shaderProgram, fragmentShader); 63 | glLinkProgram(shaderProgram); 64 | glGetProgramiv(shaderProgram, GL_LINK_STATUS, success); 65 | if (success[0] == 0) { 66 | RuntimeException e = new RuntimeException("Failed to link shader program: " + glGetProgramInfoLog(shaderProgram)); 67 | glDeleteShader(vertexShader); 68 | glDeleteShader(fragmentShader); 69 | glDeleteProgram(shaderProgram); 70 | throw e; 71 | } 72 | glDeleteShader(vertexShader); 73 | glDeleteShader(fragmentShader); 74 | 75 | projectionUniformLocation = glGetUniformLocation(shaderProgram, "projection"); 76 | } 77 | 78 | public void viewport(int width, int height) { 79 | // update projection: top-left is (0, 0) 80 | glUseProgram(shaderProgram); 81 | glUniformMatrix4fv(projectionUniformLocation, false, new float[] { 82 | 2f / width, 0, 0, 0, 83 | 0, -2f / height, 0, 0, 84 | 0, 0, 1, 0, 85 | -1, 1, 0, 1 86 | }); 87 | glUseProgram(0); 88 | } 89 | 90 | public void destroy() { 91 | glDeleteProgram(shaderProgram); 92 | } 93 | 94 | public class BufferBuilder { 95 | 96 | private static final int INITIAL_SIZE = 1024; 97 | private float[] buffer = new float[INITIAL_SIZE]; 98 | private final int vbo, vao; 99 | private boolean destroyed = false; 100 | private boolean building = false; 101 | private int pos = 0; 102 | 103 | public BufferBuilder() { 104 | vbo = glGenBuffers(); 105 | vao = glGenVertexArrays(); 106 | glBindVertexArray(vao); 107 | glBindBuffer(GL_ARRAY_BUFFER, vbo); 108 | glVertexAttribPointer(0, 2, GL_FLOAT, false, 6 * Float.BYTES, 0); 109 | glEnableVertexAttribArray(0); 110 | glVertexAttribPointer(1, 4, GL_FLOAT, false, 6 * Float.BYTES, 2 * Float.BYTES); 111 | glEnableVertexAttribArray(1); 112 | glBindBuffer(GL_ARRAY_BUFFER, 0); 113 | glBindVertexArray(0); 114 | } 115 | 116 | private void ensureCapacity(int capacity) { 117 | if (buffer.length < capacity) { 118 | float[] newBuffer = new float[buffer.length * 2]; 119 | System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); 120 | buffer = newBuffer; 121 | } 122 | } 123 | 124 | public BufferBuilder begin() { 125 | if (building) throw new IllegalStateException("Already building"); 126 | building = true; 127 | pos = 0; 128 | return this; 129 | } 130 | 131 | public BufferBuilder vertex(float x, float y, float r, float g, float b, float a) { 132 | ensureCapacity(pos + 6); 133 | buffer[pos ++] = x; 134 | buffer[pos ++] = y; 135 | buffer[pos ++] = r; 136 | buffer[pos ++] = g; 137 | buffer[pos ++] = b; 138 | buffer[pos ++] = a; 139 | return this; 140 | } 141 | 142 | public BufferBuilder triangle(float x1, float y1, float x2, float y2, float x3, float y3, float r, float g, float b, float a) { 143 | ensureCapacity(pos + 18); 144 | this.vertex(x1, y1, r, g, b, a); 145 | this.vertex(x2, y2, r, g, b, a); 146 | this.vertex(x3, y3, r, g, b, a); 147 | return this; 148 | } 149 | 150 | public BufferBuilder rect(float x, float y, float width, float height, float r, float g, float b, float a) { 151 | ensureCapacity(pos + 24); 152 | this.triangle(x, y, x, y + height, x + width, y, r, g, b, a); 153 | this.triangle(x + width, y + height, x + width, y, x, y + height, r, g, b, a); 154 | return this; 155 | } 156 | 157 | public void end() { 158 | if (destroyed) throw new IllegalStateException("Already destroyed"); 159 | if (!building) throw new IllegalStateException("Not building"); 160 | building = false; 161 | glBindBuffer(GL_ARRAY_BUFFER, vbo); 162 | glBufferData(GL_ARRAY_BUFFER, buffer, GL_DYNAMIC_DRAW); 163 | glBindBuffer(GL_ARRAY_BUFFER, 0); 164 | } 165 | 166 | public void draw() { 167 | if (destroyed) throw new IllegalStateException("Already destroyed"); 168 | if (building) throw new IllegalStateException("Still building"); 169 | glUseProgram(shaderProgram); 170 | glBindVertexArray(vao); 171 | glDrawArrays(GL_TRIANGLES, 0, pos / 6); 172 | glBindVertexArray(0); 173 | glUseProgram(0); 174 | } 175 | 176 | public void destroy() { 177 | glDeleteBuffers(vbo); 178 | glDeleteVertexArrays(vao); 179 | destroyed = true; 180 | } 181 | 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/util/AppLoaderUtil.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.util; 2 | 3 | import net.fabricmc.loader.api.FabricLoader; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.lang.reflect.Method; 9 | 10 | public class AppLoaderUtil { 11 | 12 | 13 | public static void init() { 14 | try { 15 | // final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(FabricLoader.class, MethodHandles.lookup()); 16 | // lookup.defineClass(getClassFile("com.ishland.earlyloadingscreen.platform_cl.AppLoaderAccessSupport")); 17 | // lookup.defineClass(getClassFile("com.ishland.earlyloadingscreen.platform_cl.AppLoaderAccessSupport$LoadingScreenAccessor")); 18 | // lookup.defineClass(getClassFile("com.ishland.earlyloadingscreen.platform_cl.AppLoaderAccessSupport$ProgressHolderAccessor")); 19 | 20 | defineClass("com.ishland.earlyloadingscreen.platform_cl.AppLoaderAccessSupport"); 21 | defineClass("com.ishland.earlyloadingscreen.platform_cl.AppLoaderAccessSupport$LoadingScreenAccessor"); 22 | defineClass("com.ishland.earlyloadingscreen.platform_cl.AppLoaderAccessSupport$ProgressHolderAccessor"); 23 | } catch (Throwable t) { 24 | throw new RuntimeException("Failed to init AppLoaderUtil", t); 25 | } 26 | } 27 | 28 | private static void defineClass(String name) throws IllegalAccessException, InvocationTargetException, IOException, NoSuchMethodException { 29 | if (isAlreadyLoaded(name)) { 30 | return; 31 | } 32 | final Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); 33 | defineClass.setAccessible(true); 34 | defineClass.invoke( 35 | FabricLoader.class.getClassLoader(), 36 | name, 37 | getClassFile(name), 38 | 0, 39 | getClassFile(name).length 40 | ); 41 | } 42 | 43 | private static boolean isAlreadyLoaded(String name) { 44 | try { 45 | Class.forName(name, false, FabricLoader.class.getClassLoader()); 46 | return true; 47 | } catch (ClassNotFoundException e) { 48 | return false; 49 | } 50 | } 51 | 52 | private static byte[] getClassFile(String name) throws IOException { 53 | try (InputStream in = AppLoaderUtil.class.getClassLoader().getResourceAsStream(name.replace('.', '/') + ".class")) { 54 | return in.readAllBytes(); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/util/OSDetectionUtil.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.util; 2 | 3 | import io.netty.util.internal.PlatformDependent; 4 | 5 | public class OSDetectionUtil { 6 | 7 | public static OperatingSystem getOperatingSystem() { 8 | final String normalizedOs = PlatformDependent.normalizedOs(); 9 | return switch (normalizedOs) { 10 | case "linux" -> OperatingSystem.LINUX; 11 | case "osx" -> OperatingSystem.OSX; 12 | case "sunos" -> OperatingSystem.SOLARIS; 13 | case "windows" -> OperatingSystem.WINDOWS; 14 | default -> OperatingSystem.UNKNOWN; 15 | }; 16 | } 17 | 18 | public enum OperatingSystem { 19 | LINUX("linux"), 20 | SOLARIS("solaris"), 21 | WINDOWS("windows"), 22 | OSX("osx"), 23 | UNKNOWN("unknown"); 24 | 25 | public final String name; 26 | 27 | OperatingSystem(String name) { 28 | this.name = name; 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/util/ProgressUtil.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.util; 2 | 3 | import com.ishland.earlyloadingscreen.LoadingProgressManager; 4 | import com.ishland.earlyloadingscreen.LoadingScreenManager; 5 | 6 | import java.util.List; 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.concurrent.CompletionStage; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | 11 | public class ProgressUtil { 12 | 13 | public static void createProgress(List> futures, CompletionStage combined, String name) { 14 | final LoadingProgressManager.ProgressHolder holder = LoadingProgressManager.tryCreateProgressHolder(); 15 | if (holder != null) { 16 | AtomicInteger counter = new AtomicInteger(); 17 | int total = futures.size(); 18 | for (CompletionStage future : futures) { 19 | future.whenComplete((v, throwable) -> { 20 | final int i = counter.incrementAndGet(); 21 | holder.update(() -> String.format("Loading %s... (%d/%d)", name, i, total)); 22 | holder.updateProgress(() -> (float) i / (float) total); 23 | }); 24 | } 25 | combined.whenComplete((vs, throwable) -> holder.close()); 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/util/RemapUtil.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.util; 2 | 3 | import net.fabricmc.loader.api.FabricLoader; 4 | import org.objectweb.asm.Type; 5 | 6 | import java.util.Arrays; 7 | 8 | public class RemapUtil { 9 | 10 | public static final String INTERMEDIARY = "intermediary"; 11 | 12 | public static String remapMethodDescriptor(String desc) { 13 | final Type returnType = Type.getReturnType(desc); 14 | final Type[] argumentTypes = Type.getArgumentTypes(desc); 15 | return Type.getMethodDescriptor( 16 | Type.getType(remapFieldDescriptor(returnType.getDescriptor())), 17 | Arrays.stream(argumentTypes) 18 | .map(type -> Type.getType(remapFieldDescriptor(type.getDescriptor()))) 19 | .toArray(Type[]::new) 20 | ); 21 | } 22 | 23 | public static String remapFieldDescriptor(String desc) { 24 | final Type type = Type.getType(desc); 25 | if (type.getSort() == Type.ARRAY) { // remap arrays 26 | return "[".repeat(type.getDimensions()) + remapFieldDescriptor(type.getElementType().getDescriptor()); 27 | } 28 | if (type.getSort() != Type.OBJECT) { // no need to remap primitives 29 | return desc; 30 | } 31 | final String unmappedClassDesc = type.getClassName(); 32 | final String unmappedClass; 33 | if (unmappedClassDesc.endsWith(";") && unmappedClassDesc.startsWith("L")) { 34 | unmappedClass = unmappedClassDesc.substring(1, unmappedClassDesc.length() - 1); // trim starting "L" and ending ";" 35 | } else { 36 | unmappedClass = unmappedClassDesc; 37 | } 38 | return 'L' + FabricLoader.getInstance().getMappingResolver().mapClassName(INTERMEDIARY, unmappedClass.replace('/', '.')).replace('.', '/') + ";"; 39 | } 40 | 41 | private RemapUtil() { 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/earlyloadingscreen/util/WindowCreationUtil.java: -------------------------------------------------------------------------------- 1 | package com.ishland.earlyloadingscreen.util; 2 | 3 | import com.ishland.earlyloadingscreen.LoadingProgressManager; 4 | import com.ishland.earlyloadingscreen.SharedConstants; 5 | import com.ishland.earlyloadingscreen.patch.SodiumOSDetectionPatch; 6 | import net.fabricmc.loader.api.FabricLoader; 7 | import org.lwjgl.glfw.GLFW; 8 | 9 | import java.util.concurrent.atomic.AtomicBoolean; 10 | 11 | public class WindowCreationUtil { 12 | 13 | public static long warpGlfwCreateWindow(int width, int height, CharSequence title, long monitor, long share) { 14 | FabricLoader.getInstance().invokeEntrypoints("els_before_window_creation", Runnable.class, Runnable::run); 15 | sodiumHookInit(); 16 | sodiumHook(false); 17 | try { 18 | return GLFW.glfwCreateWindow(width, height, title, monitor, share); 19 | } finally { 20 | sodiumHook(true); 21 | FabricLoader.getInstance().invokeEntrypoints("els_after_window_creation", Runnable.class, Runnable::run); 22 | } 23 | } 24 | 25 | private static AtomicBoolean ranSodiumHookInit = new AtomicBoolean(false); 26 | private static boolean foundSodium = true; 27 | 28 | private static void sodiumHookInit() { 29 | if (Boolean.getBoolean("earlyloadingscreen.duringEarlyLaunch") && !SodiumOSDetectionPatch.INITIALIZED) { 30 | final String msg = "SodiumOSDetectionPatch unavailable, sodium workarounds may not work properly"; 31 | SharedConstants.LOGGER.warn(msg); 32 | LoadingProgressManager.showMessageAsProgress(msg); 33 | return; 34 | } 35 | if (ranSodiumHookInit.compareAndSet(false, true)) { 36 | if (!FabricLoader.getInstance().isModLoaded("sodium")) { 37 | foundSodium = false; 38 | SharedConstants.LOGGER.info("Sodium not found, skipping sodium hook init"); 39 | return; 40 | } 41 | final Class graphicsAdapterProbeClazz; 42 | final Class workaroundsClazz; 43 | try { 44 | graphicsAdapterProbeClazz = locateClass("me.jellysquid.mods.sodium.client.util.workarounds.probe.GraphicsAdapterProbe", "me.jellysquid.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterProbe"); 45 | workaroundsClazz = locateClass("me.jellysquid.mods.sodium.client.util.workarounds.Workarounds", "me.jellysquid.mods.sodium.client.compatibility.workarounds.Workarounds"); 46 | } catch (Throwable t) { 47 | final String msg = "Failed to find Sodium workarounds, skipping sodium hook init"; 48 | if (FabricLoader.getInstance().isDevelopmentEnvironment() || Boolean.getBoolean("els.debug")) { 49 | SharedConstants.LOGGER.warn(msg, t); 50 | } else { 51 | SharedConstants.LOGGER.warn(msg); 52 | } 53 | LoadingProgressManager.showMessageAsProgress(msg); 54 | foundSodium = false; 55 | return; 56 | } 57 | try { 58 | graphicsAdapterProbeClazz.getMethod("findAdapters").invoke(null); 59 | workaroundsClazz.getMethod("init").invoke(null); 60 | // final Collection adapters = (Collection) graphicsAdapterProbeClazz.getMethod("getAdapters").invoke(null); 61 | // boolean foundNvidia = false; 62 | // for (Object adapter : adapters) { 63 | // final Enum vendor = (Enum) graphicsAdapterInfoClazz.getMethod("vendor").invoke(adapter); 64 | // if (vendor == Enum.valueOf(graphicsAdapterVendorClazz, "NVIDIA")) { 65 | // foundNvidia = true; 66 | // break; 67 | // } 68 | // } 69 | // if (foundNvidia && (PlatformDependent.isWindows() || PlatformDependent.normalizedOs().equals("linux"))) { 70 | // final Field activeWorkarounds = workaroundsClazz.getDeclaredField("ACTIVE_WORKAROUNDS"); 71 | // activeWorkarounds.setAccessible(true); 72 | // final Enum NVIDIA_THREADED_OPTIMIZATIONS = Enum.valueOf(workaroundsReferenceClazz, "NVIDIA_THREADED_OPTIMIZATIONS"); 73 | // ((AtomicReference>) activeWorkarounds.get(null)).set(Set.of(NVIDIA_THREADED_OPTIMIZATIONS)); 74 | // } 75 | } catch (Throwable t) { 76 | final String msg = "Failed to init Sodium workarounds, skipping sodium hook"; 77 | if (FabricLoader.getInstance().isDevelopmentEnvironment() || Boolean.getBoolean("els.debug")) { 78 | SharedConstants.LOGGER.warn(msg, t); 79 | } else { 80 | SharedConstants.LOGGER.warn(msg); 81 | } 82 | LoadingProgressManager.showMessageAsProgress(msg); 83 | foundSodium = false; 84 | return; 85 | } 86 | } 87 | } 88 | 89 | private static void sodiumHook(boolean after) { 90 | if (!foundSodium) return; 91 | final Class workaroundsClazz; 92 | final Class workaroundsReferenceClazz; 93 | final Class nvidiaWorkaroundsClazz; 94 | try { 95 | workaroundsClazz = locateClass("me.jellysquid.mods.sodium.client.util.workarounds.Workarounds", "me.jellysquid.mods.sodium.client.compatibility.workarounds.Workarounds"); 96 | workaroundsReferenceClazz = (Class>) locateClass("me.jellysquid.mods.sodium.client.util.workarounds.Workarounds$Reference", "me.jellysquid.mods.sodium.client.compatibility.workarounds.Workarounds$Reference"); 97 | nvidiaWorkaroundsClazz = locateClass("me.jellysquid.mods.sodium.client.util.workarounds.driver.nvidia.NvidiaWorkarounds", "me.jellysquid.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds"); 98 | } catch (Throwable e) { 99 | final String msg = "Failed to find Sodium workarounds, skipping sodium hook"; 100 | if (FabricLoader.getInstance().isDevelopmentEnvironment() || Boolean.getBoolean("els.debug")) { 101 | SharedConstants.LOGGER.warn(msg, e); 102 | } else { 103 | SharedConstants.LOGGER.warn(msg); 104 | } 105 | LoadingProgressManager.showMessageAsProgress(msg); 106 | foundSodium = false; 107 | return; 108 | } 109 | try { 110 | final Enum NVIDIA_THREADED_OPTIMIZATIONS = Enum.valueOf(workaroundsReferenceClazz, "NVIDIA_THREADED_OPTIMIZATIONS"); 111 | if ((boolean) workaroundsClazz.getMethod("isWorkaroundEnabled", workaroundsReferenceClazz).invoke(null, NVIDIA_THREADED_OPTIMIZATIONS)) { 112 | if (!after) { 113 | nvidiaWorkaroundsClazz.getMethod("install").invoke(null); 114 | LoadingProgressManager.showMessageAsProgress("Installed Nvidia workarounds from sodium", 5000L); 115 | } else { 116 | nvidiaWorkaroundsClazz.getMethod("uninstall").invoke(null); 117 | } 118 | } 119 | } catch (Throwable t) { 120 | t.printStackTrace(); 121 | } 122 | } 123 | 124 | private static Class locateClass(String... names) { 125 | for (String name : names) { 126 | try { 127 | return Class.forName(name); 128 | } catch (ClassNotFoundException ignored) { 129 | } 130 | } 131 | throw new RuntimeException("Failed to locate any of " + String.join(", ", names)); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/main/resources/assets/earlyloadingscreen/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishland/EarlyLoadingScreen/a54691da09c0489ddb49fe201f4f685fae72b549/src/main/resources/assets/earlyloadingscreen/icon.png -------------------------------------------------------------------------------- /src/main/resources/earlyloadingscreen.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v1 named 2 | 3 | accessible class net/minecraft/client/texture/TextureStitcher$Holder 4 | 5 | extendable class net/minecraft/client/texture/TextureStitcher$Holder 6 | -------------------------------------------------------------------------------- /src/main/resources/earlyloadingscreen.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "package": "com.ishland.earlyloadingscreen.mixin", 4 | "compatibilityLevel": "JAVA_17", 5 | "plugin": "com.ishland.earlyloadingscreen.TheMixinConfig", 6 | "mixins": [ 7 | "progress.MixinAtlasLoader", 8 | "MixinWindow", 9 | "access.ISimpleResourceReload", 10 | "progress.MixinBakedModelManager", 11 | "MixinMinecraftClient", 12 | "progress.MixinModelLoader", 13 | "MixinSplashOverlay", 14 | "progress.MixinSpriteLoader", 15 | "progress.MixinTextureManager", 16 | "progress.MixinTextureStitcher", 17 | "access.ITextureStitcherHolder" 18 | ], 19 | "injectors": { 20 | "defaultRequire": 1, 21 | "injectionPoints": [ 22 | "com.ishland.earlyloadingscreen.MixinEarlyLaunch" 23 | ] 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "earlyloadingscreen", 4 | "version": "${version}", 5 | "name": "Early Loading Screen", 6 | "description": "A mod that shows a loading screen early when the game loads.", 7 | "authors": [ 8 | "ishland" 9 | ], 10 | "contact": { 11 | "homepage": "https://modrinth.com/mod/early-loading-screen/", 12 | "sources": "https://github.com/ishland/EarlyLoadingScreen", 13 | "issues": "https://github.com/ishland/EarlyLoadingScreen/issues" 14 | }, 15 | "license": "MIT", 16 | "icon": "assets/earlyloadingscreen/icon.png", 17 | "environment": "client", 18 | "entrypoints": { 19 | "main": [ 20 | "com.ishland.earlyloadingscreen.TheMod" 21 | ], 22 | "preLaunch": [ 23 | "com.ishland.earlyloadingscreen.PreLaunchHandler" 24 | ] 25 | }, 26 | "accessWidener": "earlyloadingscreen.accesswidener", 27 | "mixins": [ 28 | "earlyloadingscreen.mixins.json" 29 | ], 30 | "languageAdapters": { 31 | "earlyloadingscreen:languageAdapterEarlyLaunch": "com.ishland.earlyloadingscreen.LanguageAdapterLaunch" 32 | }, 33 | "depends": { 34 | "fabricloader": ">=0.14.23", 35 | "minecraft": ">=1.20.2", 36 | "java": ">=17" 37 | }, 38 | "custom": { 39 | "sodium:options": { 40 | "mixin.workarounds.context_creation": false 41 | } 42 | } 43 | } 44 | --------------------------------------------------------------------------------