diff --git a/.gitattributes b/.gitattributes index e69de29b..00a51aff 100644 --- a/.gitattributes +++ b/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/.github/ISSUE_TEMPLATE/LAUNCH_ISSUE.yaml b/.github/ISSUE_TEMPLATE/LAUNCH_ISSUE.yaml new file mode 100644 index 00000000..27dc5383 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/LAUNCH_ISSUE.yaml @@ -0,0 +1,46 @@ +name: Launching Issue +description: Create an issue about your game failing to load/cache +title: "[LAUNCH]: " +labels: ["type: bug", "status: idle"] +body: + - type: markdown + attributes: + value: | + Thank you for reporting an issue about DashLoader, we care a lot about our mod and enjoy fixing every bug. + - type: input + id: version + attributes: + label: Version + description: What version of DashLoader are you running? + placeholder: 5.0.0-alpha.3 + validations: + required: true + - type: input + id: mc-version + attributes: + label: Minecraft Version + description: What Minecraft version are you using? + placeholder: 1.19.3 + validations: + required: true + - type: markdown + attributes: + value: | + Please provide **THE ENTIRE LOG** as the crashlogs don't contain much information about DashLoader. + Use a website like https://mclo.gs/ to upload logs. + Preferably we want a log for when you create the cache (The popup at the top left is present) and another log for when DashLoader loads the cache. + - type: input + id: logs + attributes: + label: Entire Logs + description: Link to the logs. + placeholder: https://mclo.gs/5K0ChKa + validations: + required: true + - type: textarea + id: extra + attributes: + label: Additional Notes + description: Anything else you want to add? + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 50d0ca35..3dc2d914 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,8 +1,8 @@ --- name: Bug report -about: Create a report for a DashLoader issue -title: "[ISSUE]: " -labels: bug +about: Create a report to help us improve +title: '' +labels: '' assignees: '' --- @@ -12,10 +12,10 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: -1. Add '...' mods -2. Launch the game -3. Join '....' world -4. Press '....' stuff +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error **Expected behavior** A clear and concise description of what you expected to happen. @@ -23,13 +23,9 @@ A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. -**Info (please complete the following information):** - - DashLoader: [e.g. 3.0-pr1] - - Minecraft: [e.g. 1.17.1, 1.18] - - Mods [e.g. Fabric API, Better End] - -**Full crash log (if relevant)** -https://pastebin.com/ ... +**Context (please complete the following information):** + - DashLoader Version [e.g. 3.0-rc14] + - Minecraft Version [e.g. 1.18.1] **Additional context** Add any other context about the problem here. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..0543f4ce --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: Build + +on: + push: + branches: [ "main", "copilot/**" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Java 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build with Gradle + run: ./gradlew build --no-daemon + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + if: success() + with: + name: build-artifacts + path: build/libs/*.jar diff --git a/.gitignore b/.gitignore index c733d8ff..78ca1597 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,5 @@ bin/ # Ignore Gradle build output directory build -/run/ +run/ +upload.sh diff --git a/README.md b/README.md index 31e1a03d..7d9b46b2 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,50 @@ -# DashLoader-Definition -Launch Minecraft at the speed of light. - -All of the source code is present in version dependant branches. - -For the core framework go to https://github.com/QuantumFusionMC/DashLoader-Core - -![image](https://user-images.githubusercontent.com/24830855/150510945-16b1f45e-3296-41f5-a7cc-4f91c9b4debe.png) -DashLoader's performance is highly aided by the [YourKit Java Profiler](https://www.curseforge.com/linkout?remoteUrl=https%253a%252f%252fwww.yourkit.com%252fjava%252fprofiler%252f) which helps us greatly with keeping standards high and load times low. +# DashLoader Github +Welcome to the codebase where DashLoader lives! Please report any issues you find with DashLoader here. +
+

+ + Description +
+ This mod accelerates the Minecraft Asset Loading system by caching all of its content, This leads to a much faster + game load. + It does this by caching all of its content on first launch and on next launch loading back that exact cache. + The cache loading is hyper fast and scalable which utilises your entire system. +


+ Important notes: + +

• The first time your launch DashLoader it will be significantly slower. + Because it needs to create a cache which contains all the assets minecraft normally loads. + This will also happen every time you change a mod/resourcepack if that configuration does not have an existing + cache. + +

• DashLoader has been known to be incompatible with a lot of mods. + DashLoader 3.0 has massively improved compatibility by not forcing mod developers to add explicit support to make + their assets cachable. + This means that DashLoader will load assets normally for mod assets that cannot be cached. + While this improves mod compatibility it hurts speed as the minecraft loading system is quite slow. + +

• If you use DashLoader for Developing mods or creating resource packs you can press + f3 + t to recreate the cache to load your new assets in. + If you want to just show off the speed of DashLoader you can press shift + f3 + t +


+ + Community +
+ Discord + + Sponsors +
+ YourKit + Makes amazing profilers for both Java and .NET. + We use their Java Profiler to understand where to optimize further and make DashLoader faster. +
+ JetBrains + Creates excellent IDEs for all programmers and have provided us with access to their enterprise products for use to + develop DashLoader and Hyphen. + + Donate +
+ I have a Ko-Fi page if you would like to Support me.
+ Please only support me if you like what I do, and you are not in a bad financial situation to do so. +

+
diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..c0d7cb40 --- /dev/null +++ b/build.gradle @@ -0,0 +1,184 @@ +plugins { + // Publishing + id 'com.matthewprenger.cursegradle' version '1.4.0' apply false + id "com.modrinth.minotaur" version "2.8.10" apply false + + id 'fabric-loom' version "${loom_version}" + id 'maven-publish' +} + +def enablePublishing = providers.gradleProperty("enablePublishing") + .map { it.toBoolean() } + .getOrElse(false) + +if (enablePublishing) { + apply plugin: "com.modrinth.minotaur" + apply plugin: "com.matthewprenger.cursegradle" +} + +base { + archivesName = project.archives_base_name +} +version = project.mod_version +group = project.maven_group + +repositories { + mavenCentral() + mavenLocal() + maven { + url 'https://jitpack.io' + } + maven { + url "https://notalpha.dev/maven/releases" + } + maven { + name = "Terraformers" + url = "https://maven.terraformersmc.com/" + } + maven { + name = "Nucleoid" + url = "https://maven.nucleoid.xyz/" + } +} + +loom { + accessWidenerPath = file("src/main/resources/dashloader.accesswidener") + log4jConfigs.from(file("log4j-dev.xml")) +} + +dependencies { + // To change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" + + implementation "dev.notalpha:Hyphen:0.4.0-rc.5" + include "dev.notalpha:Hyphen:0.4.0-rc.5" + + implementation "dev.notalpha:Taski:2.1.0" + include "dev.notalpha:Taski:2.1.0" + + implementation 'com.github.luben:zstd-jni:1.5.7-1' + include 'com.github.luben:zstd-jni:1.5.7-1' + + modCompileOnly fabricApi.module("fabric-renderer-indigo", project.fabric_version) + + // For Modmenu + modRuntimeOnly fabricApi.module("fabric-api-base", project.fabric_version) + modRuntimeOnly fabricApi.module("fabric-key-binding-api-v1", project.fabric_version) + modRuntimeOnly fabricApi.module("fabric-lifecycle-events-v1", project.fabric_version) + modRuntimeOnly fabricApi.module("fabric-resource-loader-v0", project.fabric_version) + modRuntimeOnly fabricApi.module("fabric-screen-api-v1", project.fabric_version) +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} + +processResources { + inputs.property "version", project.version + + filesMatching("fabric.mod.json") { + expand "version": project.version + } +} + +tasks.withType(JavaCompile).configureEach { + // ensure that the encoding is set to UTF-8, no matter what the system default is + // this fixes some edge cases with special characters not displaying correctly + // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html + // If Javadoc is generated, this must be specified in that task too. + it.options.encoding = "UTF-8" + + // Minecraft 1.17 (21w19a) upwards uses Java 16. + it.options.release = 21 +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + //include sources in maven publish + withSourcesJar() +} + +jar { + from("LICENSE") { + rename { "${it}_${project.base.archivesName}" } + rename { "${it}_${project.base.archivesName}" } + } +} + +// Publishing +if (enablePublishing) { + modrinth { + token = project.hasProperty("modrinthApiKey") ? project.modrinthApiKey : "" + projectId = 'ZfQ3kTvR' + changelog = file("changelog.md").getText() + versionNumber = project.version + versionName = "$project.version".split("\\+")[0] + " for $project.minecraft_version" + uploadFile = remapJar + versionType = "beta" + gameVersions = ['1.21.10'] + loaders = ['fabric', 'quilt'] + } + + curseforge { + apiKey = project.hasProperty("curseForgeApiKey") ? project.curseForgeApiKey : "" + project { + id = '472772' + changelogType = "markdown" + changelog = file("changelog.md") + releaseType = 'beta' + + addGameVersion "1.21.10" + addGameVersion "Fabric" + addGameVersion "Quilt" + addGameVersion "Java 21" + + mainArtifact(remapJar) { + displayName = "$project.version".split("\\+")[0] + " for $project.minecraft_version" + } + } + options { + forgeGradleIntegration = false + } + } + + tasks.register("publishMod") { + dependsOn 'modrinth' + dependsOn 'curseforge' + } +} else { + tasks.register("publishMod") { + doLast { + logger.lifecycle("Publishing disabled. Re-run with -PenablePublishing=true to publish.") + } + } +} + +tasks.register("getVersion") { + print("$project.version") +} + +publishing { + repositories { + maven { + name = "notalpha" + url = "https://notalpha.dev/maven/releases" + credentials(PasswordCredentials) + authentication { + basic(BasicAuthentication) + } + } + } + publications { + maven(MavenPublication) { + from components.java + } + } +} diff --git a/cfr.jar b/cfr.jar new file mode 100644 index 00000000..7f6ddc4c Binary files /dev/null and b/cfr.jar differ diff --git a/changelog.md b/changelog.md new file mode 100644 index 00000000..71aff189 --- /dev/null +++ b/changelog.md @@ -0,0 +1,15 @@ +# Fixes + +- Cache reading logic +- Vulkan mod compatibility +- Transparent textures rendering as opaque with sodium with feature `CacheSpriteContents` +- Possible memory leak in atlas caching module +- Ignore sprites with duplicate IDs + +# Features + +- Tweaked config screen + +# Internal + +- Removed `Sonatype Snapshots` maven diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..df091068 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,18 @@ +# Hello this is Froge. I like QuantumFusion and anyone who uses DashLoader. :heart: from !alpha, Froge and the QuantumFusion team. +# Current Minecraft Properties +org.gradle.jvmargs=-Xmx2560m + +minecraft_version=1.21.10 +# Using Mojang official mappings (loom.officialMojangMappings()) instead of YARN +loader_version=0.17.3 +loom_version=1.15-SNAPSHOT + +fabric_version=0.138.3+1.21.10 +# Dependencies +# ModMenu 16.0.0-rc.1 targets 1.21.10 (17.0.0-alpha.1 also supports 1.21.10 and 1.21.11) +modmenu_version=16.0.0-rc.1 + +# Mod Properties +mod_version=5.1.0-beta.8+1.21.10 +maven_group=dev.notalpha +archives_base_name=dashloader diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 00000000..7454180f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..5dc98dbc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..744e882e --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/inspect.java b/inspect.java new file mode 100644 index 00000000..303ecfe8 --- /dev/null +++ b/inspect.java @@ -0,0 +1,31 @@ +import java.lang.reflect.*; +public class Inspect { + public static void main(String[] args) throws Exception { + if (args.length == 0) { + System.err.println("usage: Inspect "); + return; + } + Class c = Class.forName(args[0]); + System.out.println("CLASS " + c.getName()); + System.out.println("FIELDS:"); + for (Field f : c.getDeclaredFields()) { + System.out.println(" " + Modifier.toString(f.getModifiers()) + " " + f.getType().getTypeName() + " " + f.getName()); + } + System.out.println("CTORS:"); + for (Constructor k : c.getDeclaredConstructors()) { + System.out.println(" " + Modifier.toString(k.getModifiers()) + " " + c.getSimpleName() + "(" + params(k.getParameterTypes()) + ")"); + } + System.out.println("METHODS:"); + for (Method m : c.getDeclaredMethods()) { + System.out.println(" " + Modifier.toString(m.getModifiers()) + " " + m.getReturnType().getTypeName() + " " + m.getName() + "(" + params(m.getParameterTypes()) + ")"); + } + } + private static String params(Class[] ps) { + StringBuilder sb = new StringBuilder(); + for (int i=0;i0) sb.append(", "); + sb.append(ps[i].getTypeName()); + } + return sb.toString(); + } +} diff --git a/log4j-dev.xml b/log4j-dev.xml new file mode 100644 index 00000000..f198f04f --- /dev/null +++ b/log4j-dev.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..8a88f8db --- /dev/null +++ b/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + maven { + name "Fabric" + url "https://maven.fabricmc.net" + } + } +} + +rootProject.name = 'dashloader' diff --git a/src/main/java/dev/notalpha/dashloader/CacheFactoryImpl.java b/src/main/java/dev/notalpha/dashloader/CacheFactoryImpl.java new file mode 100644 index 00000000..2b0cd37e --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/CacheFactoryImpl.java @@ -0,0 +1,76 @@ +package dev.notalpha.dashloader; + +import dev.notalpha.dashloader.api.DashModule; +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.cache.Cache; +import dev.notalpha.dashloader.api.cache.CacheFactory; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.registry.MissingHandler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.BiFunction; + +public class CacheFactoryImpl implements CacheFactory { + private static final Logger LOGGER = LogManager.getLogger("CacheFactory"); + private final List> dashObjects; + private final List> modules; + private final List> missingHandlers; + private boolean failed = false; + + public CacheFactoryImpl() { + this.dashObjects = new ArrayList<>(); + this.modules = new ArrayList<>(); + this.missingHandlers = new ArrayList<>(); + } + + @Override + public void addDashObject(Class> dashClass) { + final Class[] interfaces = dashClass.getInterfaces(); + if (interfaces.length == 0) { + LOGGER.error("No DashObject interface found. Class: {}", dashClass.getSimpleName()); + this.failed = true; + return; + } + this.dashObjects.add(new DashObjectClass<>(dashClass)); + } + + @Override + public void addModule(DashModule module) { + this.modules.add(module); + } + + @Override + public void addMissingHandler(Class rClass, BiFunction> func) { + this.missingHandlers.add(new MissingHandler<>(rClass, func)); + } + + @Override + public Cache build(Path cacheDir) { + if (this.failed) { + throw new RuntimeException("Failed to initialize the API"); + } + + // Set dashobject ids + this.dashObjects.sort(Comparator.comparing(o -> o.getDashClass().getName())); + this.modules.sort(Comparator.comparing(o -> o.getDataClass().getName())); + + int id = 0; + Class lastClass = null; + for (DashObjectClass dashObject : this.dashObjects) { + if (dashObject.getDashClass() == lastClass) { + DashLoader.LOG.warn("Duplicate DashObject found: {}", dashObject.getDashClass()); + continue; + } + lastClass = dashObject.getDashClass(); + dashObject.dashObjectId = id; + id += 1; + } + + return new CacheImpl(cacheDir.resolve(DashLoader.MOD_HASH + "/"), modules, dashObjects, this.missingHandlers); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/CacheImpl.java b/src/main/java/dev/notalpha/dashloader/CacheImpl.java new file mode 100644 index 00000000..96e0acf9 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/CacheImpl.java @@ -0,0 +1,221 @@ +package dev.notalpha.dashloader; + +import dev.notalpha.dashloader.api.DashModule; +import dev.notalpha.dashloader.api.cache.Cache; +import dev.notalpha.dashloader.api.cache.CacheStatus; +import dev.notalpha.dashloader.config.ConfigHandler; +import dev.notalpha.dashloader.io.MappingSerializer; +import dev.notalpha.dashloader.io.RegistrySerializer; +import dev.notalpha.dashloader.io.data.CacheInfo; +import dev.notalpha.dashloader.misc.ProfilerUtil; +import dev.notalpha.dashloader.registry.MissingHandler; +import dev.notalpha.dashloader.registry.RegistryReaderImpl; +import dev.notalpha.dashloader.registry.RegistryWriterImpl; +import dev.notalpha.dashloader.registry.data.StageData; +import dev.notalpha.taski.builtin.StepTask; +import org.apache.commons.io.FileUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; + +public final class CacheImpl implements Cache { + private static final String METADATA_FILE_NAME = "metadata.bin"; + private final Path cacheDir; + // DashLoader metadata + private final List> cacheHandlers; + private final List> dashObjects; + private final List> missingHandlers; + // Serializers + private final RegistrySerializer registrySerializer; + private final MappingSerializer mappingsSerializer; + private CacheStatus status; + private String hash; + + CacheImpl(Path cacheDir, List> cacheHandlers, List> dashObjects, List> missingHandlers) { + this.cacheDir = cacheDir; + this.cacheHandlers = cacheHandlers; + this.dashObjects = dashObjects; + this.missingHandlers = missingHandlers; + this.registrySerializer = new RegistrySerializer(dashObjects); + this.mappingsSerializer = new MappingSerializer(cacheHandlers); + } + + public void load(String name) { + this.hash = name; + + if (this.exists()) { + this.setStatus(CacheStatus.LOAD); + this.loadCache(); + } else { + this.setStatus(CacheStatus.SAVE); + } + } + + public boolean save(@Nullable Consumer taskConsumer) { + if (status != CacheStatus.SAVE) { + throw new RuntimeException("Status is not SAVE"); + } + DashLoader.LOG.info("Starting DashLoader Caching"); + try { + + Path ourDir = getDir(); + + // Max caches + int maxCaches = ConfigHandler.INSTANCE.config.maxCaches; + if (maxCaches != -1) { + DashLoader.LOG.info("Checking for cache count."); + try { + FileTime oldestTime = null; + Path oldestPath = null; + int cacheCount = 1; + try (Stream stream = Files.list(cacheDir)) { + for (Path path : stream.toList()) { + if (!Files.isDirectory(path)) { + continue; + } + + if (path.equals(ourDir)) { + continue; + } + cacheCount += 1; + + try { + BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); + FileTime lastAccessTime = attrs.lastAccessTime(); + if (oldestTime == null || lastAccessTime.compareTo(oldestTime) < 0) { + oldestTime = lastAccessTime; + oldestPath = path; + } + } catch (IOException e) { + DashLoader.LOG.warn("Could not find access time for cache.", e); + } + } + } + + if (oldestPath != null && cacheCount > maxCaches) { + DashLoader.LOG.info("Removing {} as we are currently above the maximum caches.", oldestPath); + if (!FileUtils.deleteQuietly(oldestPath.toFile())) { + DashLoader.LOG.error("Could not remove cache {}", oldestPath); + } + } + } catch (NoSuchFileException ignored) { + } catch (IOException io) { + DashLoader.LOG.error("Could not enforce maximum cache ", io); + } + } + + long start = System.currentTimeMillis(); + + StepTask main = new StepTask("save", 2); + if (taskConsumer != null) { + taskConsumer.accept(main); + } + + RegistryWriterImpl factory = RegistryWriterImpl.create(missingHandlers, dashObjects); + + // Mappings + mappingsSerializer.save(ourDir, factory, cacheHandlers, main); + main.next(); + + // serialization + main.run(new StepTask("serialize", 2), (task) -> { + try { + CacheInfo info = this.registrySerializer.serialize(ourDir, factory, task::setSubTask); + task.next(); + DashLoader.METADATA_SERIALIZER.save(ourDir.resolve(METADATA_FILE_NAME), new StepTask("hi"), info); + } catch (IOException e) { + throw new RuntimeException(e); + } + task.next(); + }); + + DashLoader.LOG.info("Saved cache in {}", ProfilerUtil.getTimeStringFromStart(start)); + return true; + } catch (Throwable thr) { + DashLoader.LOG.error("Failed caching", thr); + this.setStatus(CacheStatus.SAVE); + this.remove(); + return false; + } + } + + private void loadCache() { + if (status != CacheStatus.LOAD) { + throw new RuntimeException("Status is not LOAD"); + } + + long start = System.currentTimeMillis(); + try { + StepTask task = new StepTask("Loading DashCache", 3); + Path cacheDir = getDir(); + + // Get metadata + Path metadataPath = cacheDir.resolve(METADATA_FILE_NAME); + CacheInfo info = DashLoader.METADATA_SERIALIZER.load(metadataPath); + + // File reading + StageData[] stageData = registrySerializer.deserialize(cacheDir, info, dashObjects); + RegistryReaderImpl reader = new RegistryReaderImpl(info, stageData); + + // Exporting assets + task.run(() -> reader.export(task::setSubTask)); + + // Loading mappings + if (!mappingsSerializer.load(cacheDir, reader, cacheHandlers)) { + this.setStatus(CacheStatus.SAVE); + this.remove(); + return; + } + + DashLoader.LOG.info("Loaded cache in {}", ProfilerUtil.getTimeStringFromStart(start)); + } catch (Exception e) { + DashLoader.LOG.error("Summoned CrashLoader in {}", ProfilerUtil.getTimeStringFromStart(start), e); + this.setStatus(CacheStatus.SAVE); + this.remove(); + } + } + + public boolean exists() { + return Files.exists(this.getDir()); + } + + public void remove() { + try { + FileUtils.deleteDirectory(this.getDir().toFile()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void reset() { + this.setStatus(CacheStatus.IDLE); + } + + public Path getDir() { + if (hash == null) { + throw new RuntimeException("Cache hash has not been set."); + } + return cacheDir.resolve(hash + "/"); + } + + public CacheStatus getStatus() { + return status; + } + + private void setStatus(CacheStatus status) { + if (this.status != status) { + this.status = status; + DashLoader.LOG.info("\u001B[46m\u001B[30m DashLoader Status change {}\n\u001B[0m", status); + this.cacheHandlers.forEach(handler -> handler.reset(this)); + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/DashLoader.java b/src/main/java/dev/notalpha/dashloader/DashLoader.java new file mode 100644 index 00000000..84802bc5 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/DashLoader.java @@ -0,0 +1,54 @@ +package dev.notalpha.dashloader; + +import dev.notalpha.dashloader.io.Serializer; +import dev.notalpha.dashloader.io.data.CacheInfo; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.fabricmc.loader.api.metadata.ModMetadata; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.ArrayList; +import java.util.Comparator; + +public final class DashLoader { + public static final Logger LOG = LogManager.getLogger("DashLoader"); + public static final Serializer METADATA_SERIALIZER = new Serializer<>(CacheInfo.class); + public static final String MOD_HASH; + private static final String VERSION = FabricLoader.getInstance() + .getModContainer("dashloader") + .orElseThrow(() -> new IllegalStateException("DashLoader not found... apparently! WTF?")) + .getMetadata() + .getVersion() + .getFriendlyString(); + + static { + ArrayList versions = new ArrayList<>(); + for (ModContainer mod : FabricLoader.getInstance().getAllMods()) { + ModMetadata metadata = mod.getMetadata(); + versions.add(metadata); + } + + versions.sort(Comparator.comparing(ModMetadata::getId)); + + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < versions.size(); i++) { + ModMetadata metadata = versions.get(i); + stringBuilder.append(i).append("$").append(metadata.getId()).append('&').append(metadata.getVersion().getFriendlyString()); + } + + MOD_HASH = DigestUtils.md5Hex(stringBuilder.toString()).toUpperCase(); + } + + private DashLoader() { + LOG.info("Initializing DashLoader {}.", VERSION); + if (FabricLoader.getInstance().isDevelopmentEnvironment()) { + LOG.warn("DashLoader launched in dev."); + } + } + + @SuppressWarnings("EmptyMethod") + public static void bootstrap() { + } +} diff --git a/src/main/java/dev/notalpha/dashloader/DashObjectClass.java b/src/main/java/dev/notalpha/dashloader/DashObjectClass.java new file mode 100644 index 00000000..74b4c3a4 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/DashObjectClass.java @@ -0,0 +1,78 @@ +package dev.notalpha.dashloader; + +import dev.notalpha.dashloader.api.DashObject; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * A DashObject which is an object with adds Dash support to a target object.
+ * This class is very lazy as reflection is really slow + * + * @param Raw + * @param Dashable + */ +public final class DashObjectClass> { + private final Class dashClass; + int dashObjectId; + @Nullable + private Class targetClass; + + public DashObjectClass(Class dashClass) { + //noinspection unchecked + this.dashClass = (Class) dashClass; + } + + public Class getDashClass() { + return this.dashClass; + } + + // lazy + @NotNull + public Class getTargetClass() { + if (this.targetClass == null) { + Type[] genericInterfaces = this.dashClass.getGenericInterfaces(); + if (genericInterfaces.length == 0) { + throw new RuntimeException(this.dashClass + " does not implement DashObject."); + } + + boolean foundDashObject = false; + Class[] interfaces = this.dashClass.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) { + if (interfaces[i] == DashObject.class) { + foundDashObject = true; + var genericInterface = genericInterfaces[i]; + if (genericInterface instanceof ParameterizedType targetClass) { + if (targetClass.getActualTypeArguments()[0] instanceof Class targetClass2) { + this.targetClass = (Class) targetClass2; + } else { + throw new RuntimeException(this.dashClass + " has a non resolvable DashObject parameter"); + } + } else { + throw new RuntimeException(this.dashClass + " implements raw DashObject"); + } + } + } + + if (!foundDashObject) { + throw new RuntimeException(this.dashClass + " must implement DashObject"); + } + } + return this.targetClass; + } + + public int getDashObjectId() { + return dashObjectId; + } + + @Override + public String toString() { + return "DashObjectClass{" + + "dashClass=" + dashClass + + ", targetClass=" + targetClass + + ", dashObjectId=" + dashObjectId + + '}'; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/api/CachingData.java b/src/main/java/dev/notalpha/dashloader/api/CachingData.java new file mode 100644 index 00000000..db89eb13 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/CachingData.java @@ -0,0 +1,82 @@ +package dev.notalpha.dashloader.api; + +import dev.notalpha.dashloader.api.cache.Cache; +import dev.notalpha.dashloader.api.cache.CacheStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class CachingData { + @Nullable + private final CacheStatus onlyOn; + @Nullable + private D data; + private Cache cacheManager; + @Nullable + private CacheStatus dataStatus; + + public CachingData(@Nullable CacheStatus onlyOn) { + this.data = null; + this.onlyOn = onlyOn; + } + + public CachingData() { + this(null); + } + + public void visit(CacheStatus status, Consumer consumer) { + if (this.active(status)) { + consumer.accept(this.data); + } + } + + /** + * Gets the value or returns null if its status does not match the current state. + **/ + public @Nullable D get(CacheStatus status) { + if (this.active(status)) { + return this.data; + } + return null; + } + + public void reset(Cache cacheManager, @NotNull D data) { + reset(cacheManager, () -> data); + } + + public void reset(Cache cacheManager, Supplier<@NotNull D> data) { + this.cacheManager = cacheManager; + set(cacheManager.getStatus(), data); + } + + public void set(CacheStatus status, @NotNull D data) { + set(status, () -> data); + } + + /** + * Sets the optional data to the intended status + **/ + public void set(CacheStatus status, Supplier<@NotNull D> data) { + if (onlyOn != null && onlyOn != status) { + this.data = null; + this.dataStatus = null; + return; + } + + if (cacheManager == null) { + throw new RuntimeException("cacheManager is null. This OptionData has never been reset in its handler."); + } + + CacheStatus currentStatus = cacheManager.getStatus(); + if (status == currentStatus) { + this.dataStatus = status; + this.data = data.get(); + } + } + + public boolean active(CacheStatus status) { + return status == this.dataStatus && status == cacheManager.getStatus() && this.data != null && (onlyOn == null || onlyOn == status); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/api/DashEntrypoint.java b/src/main/java/dev/notalpha/dashloader/api/DashEntrypoint.java new file mode 100644 index 00000000..00a2f712 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/DashEntrypoint.java @@ -0,0 +1,15 @@ +package dev.notalpha.dashloader.api; + +import dev.notalpha.dashloader.api.cache.CacheFactory; + +/** + * The DashEntrypoint allows operations on the DashLoader Minecraft cache, like adding support to external DashObjects, Modules or MissingHandlers. + */ +public interface DashEntrypoint { + /** + * Runs on DashLoader initialization. This is quite early compared to the cache. + * + * @param factory Factory to register your DashObjects/Modules to. + */ + void onDashLoaderInit(CacheFactory factory); +} diff --git a/src/main/java/dev/notalpha/dashloader/api/DashModule.java b/src/main/java/dev/notalpha/dashloader/api/DashModule.java new file mode 100644 index 00000000..62cbe660 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/DashModule.java @@ -0,0 +1,68 @@ +package dev.notalpha.dashloader.api; + +import dev.notalpha.dashloader.api.cache.Cache; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.taski.builtin.StepTask; + +/** + * A DashModule is a manager of data in a Cache. + * It's responsible for providing and consuming objects from/to the registry and saving the resulting id's and/or other data into the data class. + *

+ * These may conditionally be disabled by {@link DashModule#isActive()}. + * + * @param The Data class which will be saved + */ +public interface DashModule { + /** + * This runs when the module gets reset by dashloader. + * This is used to reset CachingData instances to their correct state. + * + * @param cache The cache object which is resetting. + */ + void reset(Cache cache); + + /** + * Runs when DashLoader is creating a save. + * This should fill the RegistryFactory with objects that it wants available on next load. + * + * @param writer RegistryWriter to provide objects to. + * @param task Task to track progress of the saving. + * @return The DataObject which will be saved for next load. + */ + D save(RegistryWriter writer, StepTask task); + + /** + * Runs when DashLoader is loading back a save. + * This should read back the objects from the RegistryReading with the ids commonly saved in the DataObject. + * + * @param data DataObject which got saved in {@link DashModule#save(RegistryWriter, StepTask)} + * @param reader RegistryReader which contains objects which got cached. + * @param task Task to track progress of the loading. + */ + void load(D data, RegistryReader reader, StepTask task); + + /** + * Gets the DataClass which the module uses to save data for the cache load. + */ + Class getDataClass(); + + /** + * Returns if the module is currently active. + *

+ * When saving, if the module is active it will run the save method and then save the data object to the cache. + *

+ * When loading back the cache. If the cache did not have the module in the same state as now, it will force a recache. + */ + default boolean isActive() { + return true; + } + + /** + * The weight of the module in the progress task. + * The bigger the value the more space the module will use in the progress. + */ + default float taskWeight() { + return 100; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/api/DashObject.java b/src/main/java/dev/notalpha/dashloader/api/DashObject.java new file mode 100644 index 00000000..bc835637 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/DashObject.java @@ -0,0 +1,38 @@ +package dev.notalpha.dashloader.api; + +import dev.notalpha.dashloader.api.registry.RegistryReader; + +/** + * A DashObject is responsible for making normal objects serializable + * by mapping them to a more serializable format and deduplicating inner objects through the registry. + * + * @param The target object which it's adding support to. + */ +@SuppressWarnings("unused") +public interface DashObject { + /** + * Runs before export on the main thread. + * + * @see DashObject#export(RegistryReader) + */ + @SuppressWarnings("unused") + default void preExport(RegistryReader reader) { + } + + /** + * The export method converts the DashObject into the original counterpart which was provided on save. + *

+ * Note: This runs in parallel meaning that it does not run on the Main thread. If you need to load things on the main thread use {@link DashObject#postExport(RegistryReader)} + */ + @SuppressWarnings("unused") + O export(RegistryReader reader); + + /** + * Runs after export on the main thread. + * + * @see DashObject#export(RegistryReader) + */ + @SuppressWarnings("unused") + default void postExport(RegistryReader reader) { + } +} diff --git a/src/main/java/dev/notalpha/dashloader/api/cache/Cache.java b/src/main/java/dev/notalpha/dashloader/api/cache/Cache.java new file mode 100644 index 00000000..71253f50 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/cache/Cache.java @@ -0,0 +1,52 @@ +package dev.notalpha.dashloader.api.cache; + +import dev.notalpha.taski.builtin.StepTask; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.function.Consumer; + +/** + * The Cache is responsible for managing, saving and loading caches from its assigned directory. + * + * @see CacheFactory + */ +public interface Cache { + /** + * Attempt to load the DashLoader cache with the current name if it exists, + * else it will set the cache into SAVE status and reset managers to be ready for caching. + * + * @param name The cache name which will be used. + */ + void load(String name); + + /** + * Create and save a cache from the modules which are currently enabled. + * + * @param taskConsumer An optional task function which allows you to track the progress. + * @return If the cache creation was successful + */ + boolean save(@Nullable Consumer taskConsumer); + + /** + * Resets the cache into an IDLE state where it resets the cache storages to save memory. + */ + void reset(); + + /** + * Remove the existing cache if it exists. + */ + void remove(); + + /** + * Gets the current status or state of the Cache. + */ + CacheStatus getStatus(); + + /** + * Gets the current directory of the cache. + * + * @return Path to the cache directory which contains the data. + */ + Path getDir(); +} diff --git a/src/main/java/dev/notalpha/dashloader/api/cache/CacheFactory.java b/src/main/java/dev/notalpha/dashloader/api/cache/CacheFactory.java new file mode 100644 index 00000000..e8a7b23a --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/cache/CacheFactory.java @@ -0,0 +1,55 @@ +package dev.notalpha.dashloader.api.cache; + +import dev.notalpha.dashloader.CacheFactoryImpl; +import dev.notalpha.dashloader.api.DashModule; +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryWriter; + +import java.nio.file.Path; +import java.util.function.BiFunction; + +/** + * The CacheFactory is used to construct a {@link Cache} + */ +public interface CacheFactory { + /** + * Creates a new Factory + * + * @return CacheFactory + */ + static CacheFactory create() { + return new CacheFactoryImpl(); + } + + /** + * Adds a DashObject to the Cache, this will allow the Cache to cache the DashObject's target. + * + * @param dashClass The class + */ + void addDashObject(Class> dashClass); + + /** + * Adds a module to the Cache. Please note only enabled Modules will actually be cached. + */ + void addModule(DashModule module); + + /** + * Adds a missing handler to the Cache, a missing handler is used when an Object does not have a DashObject directly bound to it. + * The registry will go through every missing handler until it finds one which does not return {@code null}. + * + * @param rClass The class which the object needs to implement. + * If you want to go through any object you can insert {@code Object.class} because every java object inherits this. + * @param func The consumer function for an object which fits the {@code rClass}. + * If this function returns a non-null value, it will use that DashObject for serialization of that object. + * @param The super class of the objects being missed. + */ + void addMissingHandler(Class rClass, BiFunction> func); + + /** + * Builds the cache object. + * + * @param path The directory which contains the caches. + * @return A DashLoader cache object. + */ + Cache build(Path path); +} diff --git a/src/main/java/dev/notalpha/dashloader/api/cache/CacheStatus.java b/src/main/java/dev/notalpha/dashloader/api/cache/CacheStatus.java new file mode 100644 index 00000000..02c7b5d6 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/cache/CacheStatus.java @@ -0,0 +1,19 @@ +package dev.notalpha.dashloader.api.cache; + +/** + * Status/State values for a given Cache. + */ +public enum CacheStatus { + /** + * The cache is in an IDLE state where there are no temporary resources in memory. + */ + IDLE, + /** + * The Cache is loading back an existing cache from a file. + */ + LOAD, + /** + * The Cache is trying to create/save a cache. + */ + SAVE, +} diff --git a/src/main/java/dev/notalpha/dashloader/api/collection/IntIntList.java b/src/main/java/dev/notalpha/dashloader/api/collection/IntIntList.java new file mode 100644 index 00000000..6a82ada4 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/collection/IntIntList.java @@ -0,0 +1,26 @@ +package dev.notalpha.dashloader.api.collection; + +import java.util.ArrayList; +import java.util.List; + +public record IntIntList(List list) { + public IntIntList() { + this(new ArrayList<>()); + } + + public void put(int key, int value) { + this.list.add(new IntInt(key, value)); + } + + public void forEach(IntIntConsumer c) { + this.list.forEach(v -> c.accept(v.key, v.value)); + } + + @FunctionalInterface + public interface IntIntConsumer { + void accept(int key, int value); + } + + public record IntInt(int key, int value) { + } +} diff --git a/src/main/java/dev/notalpha/dashloader/api/collection/IntObjectList.java b/src/main/java/dev/notalpha/dashloader/api/collection/IntObjectList.java new file mode 100644 index 00000000..46f7104c --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/collection/IntObjectList.java @@ -0,0 +1,26 @@ +package dev.notalpha.dashloader.api.collection; + +import java.util.ArrayList; +import java.util.List; + +public record IntObjectList(List> list) { + public IntObjectList() { + this(new ArrayList<>()); + } + + public void put(int key, V value) { + this.list.add(new IntObjectEntry<>(key, value)); + } + + public void forEach(IntObjectConsumer c) { + this.list.forEach(v -> c.accept(v.key, v.value)); + } + + @FunctionalInterface + public interface IntObjectConsumer { + void accept(int key, V value); + } + + public record IntObjectEntry(int key, V value) { + } +} diff --git a/src/main/java/dev/notalpha/dashloader/api/collection/ObjectIntList.java b/src/main/java/dev/notalpha/dashloader/api/collection/ObjectIntList.java new file mode 100644 index 00000000..e26aaeca --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/collection/ObjectIntList.java @@ -0,0 +1,26 @@ +package dev.notalpha.dashloader.api.collection; + +import java.util.ArrayList; +import java.util.List; + +public record ObjectIntList(List> list) { + public ObjectIntList() { + this(new ArrayList<>()); + } + + public void put(K key, int value) { + this.list.add(new ObjectIntEntry<>(key, value)); + } + + public void forEach(ObjectIntConsumer c) { + this.list.forEach(v -> c.accept(v.key, v.value)); + } + + @FunctionalInterface + public interface ObjectIntConsumer { + void accept(K key, int value); + } + + public record ObjectIntEntry(K key, int value) { + } +} diff --git a/src/main/java/dev/notalpha/dashloader/api/collection/ObjectObjectList.java b/src/main/java/dev/notalpha/dashloader/api/collection/ObjectObjectList.java new file mode 100644 index 00000000..10698eca --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/collection/ObjectObjectList.java @@ -0,0 +1,22 @@ +package dev.notalpha.dashloader.api.collection; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +public record ObjectObjectList(List> list) { + public ObjectObjectList() { + this(new ArrayList<>()); + } + + public void put(K key, V value) { + this.list.add(new ObjectObjectEntry<>(key, value)); + } + + public void forEach(BiConsumer c) { + this.list.forEach(v -> c.accept(v.key, v.value)); + } + + public record ObjectObjectEntry(K key, V value) { + } +} diff --git a/src/main/java/dev/notalpha/dashloader/api/registry/RegistryAddException.java b/src/main/java/dev/notalpha/dashloader/api/registry/RegistryAddException.java new file mode 100644 index 00000000..30fc972f --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/registry/RegistryAddException.java @@ -0,0 +1,17 @@ +package dev.notalpha.dashloader.api.registry; + +public class RegistryAddException extends RuntimeException { + public final Class targetClass; + public final Object object; + + public RegistryAddException(Class targetClass, Object object) { + super(); + this.targetClass = targetClass; + this.object = object; + } + + @Override + public String getMessage() { + return "Could not find a ChunkWriter for " + targetClass + ": " + object; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/api/registry/RegistryReader.java b/src/main/java/dev/notalpha/dashloader/api/registry/RegistryReader.java new file mode 100644 index 00000000..d114f330 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/registry/RegistryReader.java @@ -0,0 +1,18 @@ +package dev.notalpha.dashloader.api.registry; + +/** + * The RegistryReader is used to read objects from the cache's registry. + * + * @see RegistryWriter + */ +public interface RegistryReader { + /** + * Gets an object from the Cache. + * + * @param pointer The registry pointer to the object. + * @param Target object class. + * @return The object that got cached. + * @see RegistryWriter#add(Object) + */ + R get(final int pointer); +} diff --git a/src/main/java/dev/notalpha/dashloader/api/registry/RegistryUtil.java b/src/main/java/dev/notalpha/dashloader/api/registry/RegistryUtil.java new file mode 100644 index 00000000..6f4aa41f --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/registry/RegistryUtil.java @@ -0,0 +1,43 @@ +package dev.notalpha.dashloader.api.registry; + +/** + * Contains utilities for handling RegistryIds + */ +public final class RegistryUtil { + /** + * Creates a new registry id. + * + * @param objectPos The chunk object position. + * @param chunkPos The index to the chunk the object is in. + * @return Registry ID + */ + public static int createId(int objectPos, byte chunkPos) { + if (chunkPos > 0b111111) { + throw new IllegalStateException("Chunk pos is too big. " + chunkPos + " > " + 0x3f); + } + if (objectPos > 0x3ffffff) { + throw new IllegalStateException("Object pos is too big. " + objectPos + " > " + 0x3ffffff); + } + return objectPos << 6 | (chunkPos & 0x3f); + } + + /** + * Gets the chunk id portion of the Registry ID + * + * @param id Registry ID + * @return Chunk index. + */ + public static byte getChunkId(int id) { + return (byte) (id & 0x3f); + } + + /** + * Gets the object id portion of the Registry ID + * + * @param id Registry ID + * @return The index of the object in the chunk. + */ + public static int getObjectId(int id) { + return id >>> 6; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/api/registry/RegistryWriter.java b/src/main/java/dev/notalpha/dashloader/api/registry/RegistryWriter.java new file mode 100644 index 00000000..8b93f32e --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/api/registry/RegistryWriter.java @@ -0,0 +1,19 @@ +package dev.notalpha.dashloader.api.registry; + +/** + * A RegistryWriter is provided to DashObjects and Modules on save minecraft objects to the cache by converting them into DashObjects. + * On cache load, a RegistryReader is provided so you can read back the objects from the cache. + * + * @see RegistryReader + */ +public interface RegistryWriter { + /** + * Adds an object to the Cache, the object needs to have a DashObject backing it else it will fail. + * + * @param object The Object to add to the cache. + * @param The target class being cached. + * @return A registry id which points to the object. + * @see RegistryReader#get(int) + */ + int add(R object); +} diff --git a/src/main/java/dev/notalpha/dashloader/client/DashLoaderClient.java b/src/main/java/dev/notalpha/dashloader/client/DashLoaderClient.java new file mode 100644 index 00000000..9aa69dc0 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/DashLoaderClient.java @@ -0,0 +1,86 @@ +package dev.notalpha.dashloader.client; + +import dev.notalpha.dashloader.api.DashEntrypoint; +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.cache.Cache; +import dev.notalpha.dashloader.api.cache.CacheFactory; +import dev.notalpha.dashloader.client.atlas.AtlasModule; +import dev.notalpha.dashloader.client.blockstate.DashBlockState; +import dev.notalpha.dashloader.client.font.*; +import dev.notalpha.dashloader.client.identifier.DashIdentifier; +import dev.notalpha.dashloader.client.identifier.DashSpriteIdentifier; +import dev.notalpha.dashloader.client.model.ModelModule; +import dev.notalpha.dashloader.client.model.predicates.*; +import dev.notalpha.dashloader.client.shader.ShaderModule; +import dev.notalpha.dashloader.client.splash.SplashModule; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.renderer.block.model.multipart.CombinedCondition; +import net.minecraft.client.renderer.block.model.multipart.Condition; +import net.minecraft.client.renderer.block.model.multipart.KeyValueCondition; +import net.minecraft.client.resources.model.Material; +import net.minecraft.resources.ResourceLocation; + +import java.nio.file.Path; +import java.util.List; + +public class DashLoaderClient implements DashEntrypoint { + public static final Cache CACHE; + public static boolean NEEDS_RELOAD = false; + + static { + CacheFactory cacheManagerFactory = CacheFactory.create(); + List entryPoints = FabricLoader.getInstance().getEntrypoints("dashloader", DashEntrypoint.class); + for (DashEntrypoint entryPoint : entryPoints) { + entryPoint.onDashLoaderInit(cacheManagerFactory); + } + + CACHE = cacheManagerFactory.build(Path.of("./dashloader-cache/client/")); + } + + @Override + public void onDashLoaderInit(CacheFactory factory) { + factory.addModule(new AtlasModule()); + factory.addModule(new FontModule()); + factory.addModule(new ModelModule()); + factory.addModule(new ShaderModule()); + factory.addModule(new SplashModule()); + + factory.addMissingHandler(ResourceLocation.class, (identifier, registryWriter) -> new DashIdentifier(identifier)); + factory.addMissingHandler(Material.class, DashSpriteIdentifier::new); + factory.addMissingHandler( + Condition.class, + (selector, writer) -> { + if (selector instanceof CombinedCondition s && s.operation() == CombinedCondition.Operation.AND) { + return new DashAndPredicate(s, writer); + } else if (selector instanceof CombinedCondition s && s.operation() == CombinedCondition.Operation.OR) { + return new DashOrPredicate(s, writer); + } else if (selector instanceof KeyValueCondition s) { + return new DashSimplePredicate(s); + } else if (selector instanceof BooleanSelector s) { + return new DashStaticPredicate(s.selector); + } else { + throw new RuntimeException("someone is having fun with lambda selectors again"); + } + } + ); + + //noinspection unchecked + for (Class> aClass : new Class[]{ + DashIdentifier.class, + DashSpriteIdentifier.class, + DashAndPredicate.class, + DashOrPredicate.class, + DashSimplePredicate.class, + DashStaticPredicate.class, + DashBitmapFont.class, + DashBlankFont.class, + DashSpaceFont.class, + DashTrueTypeFont.class, + DashUnihexFont.class, + DashFontFilterPair.class, + DashBlockState.class + }) { + factory.addDashObject(aClass); + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/client/Dazy.java b/src/main/java/dev/notalpha/dashloader/client/Dazy.java new file mode 100644 index 00000000..cf9d2a87 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/Dazy.java @@ -0,0 +1,23 @@ +package dev.notalpha.dashloader.client; + +import java.util.function.Function; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.model.Material; +import org.jetbrains.annotations.Nullable; + +// its lazy, but dash! Used for resolution of sprites. +public abstract class Dazy { + @Nullable + private transient V loaded; + + protected abstract V resolve(Function spriteLoader); + + public V get(Function spriteLoader) { + if (loaded != null) { + return loaded; + } + + loaded = resolve(spriteLoader); + return loaded; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/client/ModMenuCompat.java b/src/main/java/dev/notalpha/dashloader/client/ModMenuCompat.java new file mode 100644 index 00000000..3adab8a3 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/ModMenuCompat.java @@ -0,0 +1,12 @@ +package dev.notalpha.dashloader.client; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import net.minecraft.client.gui.screens.Screen; + +public class ModMenuCompat implements ModMenuApi { + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return parent -> parent; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/client/atlas/AtlasModule.java b/src/main/java/dev/notalpha/dashloader/client/atlas/AtlasModule.java new file mode 100644 index 00000000..0ff3db0a --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/atlas/AtlasModule.java @@ -0,0 +1,83 @@ +package dev.notalpha.dashloader.client.atlas; + +import dev.notalpha.dashloader.api.CachingData; +import dev.notalpha.dashloader.api.DashModule; +import dev.notalpha.dashloader.api.cache.Cache; +import dev.notalpha.dashloader.api.cache.CacheStatus; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.client.DashLoaderClient; +import dev.notalpha.dashloader.config.ConfigHandler; +import dev.notalpha.dashloader.config.Option; +import dev.notalpha.taski.builtin.StepTask; +import net.minecraft.client.Minecraft; +import com.mojang.blaze3d.platform.NativeImage; +import org.apache.commons.codec.digest.DigestUtils; + +import java.io.FileInputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.FutureTask; + +public class AtlasModule implements DashModule { + public static final CachingData>>> ATLASES = new CachingData<>(); + + @Override + public void reset(Cache cache) { + ATLASES.reset(cache, new HashMap<>()); + } + + @Override + public Data save(RegistryWriter writer, StepTask task) { + var cachedAtlases = ATLASES.get(CacheStatus.SAVE); + // Not saving the atlases in the main cache, check `SpriteAtlasTextureMixin` + + if (cachedAtlases == null) { + return null; + } + + return new Data(cachedAtlases.keySet().toArray(new String[0])); + } + + @Override + public void load(Data data, RegistryReader reader, StepTask t) { + var path = getAtlasFolder(); + + HashMap>> out = new HashMap<>(); + + var maxMipLevel = Minecraft.getInstance().options.mipmapLevels().get(); + for (String atlasId : data.atlasIds) { + var tasks = new ArrayList>(); + + for (int i = 0; i <= maxMipLevel; i++) { // don't load more atlases than needed + Path imgPath = path.resolve(DigestUtils.md5Hex(atlasId + i).toUpperCase()); + if (!Files.exists(imgPath)) break; + + tasks.add(new FutureTask<>(() -> NativeImage.read(new FileInputStream(imgPath.toFile())))); + Thread.startVirtualThread(tasks.getLast()); + } + out.put(atlasId, tasks); + } + + ATLASES.set(CacheStatus.LOAD, out); + } + + @Override + public boolean isActive() { + return ConfigHandler.optionActive(Option.CACHE_ATLASES); + } + + public static Path getAtlasFolder() { + return DashLoaderClient.CACHE.getDir().resolve("atlases"); + } + + @Override + public Class getDataClass() { + return Data.class; + } + + public record Data(String[] atlasIds) { + } +} diff --git a/src/main/java/dev/notalpha/dashloader/client/blockstate/DashBlockState.java b/src/main/java/dev/notalpha/dashloader/client/blockstate/DashBlockState.java new file mode 100644 index 00000000..9f42ab3a --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/blockstate/DashBlockState.java @@ -0,0 +1,86 @@ +package dev.notalpha.dashloader.client.blockstate; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.mixin.accessor.ModelLoaderAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; + +public final class DashBlockState implements DashObject { + public static final ResourceLocation ITEM_FRAME = ResourceLocation.fromNamespaceAndPath("dashloader", "itemframewhy"); + public final int owner; + public final int pos; + + public DashBlockState(int owner, int pos) { + this.owner = owner; + this.pos = pos; + } + + public DashBlockState(BlockState blockState, RegistryWriter writer) { + var block = blockState.getBlock(); + int pos = -1; + + ResourceLocation owner = null; + { + var states = ModelLoaderAccessor.getTheItemFrameThing().getPossibleStates(); + for (int i = 0; i < states.size(); i++) { + BlockState state = states.get(i); + if (state.equals(blockState)) { + pos = i; + owner = ITEM_FRAME; + break; + } + } + } + + if (pos == -1) { + var states = block.getStateDefinition().getPossibleStates(); + for (int i = 0; i < states.size(); i++) { + BlockState state = states.get(i); + if (state.equals(blockState)) { + pos = i; + owner = BuiltInRegistries.BLOCK.getKey(block); + break; + } + } + } + + if (owner == null) { + throw new RuntimeException("Could not find a blockstate for " + blockState); + } + + this.owner = writer.add(owner); + this.pos = pos; + } + + @Override + public BlockState export(final RegistryReader reader) { + final ResourceLocation id = reader.get(this.owner); + // if its item frame get its state from the model loader as mojank is mojank + if (id.equals(ITEM_FRAME)) { + return ModelLoaderAccessor.getTheItemFrameThing().getPossibleStates().get(this.pos); + } else { + return BuiltInRegistries.BLOCK.getValue(id).getStateDefinition().getPossibleStates().get(this.pos); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DashBlockState that = (DashBlockState) o; + + if (owner != that.owner) return false; + return pos == that.pos; + } + + @Override + public int hashCode() { + int result = owner; + result = 31 * result + pos; + return result; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/client/font/DashBitmapFont.java b/src/main/java/dev/notalpha/dashloader/client/font/DashBitmapFont.java new file mode 100644 index 00000000..75ee28f6 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/font/DashBitmapFont.java @@ -0,0 +1,35 @@ +package dev.notalpha.dashloader.client.font; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.collection.IntObjectList; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.mixin.accessor.BitmapFontAccessor; +import net.minecraft.client.gui.font.CodepointMap; +import net.minecraft.client.gui.font.providers.BitmapProvider; + +import java.util.ArrayList; + +public final class DashBitmapFont implements DashObject { +public final int image; +public final IntObjectList glyphs; + +public DashBitmapFont(int image, + IntObjectList glyphs) { +this.image = image; +this.glyphs = glyphs; +} + +public DashBitmapFont(BitmapProvider bitmapFont, RegistryWriter writer) { +BitmapFontAccessor font = ((BitmapFontAccessor) bitmapFont); +this.image = writer.add(font.getImage()); +this.glyphs = new IntObjectList<>(new ArrayList<>()); +font.getGlyphs().forEach((integer, bitmapFontGlyph) -> this.glyphs.put(integer, new DashBitmapFontGlyph(bitmapFontGlyph, writer))); +} + +public BitmapProvider export(RegistryReader reader) { +CodepointMap out = new CodepointMap<>(Object[]::new, size -> new Object[size][]); +this.glyphs.forEach((key, value) -> out.put(key, value.export(reader))); +return BitmapFontAccessor.init(reader.get(this.image), out); +} +} diff --git a/src/main/java/dev/notalpha/dashloader/client/font/DashBitmapFontGlyph.java b/src/main/java/dev/notalpha/dashloader/client/font/DashBitmapFontGlyph.java new file mode 100644 index 00000000..3e027dc4 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/font/DashBitmapFontGlyph.java @@ -0,0 +1,43 @@ +package dev.notalpha.dashloader.client.font; + +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.mixin.accessor.BitmapFontGlyphAccessor; + +public final class DashBitmapFontGlyph { +public final float scaleFactor; +public final int image; +public final int x; +public final int y; +public final int width; +public final int height; +public final int advance; +public final int ascent; + +public DashBitmapFontGlyph(float scaleFactor, int image, int x, int y, int width, int height, int advance, int ascent) { +this.scaleFactor = scaleFactor; +this.image = image; +this.x = x; +this.y = y; +this.width = width; +this.height = height; +this.advance = advance; +this.ascent = ascent; +} + +public DashBitmapFontGlyph(Object bitmapFontGlyph, RegistryWriter writer) { +BitmapFontGlyphAccessor font = ((BitmapFontGlyphAccessor) bitmapFontGlyph); +this.scaleFactor = font.getScaleFactor(); +this.image = writer.add(font.getImage()); +this.x = font.getX(); +this.y = font.getY(); +this.width = font.getWidth(); +this.height = font.getHeight(); +this.advance = font.getAdvance(); +this.ascent = font.getAscent(); +} + +public Object export(RegistryReader handler) { +return BitmapFontGlyphAccessor.init(this.scaleFactor, handler.get(this.image), this.x, this.y, this.width, this.height, this.advance, this.ascent); +} +} diff --git a/src/main/java/dev/notalpha/dashloader/client/font/DashBlankFont.java b/src/main/java/dev/notalpha/dashloader/client/font/DashBlankFont.java new file mode 100644 index 00000000..89d01a94 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/font/DashBlankFont.java @@ -0,0 +1,12 @@ +package dev.notalpha.dashloader.client.font; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import net.minecraft.client.gui.font.AllMissingGlyphProvider; + +public final class DashBlankFont implements DashObject { +@Override +public AllMissingGlyphProvider export(RegistryReader exportHandler) { +return new AllMissingGlyphProvider(); +} +} diff --git a/src/main/java/dev/notalpha/dashloader/client/font/DashFontFilterPair.java b/src/main/java/dev/notalpha/dashloader/client/font/DashFontFilterPair.java new file mode 100644 index 00000000..d30267b8 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/font/DashFontFilterPair.java @@ -0,0 +1,37 @@ +package dev.notalpha.dashloader.client.font; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.collection.IntIntList; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.mixin.accessor.FilterMapAccessor; +import com.mojang.blaze3d.font.GlyphProvider; +import net.minecraft.client.gui.font.FontOption; + +import java.util.HashMap; +import java.util.Map; + +public class DashFontFilterPair implements DashObject { +public final int provider; +public final IntIntList filter; + +public DashFontFilterPair(int provider, IntIntList filter) { +this.provider = provider; +this.filter = filter; +} + +public DashFontFilterPair(GlyphProvider.Conditional fontFilterPair, RegistryWriter writer) { +this.provider = writer.add(fontFilterPair.provider()); + +filter = new IntIntList(); +((FilterMapAccessor) fontFilterPair.filter()).getValues().forEach( +(key, value) -> filter.put(key.ordinal(), value ? 1 : 0)); +} + +@Override +public GlyphProvider.Conditional export(RegistryReader reader) { +Map activeFilters = new HashMap<>(); +filter.forEach((key, value) -> activeFilters.put(FontOption.values()[key], value == 1)); +return new GlyphProvider.Conditional(reader.get(provider), new FontOption.Filter(activeFilters)); +} +} diff --git a/src/main/java/dev/notalpha/dashloader/client/font/DashSpaceFont.java b/src/main/java/dev/notalpha/dashloader/client/font/DashSpaceFont.java new file mode 100644 index 00000000..452d467a --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/font/DashSpaceFont.java @@ -0,0 +1,41 @@ +package dev.notalpha.dashloader.client.font; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import com.mojang.blaze3d.font.SpaceProvider; +import it.unimi.dsi.fastutil.ints.IntSet; +import java.util.HashMap; +import java.util.Map; + +public final class DashSpaceFont implements DashObject { +public final int[] ints; +public final float[] floats; + +public DashSpaceFont(int[] ints, float[] floats) { +this.ints = ints; +this.floats = floats; +} + +public DashSpaceFont(SpaceProvider font) { +IntSet glyphs = font.getSupportedGlyphs(); +this.ints = new int[glyphs.size()]; +this.floats = new float[glyphs.size()]; +int i = 0; +for (Integer providedGlyph : glyphs) { +var glyph = font.getGlyph(providedGlyph); +assert glyph != null; +this.ints[i] = providedGlyph; +this.floats[i] = glyph.info().getAdvance(); +i++; +} +} + +@Override +public SpaceProvider export(RegistryReader exportHandler) { +Map map = new HashMap<>(this.ints.length); +for (int i = 0; i < this.ints.length; i++) { + map.put(this.ints[i], this.floats[i]); +} +return new SpaceProvider(map); +} +} diff --git a/src/main/java/dev/notalpha/dashloader/client/font/DashTrueTypeFont.java b/src/main/java/dev/notalpha/dashloader/client/font/DashTrueTypeFont.java new file mode 100644 index 00000000..bc54382f --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/font/DashTrueTypeFont.java @@ -0,0 +1,112 @@ +package dev.notalpha.dashloader.client.font; + +import com.mojang.blaze3d.font.TrueTypeGlyphProvider; +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.cache.CacheStatus; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.io.IOHelper; +import dev.notalpha.dashloader.mixin.accessor.TrueTypeGlyphProviderAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.font.providers.FreeTypeUtil; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import org.lwjgl.PointerBuffer; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.util.freetype.FT_Face; +import org.lwjgl.util.freetype.FT_Vector; +import org.lwjgl.util.freetype.FreeType; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Optional; + +public final class DashTrueTypeFont implements DashObject { + public final byte[] fontData; + public final float size; + public final float oversample; + public final String skip; + public final float shiftX; + public final float shiftY; + + public DashTrueTypeFont(byte[] fontData, float size, float oversample, String skip, float shiftX, float shiftY) { + this.fontData = fontData; + this.size = size; + this.oversample = oversample; + this.skip = skip; + this.shiftX = shiftX; + this.shiftY = shiftY; + } + + public DashTrueTypeFont(TrueTypeGlyphProvider font) { + TrueTypeGlyphProviderAccessor fontAccess = (TrueTypeGlyphProviderAccessor) font; + FT_Face ftFace = fontAccess.getFace(); + FontPrams params = FontModule.FONT_TO_DATA.get(CacheStatus.SAVE).get(ftFace); + if (params == null) { + throw new IllegalStateException("Missing cached font parameters for TrueType provider"); + } + + byte[] data = null; + try { + Optional resource = Minecraft.getInstance().getResourceManager().getResource(params.id().withPrefix("font/")); + if (resource.isPresent()) { + try (var stream = resource.get().open()) { + data = IOHelper.streamToArray(stream); + } + } + } catch (IOException e) { + throw new RuntimeException("Failed to read TrueType font resource", e); + } + if (data == null) { + throw new IllegalStateException("TrueType font resource was not found"); + } + + try (MemoryStack memoryStack = MemoryStack.stackPush()) { + FT_Vector vec = FT_Vector.malloc(memoryStack); + FreeType.FT_Get_Transform(ftFace, null, vec); + this.shiftX = vec.x() / 64F; + this.shiftY = vec.y() / 64F; + } + + this.fontData = data; + this.size = params.size(); + this.skip = params.skip(); + this.oversample = fontAccess.getOversample(); + } + + @Override + public TrueTypeGlyphProvider export(RegistryReader handler) { + ByteBuffer fontBuffer = MemoryUtil.memAlloc(this.fontData.length); + fontBuffer.put(this.fontData); + fontBuffer.flip(); + + FT_Face ftFace = null; + try { + synchronized (FreeTypeUtil.LIBRARY_LOCK) { + try (MemoryStack memoryStack = MemoryStack.stackPush()) { + PointerBuffer pointerBuffer = memoryStack.mallocPointer(1); + FreeTypeUtil.assertError(FreeType.FT_New_Memory_Face(FreeTypeUtil.getLibrary(), fontBuffer, 0L, pointerBuffer), "Initializing font face"); + ftFace = FT_Face.create(pointerBuffer.get()); + } + + String format = FreeType.FT_Get_Font_Format(ftFace); + if (!"TrueType".equals(format)) { + throw new IllegalStateException("Font is not in TTF format, was " + format); + } + FreeTypeUtil.assertError(FreeType.FT_Select_Charmap(ftFace, FreeType.FT_ENCODING_UNICODE), "Find unicode charmap"); + return new TrueTypeGlyphProvider(fontBuffer, ftFace, this.size, this.oversample, this.shiftX, this.shiftY, this.skip); + } + } catch (Throwable e) { + synchronized (FreeTypeUtil.LIBRARY_LOCK) { + if (ftFace != null) { + FreeType.FT_Done_Face(ftFace); + } + } + MemoryUtil.memFree(fontBuffer); + throw e; + } + } + + public record FontPrams(ResourceLocation id, float size, String skip) { + } +} diff --git a/src/main/java/dev/notalpha/dashloader/client/font/DashUnihexFont.java b/src/main/java/dev/notalpha/dashloader/client/font/DashUnihexFont.java new file mode 100644 index 00000000..95b0e679 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/font/DashUnihexFont.java @@ -0,0 +1,131 @@ +package dev.notalpha.dashloader.client.font; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.collection.IntObjectList; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.mixin.accessor.UnihexProviderAccessor; +import net.minecraft.client.gui.font.CodepointMap; +import net.minecraft.client.gui.font.providers.UnihexProvider; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +public final class DashUnihexFont implements DashObject { + public final IntObjectList glyphs; + + public DashUnihexFont(IntObjectList glyphs) { + this.glyphs = glyphs; + } + + public DashUnihexFont(UnihexProvider rawFont, RegistryWriter writer) { + this.glyphs = new IntObjectList<>(); + var font = ((UnihexProviderAccessor) rawFont); + font.getGlyphs().forEach((codepoint, glyph) -> this.glyphs.put(codepoint, new DashUnicodeTextureGlyph(glyph))); + } + + @Override + public UnihexProvider export(RegistryReader handler) { + CodepointMap container = new CodepointMap<>(Object[]::new, i -> new Object[i][]); + this.glyphs.forEach((codepoint, glyph) -> container.put(codepoint, glyph.exportGlyph())); + return UnihexProviderAccessor.create(container); + } + + public static class DashUnicodeTextureGlyph { + public final byte[] bytes; + public final short[] shorts; + public final int[] ints; + public final int bitWidth; + public final int left; + public final int right; + + public DashUnicodeTextureGlyph(byte[] bytes, short[] shorts, int[] ints, int bitWidth, int left, int right) { + this.bytes = bytes; + this.shorts = shorts; + this.ints = ints; + this.bitWidth = bitWidth; + this.left = left; + this.right = right; + } + + public DashUnicodeTextureGlyph(Object glyph) { + try { + Class glyphClass = glyph.getClass(); + Method contentsMethod = glyphClass.getDeclaredMethod("contents"); + Method leftMethod = glyphClass.getDeclaredMethod("left"); + Method rightMethod = glyphClass.getDeclaredMethod("right"); + contentsMethod.setAccessible(true); + leftMethod.setAccessible(true); + rightMethod.setAccessible(true); + + Object contents = contentsMethod.invoke(glyph); + Method bitWidthMethod = contents.getClass().getMethod("bitWidth"); + Method lineMethod = contents.getClass().getMethod("line", int.class); + int width = (int) bitWidthMethod.invoke(contents); + + this.left = (int) leftMethod.invoke(glyph); + this.right = (int) rightMethod.invoke(glyph); + this.bitWidth = width; + + if (width == 8) { + this.bytes = new byte[16]; + for (int i = 0; i < 16; i++) { + int line = (int) lineMethod.invoke(contents, i); + this.bytes[i] = (byte) (line >>> 24); + } + this.shorts = null; + this.ints = null; + } else if (width == 16) { + this.shorts = new short[16]; + for (int i = 0; i < 16; i++) { + int line = (int) lineMethod.invoke(contents, i); + this.shorts[i] = (short) (line >>> 16); + } + this.bytes = null; + this.ints = null; + } else { + this.ints = new int[16]; + for (int i = 0; i < 16; i++) { + this.ints[i] = (int) lineMethod.invoke(contents, i); + } + this.bytes = null; + this.shorts = null; + } + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed to snapshot Unihex glyph", e); + } + } + + public Object exportGlyph() { + try { + Object lineData = this.exportLineData(); + Class lineDataClass = Class.forName("net.minecraft.client.gui.font.providers.UnihexProvider$LineData"); + Class glyphClass = Class.forName("net.minecraft.client.gui.font.providers.UnihexProvider$Glyph"); + Constructor glyphCtor = glyphClass.getDeclaredConstructor(lineDataClass, int.class, int.class); + glyphCtor.setAccessible(true); + return glyphCtor.newInstance(lineData, this.left, this.right); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed to rebuild Unihex glyph", e); + } + } + + private Object exportLineData() throws ReflectiveOperationException { + if (this.bitWidth == 8) { + Class byteClass = Class.forName("net.minecraft.client.gui.font.providers.UnihexProvider$ByteContents"); + Constructor ctor = byteClass.getDeclaredConstructor(byte[].class); + ctor.setAccessible(true); + return ctor.newInstance((Object) this.bytes); + } + if (this.bitWidth == 16) { + Class shortClass = Class.forName("net.minecraft.client.gui.font.providers.UnihexProvider$ShortContents"); + Constructor ctor = shortClass.getDeclaredConstructor(short[].class); + ctor.setAccessible(true); + return ctor.newInstance((Object) this.shorts); + } + Class intClass = Class.forName("net.minecraft.client.gui.font.providers.UnihexProvider$IntContents"); + Constructor ctor = intClass.getDeclaredConstructor(int[].class, int.class); + ctor.setAccessible(true); + return ctor.newInstance(this.ints, this.bitWidth); + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/client/font/FontModule.java b/src/main/java/dev/notalpha/dashloader/client/font/FontModule.java new file mode 100644 index 00000000..a9c926e1 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/font/FontModule.java @@ -0,0 +1,115 @@ +package dev.notalpha.dashloader.client.font; + +import dev.notalpha.dashloader.api.CachingData; +import dev.notalpha.dashloader.api.DashModule; +import dev.notalpha.dashloader.api.cache.Cache; +import dev.notalpha.dashloader.api.cache.CacheStatus; +import dev.notalpha.dashloader.api.collection.IntObjectList; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.config.ConfigHandler; +import dev.notalpha.dashloader.config.Option; +import dev.notalpha.taski.builtin.StepTask; +import com.mojang.blaze3d.font.GlyphProvider; +import net.minecraft.resources.ResourceLocation; +import org.lwjgl.util.freetype.FT_Face; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FontModule implements DashModule { +public static final CachingData DATA = new CachingData<>(); +public static final CachingData> FONT_TO_DATA = new CachingData<>(); + +@Override +public void reset(Cache cache) { +DATA.reset(cache, new ProviderIndex(new HashMap<>(), new ArrayList<>())); +FONT_TO_DATA.reset(cache, new HashMap<>()); +} + +@Override +public Data save(RegistryWriter factory, StepTask task) { +ProviderIndex providerIndex = DATA.get(CacheStatus.SAVE); +assert providerIndex != null; + +int taskSize = 0; +for (List value : providerIndex.providers.values()) { +taskSize += value.size(); +} +taskSize += providerIndex.allProviders.size(); +task.reset(taskSize); + +var providers = new IntObjectList>(); +providerIndex.providers.forEach((identifier, fontFilterPairs) -> { +var values = new ArrayList(); +for (GlyphProvider.Conditional fontFilterPair : fontFilterPairs) { +values.add(factory.add(fontFilterPair)); +task.next(); +} +providers.put(factory.add(identifier), values); +}); + +var allProviders = new ArrayList(); +for (GlyphProvider allProvider : providerIndex.allProviders) { +allProviders.add(factory.add(allProvider)); +task.next(); +} + +return new Data(new DashProviderIndex(providers, allProviders)); +} + +@Override +public void load(Data data, RegistryReader reader, StepTask task) { +ProviderIndex index = new ProviderIndex(new HashMap<>(), new ArrayList<>()); +data.fontMap.providers.forEach((key, value) -> { +var fonts = new ArrayList(); +for (Integer i : value) { +fonts.add(reader.get(i)); +} +index.providers.put(reader.get(key), fonts); +}); + +data.fontMap.allProviders.forEach((value) -> index.allProviders.add(reader.get(value))); +DATA.set(CacheStatus.LOAD, index); +} + +@Override +public Class getDataClass() { +return Data.class; +} + +@Override +public boolean isActive() { +return ConfigHandler.optionActive(Option.CACHE_FONT); +} + +public static final class Data { +public final DashProviderIndex fontMap; + +public Data(DashProviderIndex fontMap) { +this.fontMap = fontMap; +} +} + +public static final class DashProviderIndex { +public final IntObjectList> providers; +public final List allProviders; + +public DashProviderIndex(IntObjectList> providers, List allProviders) { +this.providers = providers; +this.allProviders = allProviders; +} +} + +public static final class ProviderIndex { +public final Map> providers; +public final List allProviders; + +public ProviderIndex(Map> providers, List allProviders) { +this.providers = providers; +this.allProviders = allProviders; +} +} +} diff --git a/src/main/java/dev/notalpha/dashloader/client/identifier/DashIdentifier.java b/src/main/java/dev/notalpha/dashloader/client/identifier/DashIdentifier.java new file mode 100644 index 00000000..4e795822 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/identifier/DashIdentifier.java @@ -0,0 +1,44 @@ +package dev.notalpha.dashloader.client.identifier; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.mixin.accessor.IdentifierAccessor; +import net.minecraft.resources.ResourceLocation; + +public final class DashIdentifier implements DashObject { + public final String namespace; + public final String path; + + public DashIdentifier(String namespace, String path) { + this.namespace = namespace; + this.path = path; + } + + public DashIdentifier(ResourceLocation identifier) { + this.namespace = identifier.getNamespace(); + this.path = identifier.getPath(); + } + + @Override + public ResourceLocation export(RegistryReader exportHandler) { + return IdentifierAccessor.init(this.namespace, this.path); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DashIdentifier that = (DashIdentifier) o; + + if (!namespace.equals(that.namespace)) return false; + return path.equals(that.path); + } + + @Override + public int hashCode() { + int result = namespace.hashCode(); + result = 31 * result + path.hashCode(); + return result; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/client/identifier/DashSpriteIdentifier.java b/src/main/java/dev/notalpha/dashloader/client/identifier/DashSpriteIdentifier.java new file mode 100644 index 00000000..ffecd704 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/identifier/DashSpriteIdentifier.java @@ -0,0 +1,26 @@ +package dev.notalpha.dashloader.client.identifier; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import net.minecraft.client.resources.model.Material; + +public class DashSpriteIdentifier implements DashObject { + public final int atlas; + public final int texture; + + public DashSpriteIdentifier(int atlas, int texture) { + this.atlas = atlas; + this.texture = texture; + } + + public DashSpriteIdentifier(Material identifier, RegistryWriter writer) { + this.atlas = writer.add(identifier.atlasLocation()); + this.texture = writer.add(identifier.texture()); + } + + @Override + public Material export(RegistryReader reader) { + return new Material(reader.get(atlas), reader.get(texture)); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/client/model/ModelModule.java b/src/main/java/dev/notalpha/dashloader/client/model/ModelModule.java new file mode 100644 index 00000000..bd4f706d --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/model/ModelModule.java @@ -0,0 +1,42 @@ +package dev.notalpha.dashloader.client.model; + +import dev.notalpha.dashloader.api.DashModule; +import dev.notalpha.dashloader.api.cache.Cache; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.config.ConfigHandler; +import dev.notalpha.dashloader.config.Option; +import dev.notalpha.taski.builtin.StepTask; + +public class ModelModule implements DashModule { + @Override + public void reset(Cache cache) { + } + + @Override + public Data save(RegistryWriter factory, StepTask task) { + return new Data(); + } + + @Override + public void load(Data data, RegistryReader reader, StepTask task) { + } + + @Override + public Class getDataClass() { + return Data.class; + } + + @Override + public float taskWeight() { + return 1000; + } + + @Override + public boolean isActive() { + return ConfigHandler.optionActive(Option.CACHE_MODEL_LOADER); + } + + public static final class Data { + } +} diff --git a/src/main/java/dev/notalpha/dashloader/client/model/predicates/BooleanSelector.java b/src/main/java/dev/notalpha/dashloader/client/model/predicates/BooleanSelector.java new file mode 100644 index 00000000..956ab7cd --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/model/predicates/BooleanSelector.java @@ -0,0 +1,26 @@ +package dev.notalpha.dashloader.client.model.predicates; + +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.client.renderer.block.model.multipart.Condition; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.StateHolder; + +import java.util.function.Predicate; + +public class BooleanSelector implements Condition { +public final boolean selector; + +public BooleanSelector(boolean selector) { +this.selector = selector; +} + + public BooleanSelector(Condition selector) { + this.selector = selector instanceof BooleanSelector b && b.selector; + } + + @Override + public > Predicate instantiate(StateDefinition stateFactory) { + return stateHolder -> selector; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/client/model/predicates/DashAndPredicate.java b/src/main/java/dev/notalpha/dashloader/client/model/predicates/DashAndPredicate.java new file mode 100644 index 00000000..0afa092a --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/model/predicates/DashAndPredicate.java @@ -0,0 +1,51 @@ +package dev.notalpha.dashloader.client.model.predicates; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import net.minecraft.client.renderer.block.model.multipart.CombinedCondition; +import net.minecraft.client.renderer.block.model.multipart.Condition; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class DashAndPredicate implements DashObject { +public final int[] selectors; + +public DashAndPredicate(int[] selectors) { +this.selectors = selectors; +} + +public DashAndPredicate(CombinedCondition selector, RegistryWriter writer) { + this.selectors = new int[selector.terms().size()]; + for (int i = 0; i < selector.terms().size(); i++) { + this.selectors[i] = writer.add(selector.terms().get(i)); + } +} + +@Override +public CombinedCondition export(RegistryReader handler) { +final List selectors = new ArrayList<>(this.selectors.length); +for (int accessSelector : this.selectors) { +selectors.add(handler.get(accessSelector)); +} + +return new CombinedCondition(CombinedCondition.Operation.AND, selectors); +} + +@Override +public boolean equals(Object o) { +if (this == o) return true; +if (o == null || getClass() != o.getClass()) return false; + +DashAndPredicate that = (DashAndPredicate) o; + +return Arrays.equals(selectors, that.selectors); +} + +@Override +public int hashCode() { +return Arrays.hashCode(selectors); +} +} diff --git a/src/main/java/dev/notalpha/dashloader/client/model/predicates/DashOrPredicate.java b/src/main/java/dev/notalpha/dashloader/client/model/predicates/DashOrPredicate.java new file mode 100644 index 00000000..0af5a4cc --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/model/predicates/DashOrPredicate.java @@ -0,0 +1,51 @@ +package dev.notalpha.dashloader.client.model.predicates; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import net.minecraft.client.renderer.block.model.multipart.CombinedCondition; +import net.minecraft.client.renderer.block.model.multipart.Condition; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class DashOrPredicate implements DashObject { +public final int[] selectors; + +public DashOrPredicate(int[] selectors) { +this.selectors = selectors; +} + +public DashOrPredicate(CombinedCondition selector, RegistryWriter writer) { + this.selectors = new int[selector.terms().size()]; + for (int i = 0; i < selector.terms().size(); i++) { + this.selectors[i] = writer.add(selector.terms().get(i)); + } +} + +@Override +public CombinedCondition export(RegistryReader handler) { +final List selectors = new ArrayList<>(this.selectors.length); +for (int accessSelector : this.selectors) { +selectors.add(handler.get(accessSelector)); +} + +return new CombinedCondition(CombinedCondition.Operation.OR, selectors); +} + +@Override +public boolean equals(Object o) { +if (this == o) return true; +if (o == null || getClass() != o.getClass()) return false; + +DashOrPredicate that = (DashOrPredicate) o; + +return Arrays.equals(selectors, that.selectors); +} + +@Override +public int hashCode() { +return Arrays.hashCode(selectors); +} +} diff --git a/src/main/java/dev/notalpha/dashloader/client/model/predicates/DashSimplePredicate.java b/src/main/java/dev/notalpha/dashloader/client/model/predicates/DashSimplePredicate.java new file mode 100644 index 00000000..9fa09768 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/model/predicates/DashSimplePredicate.java @@ -0,0 +1,47 @@ +package dev.notalpha.dashloader.client.model.predicates; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.collection.ObjectObjectList; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import net.minecraft.client.renderer.block.model.multipart.KeyValueCondition; + +import java.util.HashMap; +import java.util.Map; + +public final class DashSimplePredicate implements DashObject { +public final ObjectObjectList tests; + +public DashSimplePredicate(ObjectObjectList tests) { +this.tests = tests; +} + +public DashSimplePredicate(KeyValueCondition simpleMultipartModelSelector) { +this.tests = new ObjectObjectList<>(); +simpleMultipartModelSelector.tests().forEach((key, value) -> this.tests.put(key, value.toString())); +} + +@Override +public KeyValueCondition export(RegistryReader handler) { +Map out = new HashMap<>(this.tests.list().size()); +this.tests.forEach((key, value) -> out.put( + key, + KeyValueCondition.Terms.parse(value).result().orElseThrow(() -> new IllegalStateException("Invalid key-value condition term: " + value)) +)); +return new KeyValueCondition(out); +} + +@Override +public boolean equals(Object o) { +if (this == o) return true; +if (o == null || getClass() != o.getClass()) return false; + +DashSimplePredicate that = (DashSimplePredicate) o; + +return tests.equals(that.tests); +} + +@Override +public int hashCode() { +return tests.hashCode(); +} +} diff --git a/src/main/java/dev/notalpha/dashloader/client/model/predicates/DashStaticPredicate.java b/src/main/java/dev/notalpha/dashloader/client/model/predicates/DashStaticPredicate.java new file mode 100644 index 00000000..7803d11c --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/model/predicates/DashStaticPredicate.java @@ -0,0 +1,36 @@ +package dev.notalpha.dashloader.client.model.predicates; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryReader; + +public final class DashStaticPredicate implements DashObject { +public final boolean value; + +public DashStaticPredicate(boolean value) { +this.value = value; +} + +public DashStaticPredicate(BooleanSelector multipartModelSelector) { +this.value = multipartModelSelector.selector; +} + +@Override +public BooleanSelector export(RegistryReader exportHandler) { +return new BooleanSelector(value); +} + +@Override +public boolean equals(Object o) { +if (this == o) return true; +if (o == null || getClass() != o.getClass()) return false; + +DashStaticPredicate that = (DashStaticPredicate) o; + +return value == that.value; +} + +@Override +public int hashCode() { +return (value ? 1 : 0); +} +} diff --git a/src/main/java/dev/notalpha/dashloader/client/shader/ShaderModule.java b/src/main/java/dev/notalpha/dashloader/client/shader/ShaderModule.java new file mode 100644 index 00000000..7878bae4 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/shader/ShaderModule.java @@ -0,0 +1,37 @@ +package dev.notalpha.dashloader.client.shader; + +import dev.notalpha.dashloader.api.DashModule; +import dev.notalpha.dashloader.api.cache.Cache; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.config.ConfigHandler; +import dev.notalpha.dashloader.config.Option; +import dev.notalpha.taski.builtin.StepTask; + +public class ShaderModule implements DashModule { + @Override + public void reset(Cache cache) { + } + + @Override + public Data save(RegistryWriter factory, StepTask task) { + return new Data(); + } + + @Override + public void load(Data data, RegistryReader reader, StepTask task) { + } + + @Override + public Class getDataClass() { + return Data.class; + } + + @Override + public boolean isActive() { + return ConfigHandler.optionActive(Option.CACHE_SHADER); + } + + public static final class Data { + } +} diff --git a/src/main/java/dev/notalpha/dashloader/client/splash/SplashModule.java b/src/main/java/dev/notalpha/dashloader/client/splash/SplashModule.java new file mode 100644 index 00000000..51f91104 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/client/splash/SplashModule.java @@ -0,0 +1,56 @@ +package dev.notalpha.dashloader.client.splash; + +import dev.notalpha.dashloader.api.CachingData; +import dev.notalpha.dashloader.api.DashModule; +import dev.notalpha.dashloader.api.cache.Cache; +import dev.notalpha.dashloader.api.cache.CacheStatus; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.config.ConfigHandler; +import dev.notalpha.dashloader.config.Option; +import dev.notalpha.taski.builtin.StepTask; + +import java.util.ArrayList; +import java.util.List; + +public class SplashModule implements DashModule { + public static final CachingData> TEXTS = new CachingData<>(); + + @Override + public void reset(Cache cache) { + TEXTS.reset(cache, new ArrayList<>()); + } + + @Override + public Data save(RegistryWriter writer, StepTask task) { + return new Data(TEXTS.get(CacheStatus.SAVE)); + } + + @Override + public void load(Data data, RegistryReader reader, StepTask task) { + TEXTS.set(CacheStatus.LOAD, data.splashList); + } + + @Override + public Class getDataClass() { + return SplashModule.Data.class; + } + + @Override + public boolean isActive() { + return ConfigHandler.optionActive(Option.CACHE_SPLASH_TEXT); + } + + @Override + public float taskWeight() { + return 1; + } + + public static final class Data { + public final List splashList; + + public Data(List splashList) { + this.splashList = splashList; + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/config/Config.java b/src/main/java/dev/notalpha/dashloader/config/Config.java new file mode 100644 index 00000000..27476949 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/config/Config.java @@ -0,0 +1,17 @@ +package dev.notalpha.dashloader.config; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("CanBeFinal") +public class Config { + public Map options = new LinkedHashMap<>(); + public byte compression = 3; + public int maxCaches = 5; + public List customSplashLines = new ArrayList<>(); + public boolean addDefaultSplashLines = true; + public boolean singleThreadedReading = false; + public boolean showCachingToast = true; +} diff --git a/src/main/java/dev/notalpha/dashloader/config/ConfigHandler.java b/src/main/java/dev/notalpha/dashloader/config/ConfigHandler.java new file mode 100644 index 00000000..15aa5348 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/config/ConfigHandler.java @@ -0,0 +1,112 @@ +package dev.notalpha.dashloader.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import dev.notalpha.dashloader.DashLoader; +import net.fabricmc.loader.api.FabricLoader; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.EnumMap; + +public class ConfigHandler { + private static final EnumMap OPTION_ACTIVE = new EnumMap<>(Option.class); + private static final String DISABLE_OPTION_TAG = "dashloader:disableoption"; + + static { + for (Option value : Option.values()) { + OPTION_ACTIVE.put(value, true); + } + } + + public static final ConfigHandler INSTANCE = new ConfigHandler(FabricLoader.getInstance().getConfigDir().normalize().resolve("dashloader.json")); + + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + private final Path configPath; + public Config config = new Config(); + + public ConfigHandler(Path configPath) { + this.configPath = configPath; + this.reloadConfig(); + this.config.options.forEach((s, aBoolean) -> { + try { + var option = Option.valueOf(s.toUpperCase()); + OPTION_ACTIVE.put(option, aBoolean); + if (!aBoolean) { + DashLoader.LOG.warn("Disabled Optional Feature {} from DashLoader config.", s); + } + } catch (IllegalArgumentException illegalArgumentException) { + DashLoader.LOG.error("Could not disable Optional Feature {} from DashLoader config as it does not exist.", s); + } + }); + + for (var modContainer : FabricLoader.getInstance().getAllMods()) { + var mod = modContainer.getMetadata(); + if (mod.containsCustomValue(DISABLE_OPTION_TAG)) { + for (var value : mod.getCustomValue(DISABLE_OPTION_TAG).getAsArray()) { + final String feature = value.getAsString(); + try { + var option = Option.valueOf(feature.toUpperCase()); + OPTION_ACTIVE.put(option, false); + DashLoader.LOG.warn("Disabled Optional Feature {} from {} config. {}", feature, mod.getId(), mod.getName()); + } catch (IllegalArgumentException illegalArgumentException) { + DashLoader.LOG.error("Could not disable Optional Feature {} from {} config as it does not exist. {}", feature, mod.getId(), mod.getName()); + } + } + } + } + if (isVulkanModPresent()) { + for (Option option : new Option[]{Option.CACHE_SHADER, Option.UNSAFE_MIPMAP_GENERATION, Option.CACHE_ATLASES}) { + OPTION_ACTIVE.put(option, false); + DashLoader.LOG.warn("Found VulkanMod, Disabling Optional Feature {}", option.name()); + } + } + } + + public static boolean shouldApplyMixin(String name) { + for (Option value : Option.values()) { + if (name.contains(value.mixinContains)) { + return OPTION_ACTIVE.get(value); + } + } + return true; + } + + public static boolean optionActive(Option option) { + return OPTION_ACTIVE.get(option); + } + + public void reloadConfig() { + try { + if (Files.exists(this.configPath)) { + final BufferedReader json = Files.newBufferedReader(this.configPath); + this.config = this.gson.fromJson(json, Config.class); + json.close(); + } + } catch (Throwable err) { + DashLoader.LOG.info("Config corrupted creating a new one.", err); + } + + this.saveConfig(); + } + + public void saveConfig() { + try { + Files.createDirectories(this.configPath.getParent()); + Files.deleteIfExists(this.configPath); + final BufferedWriter writer = Files.newBufferedWriter(this.configPath, StandardOpenOption.CREATE); + this.gson.toJson(this.config, writer); + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static boolean isVulkanModPresent() { + return FabricLoader.getInstance().isModLoaded("vulkanmod"); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/config/Option.java b/src/main/java/dev/notalpha/dashloader/config/Option.java new file mode 100644 index 00000000..8a849843 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/config/Option.java @@ -0,0 +1,21 @@ +package dev.notalpha.dashloader.config; + +public enum Option { + CACHE_ATLASES("cache.SpriteAtlasTextureMixin"), // Caches stitched texture atlases, significantly reducing GPU upload time + CACHE_FONT("cache.font"), // Caches fonts and their images. + CACHE_MODEL_LOADER("cache.model"), // Caches BakedModels which allows the game to load extremely fast + CACHE_SHADER("cache.shader"), // Caches the GL Shaders + CACHE_SPLASH_TEXT("cache.SplashTextResourceSupplierMixin"), // Caches the splash texts from the main screen + CACHE_SPRITE_CONTENT("cache.sprite.content"), // Caches sprite loading + CACHE_SPRITE_STITCHING("cache.sprite.stitch"), // Caches sprite stitching + + FAST_MODEL_IDENTIFIER_EQUALS("misc.ModelIdentifierMixin"), // Use a much faster .equals() on ModelIdentifiers + FAST_WALL_BLOCK("WallBlockMixin"), // Caches the two most common blockstates for wall blocks + UNSAFE_MIPMAP_GENERATION("misc.MipmapGenerator"); // Speeds up get/set pixel operations when generating mipmaps by skipping redundant safety checks + + public final String mixinContains; + + Option(String mixinContains) { + this.mixinContains = mixinContains; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/IOHelper.java b/src/main/java/dev/notalpha/dashloader/io/IOHelper.java new file mode 100644 index 00000000..8817b20d --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/IOHelper.java @@ -0,0 +1,151 @@ +package dev.notalpha.dashloader.io; + +import com.github.luben.zstd.Zstd; +import dev.notalpha.hyphen.io.ByteBufferIO; +import dev.notalpha.taski.builtin.StepTask; +import org.apache.commons.io.IOUtils; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.system.MemoryUtil; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +public final class IOHelper { + public static int[] toArray(IntBuffer buffer) { + if (buffer == null) { + return null; + } + buffer.rewind(); + int[] foo = new int[buffer.remaining()]; + buffer.get(foo); + return foo; + } + + public static float[] toArray(FloatBuffer buffer) { + if (buffer == null) { + return null; + } + + buffer.rewind(); + float[] foo = new float[buffer.remaining()]; + buffer.get(foo); + return foo; + } + + public static byte[] toArray(ByteBuffer buffer) { + if (buffer == null) { + return null; + } + + buffer.rewind(); + byte[] foo = new byte[buffer.remaining()]; + buffer.get(foo); + return foo; + } + + public static IntBuffer fromArray(int[] arr) { + if (arr == null) { + return null; + } + + var buffer = MemoryUtil.memAllocInt(arr.length); + buffer.put(arr); + buffer.rewind(); + return buffer; + } + + public static FloatBuffer fromArray(float[] arr) { + if (arr == null) { + return null; + } + + var buffer = MemoryUtil.memAllocFloat(arr.length); + buffer.put(arr); + buffer.rewind(); + return buffer; + } + + public static void save(Path path, StepTask task, ByteBufferIO io, int fileSize, byte compressionLevel) throws IOException { + io.rewind(); + io.byteBuffer.limit(fileSize); + try (FileChannel channel = createFile(path)) { + if (compressionLevel > 0) { + task.reset(4); + // Allocate + final long maxSize = Zstd.compressBound(fileSize); + final var dst = ByteBufferIO.createDirect((int) maxSize); + task.next(); + + // Compress + final long size = Zstd.compress(dst.byteBuffer, io.byteBuffer, compressionLevel); + task.next(); + + // Write + dst.rewind(); + dst.byteBuffer.limit((int) size); + final var map = channel.map(FileChannel.MapMode.READ_WRITE, 0, size + 5).order(ByteOrder.LITTLE_ENDIAN); + task.next(); + + map.put(compressionLevel); + map.putInt(fileSize); + map.put(dst.byteBuffer); + io.close(); + dst.close(); + } else { + task.reset(2); + final var map = channel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize + 1).order(ByteOrder.LITTLE_ENDIAN); + task.next(); + ByteBufferIO file = ByteBufferIO.wrap(map); + file.putByte(compressionLevel); + file.putByteBuffer(io.byteBuffer, fileSize); + task.next(); + } + } + } + + public static ByteBufferIO load(Path path) throws IOException { + try (FileChannel channel = openFile(path)) { + var buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()).order(ByteOrder.LITTLE_ENDIAN); + // Check compression + if (buffer.get() > 0) { + final int size = buffer.getInt(); + final var dst = ByteBufferIO.createDirect(size); + Zstd.decompress(dst.byteBuffer, buffer); + dst.rewind(); + return dst; + } else { + return ByteBufferIO.wrap(buffer); + } + } + } + + public static FileChannel createFile(Path path) throws IOException { + Files.createDirectories(path.getParent()); + Files.deleteIfExists(path); + return FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ); + } + + public static FileChannel openFile(Path path) throws IOException { + return FileChannel.open(path, StandardOpenOption.READ); + } + + public static byte[] streamToArray(InputStream inputStream) throws IOException { + final ByteArrayOutputStream output = new ByteArrayOutputStream() { + @Override + public synchronized byte @NotNull [] toByteArray() { + return this.buf; + } + }; + IOUtils.copy(inputStream, output); + return output.toByteArray(); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/MappingSerializer.java b/src/main/java/dev/notalpha/dashloader/io/MappingSerializer.java new file mode 100644 index 00000000..6abe0742 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/MappingSerializer.java @@ -0,0 +1,120 @@ +package dev.notalpha.dashloader.io; + +import dev.notalpha.dashloader.DashLoader; +import dev.notalpha.dashloader.api.DashModule; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.config.ConfigHandler; +import dev.notalpha.hyphen.io.ByteBufferIO; +import dev.notalpha.taski.Task; +import dev.notalpha.taski.builtin.StepTask; +import dev.notalpha.taski.builtin.WeightedStageTask; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +public class MappingSerializer { + private final Object2ObjectMap, Serializer> serializers; + + public MappingSerializer(List> cacheHandlers) { + this.serializers = new Object2ObjectOpenHashMap<>(); + + cacheHandlers.forEach(handler -> { + Class dataClass = handler.getDataClass(); + this.serializers.put(dataClass, new Serializer<>(dataClass)); + }); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public void save(Path dir, RegistryWriter factory, List> handlers, StepTask parent) { + List tasks = new ArrayList<>(); + for (DashModule value : handlers) { + tasks.add(new WeightedStageTask.WeightedStage(value.taskWeight(), new StepTask(value.getDataClass().getSimpleName(), 1))); + } + WeightedStageTask stageTask = new WeightedStageTask("Mapping", tasks); + parent.setSubTask(stageTask); + + List objects = new ArrayList<>(); + int i = 0; + for (DashModule handler : handlers) { + Task task = stageTask.getStages().get(i).task; + if (handler.isActive()) { + Object object = handler.save(factory, (StepTask) task); + Class dataClass = handler.getDataClass(); + if (object.getClass() != dataClass) { + throw new RuntimeException("Handler DataClass does not match the output of saveMappings on " + handler.getClass()); + } + objects.add(object); + } else { + objects.add(null); + } + //noinspection DataFlowIssue + task.finish(); + i++; + } + + Path path = dir.resolve("mapping.bin"); + + int measure = 0; + for (Object object : objects) { + measure += 1; + if (object != null) { + Class aClass = object.getClass(); + Serializer serializer = this.serializers.get(aClass); + + if (serializer == null) { + throw new RuntimeException("Could not find mapping serializer for " + aClass); + } + + measure += serializer.measure(object); + } + } + + ByteBufferIO io = ByteBufferIO.createDirect(measure); + for (Object object : objects) { + if (object == null) { + io.putByte((byte) 0); + } else { + io.putByte((byte) 1); + Serializer serializer = this.serializers.get(object.getClass()); + serializer.put(io, object); + } + } + + try { + io.rewind(); + IOHelper.save(path, new StepTask(""), io, measure, ConfigHandler.INSTANCE.config.compression); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public boolean load(Path dir, RegistryReader reader, List> handlers) { + try { + ByteBufferIO io = IOHelper.load(dir.resolve("mapping.bin")); + for (DashModule handler : handlers) { + if (io.getByte() == 1) { + Class dataClass = handler.getDataClass(); + Serializer serializer = this.serializers.get(dataClass); + Object object = serializer.get(io); + + if (handler.isActive()) { + handler.load(object, reader, new StepTask("")); + } + } else if (handler.isActive()) { + DashLoader.LOG.info("Recaching as {} is now active.", handler.getClass().getSimpleName()); + return false; + } + } + + return true; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/RegistrySerializer.java b/src/main/java/dev/notalpha/dashloader/io/RegistrySerializer.java new file mode 100644 index 00000000..dfedf7ab --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/RegistrySerializer.java @@ -0,0 +1,218 @@ +package dev.notalpha.dashloader.io; + +import dev.notalpha.dashloader.DashLoader; +import dev.notalpha.dashloader.DashObjectClass; +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.config.ConfigHandler; +import dev.notalpha.dashloader.io.data.CacheInfo; +import dev.notalpha.dashloader.io.data.ChunkInfo; +import dev.notalpha.dashloader.io.data.fragment.CacheFragment; +import dev.notalpha.dashloader.io.data.fragment.ChunkFragment; +import dev.notalpha.dashloader.io.data.fragment.StageFragment; +import dev.notalpha.dashloader.io.fragment.Fragment; +import dev.notalpha.dashloader.io.fragment.SimplePiece; +import dev.notalpha.dashloader.io.fragment.SizePiece; +import dev.notalpha.dashloader.registry.RegistryWriterImpl; +import dev.notalpha.dashloader.registry.data.ChunkData; +import dev.notalpha.dashloader.registry.data.ChunkFactory; +import dev.notalpha.dashloader.registry.data.StageData; +import dev.notalpha.dashloader.thread.ThreadHandler; +import dev.notalpha.hyphen.io.ByteBufferIO; +import dev.notalpha.taski.Task; +import dev.notalpha.taski.builtin.StepTask; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public class RegistrySerializer { + // 20MB + private static final int MIN_PER_THREAD_FRAGMENT_SIZE = 1024 * 1024 * 20; + // 1GB + private static final int MAX_FRAGMENT_SIZE = 1024 * 1024 * 1024; + private final Object2ObjectMap, Serializer> serializers; + + public RegistrySerializer(List> dashObjects) { + this.serializers = new Object2ObjectOpenHashMap<>(); + for (DashObjectClass dashObject : dashObjects) { + Class dashClass = dashObject.getDashClass(); + this.serializers.put(dashClass, new Serializer<>(dashClass)); + } + } + + public > Serializer getSerializer(DashObjectClass dashObject) { + return (Serializer) this.serializers.get(dashObject.getDashClass()); + } + + public CacheInfo serialize(Path dir, RegistryWriterImpl factory, Consumer taskConsumer) throws IOException { + StageData[] stages = factory.export(); + + SimplePiece[] value = new SimplePiece[stages.length]; + for (int i = 0; i < stages.length; i++) { + StageData stage = stages[i]; + SimplePiece[] value2 = new SimplePiece[stage.chunks.length]; + for (int i1 = 0; i1 < stage.chunks.length; i1++) { + ChunkData chunk = stage.chunks[i1]; + Serializer serializer = getSerializer(chunk.dashObject); + SizePiece[] value3 = new SizePiece[chunk.dashables.length]; + for (int i2 = 0; i2 < chunk.dashables.length; i2++) { + value3[i2] = new SizePiece(serializer.measure(chunk.dashables[i2].data) + 4); + } + + value2[i1] = new SimplePiece(value3); + } + + value[i] = new SimplePiece(value2); + } + SimplePiece piece = new SimplePiece(value); + + int[][] stageSizes = new int[stages.length][]; + for (int i = 0; i < stages.length; i++) { + StageData stage = stages[i]; + int[] chunkSizes = new int[stage.chunks.length]; + for (int i1 = 0; i1 < stage.chunks.length; i1++) { + chunkSizes[i1] = stage.chunks[i1].dashables.length; + } + stageSizes[i] = chunkSizes; + } + + // Calculate amount of fragments required + int minFragments = (int) (piece.size / MAX_FRAGMENT_SIZE); + int maxFragments = (int) (piece.size / MIN_PER_THREAD_FRAGMENT_SIZE); + int fragmentCount = Integer.max(Integer.max(Integer.min(ThreadHandler.THREADS, maxFragments), minFragments), 1); + long remainingSize = piece.size; + + List fragments = new ArrayList<>(); + for (int i = 0; i < fragmentCount; i++) { + long fragmentSize = remainingSize / (fragmentCount - i); + if (i == fragmentCount - 1) { + fragmentSize = Long.MAX_VALUE; + } + Fragment fragment = piece.fragment(fragmentSize); + remainingSize -= fragment.size; + fragments.add(new CacheFragment(fragment)); + } + + StepTask task = new StepTask("fragment", fragments.size() * 2); + taskConsumer.accept(task); + // Serialize + for (int k = 0; k < fragments.size(); k++) { + DashLoader.LOG.info("Serializing fragment {}", k); + CacheFragment fragment = fragments.get(k); + List stageFragmentMetadata = fragment.stages; + ByteBufferIO io = ByteBufferIO.createDirect((int) fragment.info.fileSize); + + int taskSize = 0; + for (var stage : stageFragmentMetadata) { + for (var chunk : stage.chunks) { + taskSize += chunk.info.rangeEnd - chunk.info.rangeStart; + } + } + + StepTask stageTask = new StepTask("stage", taskSize); + task.setSubTask(stageTask); + for (int i = 0; i < stageFragmentMetadata.size(); i++) { + StageFragment stage = stageFragmentMetadata.get(i); + StageData data = stages[i + fragment.info.rangeStart]; + + List chunks = stage.chunks; + for (int j = 0; j < chunks.size(); j++) { + ChunkFragment chunk = chunks.get(j); + ChunkData chunkData = data.chunks[j + stage.info.rangeStart]; + Serializer serializer = serializers.get(chunkData.dashObject.getDashClass()); + for (int i1 = chunk.info.rangeStart; i1 < chunk.info.rangeEnd; i1++) { + ChunkData.Entry dashable = chunkData.dashables[i1]; + io.putInt(dashable.pos); + serializer.put(io, dashable.data); + stageTask.next(); + } + } + } + task.next(); + + StepTask serializingTask = new StepTask("Serializing"); + task.setSubTask(serializingTask); + + int fileSize = (int) fragment.info.fileSize; + IOHelper.save(fragmentFilePath(dir, k), serializingTask, io, fileSize, ConfigHandler.INSTANCE.config.compression); + task.next(); + } + + List chunks = new ArrayList<>(); + for (ChunkFactory chunk : factory.chunks) { + chunks.add(new ChunkInfo(chunk)); + } + + return new CacheInfo(fragments, chunks, stageSizes); + } + + public StageData[] deserialize(Path dir, CacheInfo metadata, List> objects) { + StageData[] out = new StageData[metadata.stageSizes.length]; + for (int i = 0; i < metadata.stageSizes.length; i++) { + int[] chunkSizes = metadata.stageSizes[i]; + ChunkData[] chunks = new ChunkData[chunkSizes.length]; + for (int j = 0; j < chunks.length; j++) { + ChunkInfo chunkInfo = metadata.chunks.get(j); + chunks[j] = new ChunkData( + (byte) j, + chunkInfo.name, + objects.get(chunkInfo.dashObjectId), + new ChunkData.Entry[chunkSizes[j]] + ); + } + + out[i] = new StageData(chunks); + } + + List fragments = metadata.fragments; + List runnables = new ArrayList<>(); + for (int j = 0; j < fragments.size(); j++) { + CacheFragment fragment = fragments.get(j); + int finalJ = j; + runnables.add(() -> { + try { + ByteBufferIO io = IOHelper.load(fragmentFilePath(dir, finalJ)); + deserialize(out, io, fragment); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + if (ConfigHandler.INSTANCE.config.singleThreadedReading) { + for (Runnable runnable : runnables) { + runnable.run(); + } + } else { + ThreadHandler.INSTANCE.parallelRunnable(runnables); + } + + return out; + } + + private void deserialize(StageData[] data, ByteBufferIO io, CacheFragment fragment) { + for (int i = 0; i < fragment.stages.size(); i++) { + StageFragment stageFragment = fragment.stages.get(i); + StageData stage = data[fragment.info.rangeStart + i]; + for (int i1 = 0; i1 < stageFragment.chunks.size(); i1++) { + ChunkFragment chunkFragment = stageFragment.chunks.get(i1); + ChunkData chunkData = stage.chunks[stageFragment.info.rangeStart + i1]; + Serializer serializer = getSerializer(chunkData.dashObject); + for (int i2 = chunkFragment.info.rangeStart; i2 < chunkFragment.info.rangeEnd; i2++) { + int pos = io.getInt(); + Object out = serializer.get(io); + chunkData.dashables[i2] = new ChunkData.Entry<>(out, pos); + } + } + } + } + + private Path fragmentFilePath(Path dir, int fragment) { + return dir.resolve("fragment-" + fragment + ".bin"); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/Serializer.java b/src/main/java/dev/notalpha/dashloader/io/Serializer.java new file mode 100644 index 00000000..00e1f495 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/Serializer.java @@ -0,0 +1,78 @@ +package dev.notalpha.dashloader.io; + +import dev.notalpha.dashloader.config.ConfigHandler; +import dev.notalpha.dashloader.io.def.NativeImageData; +import dev.notalpha.dashloader.io.def.NativeImageDataDef; +import dev.notalpha.dashloader.registry.data.ChunkData; +import dev.notalpha.hyphen.HyphenSerializer; +import dev.notalpha.hyphen.SerializerFactory; +import dev.notalpha.hyphen.io.ByteBufferIO; +import dev.notalpha.hyphen.scan.annotations.DataSubclasses; +import dev.notalpha.taski.builtin.StepTask; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.nio.file.Path; + +public class Serializer { + private final HyphenSerializer serializer; + + public Serializer(Class aClass) { + var factory = SerializerFactory.createDebug(ByteBufferIO.class, aClass); + factory.addAnnotationProvider(ChunkData.class, new DataSubclasses() { + @Override + public Class annotationType() { + return DataSubclasses.class; + } + + @Override + public Class[] value() { + return new Class[]{ChunkData.class}; + } + }); + factory.setClassName(getSerializerClassName(aClass)); + + factory.addDynamicDef(NativeImageData.class, NativeImageDataDef::new); + this.serializer = factory.build(); + } + + @NotNull + private static String getSerializerClassName(Class holderClass) { + return holderClass.getSimpleName().toLowerCase() + "-serializer"; + } + + public O get(ByteBufferIO io) { + return this.serializer.get(io); + } + + public void put(ByteBufferIO io, O data) { + this.serializer.put(io, data); + } + + public long measure(O data) { + return this.serializer.measure(data); + } + + public void save(Path path, StepTask task, O data) { + var measure = (int) this.serializer.measure(data); + var io = ByteBufferIO.createDirect(measure); + this.serializer.put(io, data); + io.rewind(); + try { + + IOHelper.save(path, task, io, measure, ConfigHandler.INSTANCE.config.compression); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public O load(Path path) { + try { + ByteBufferIO io = IOHelper.load(path); + return this.serializer.get(io); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/data/CacheInfo.java b/src/main/java/dev/notalpha/dashloader/io/data/CacheInfo.java new file mode 100644 index 00000000..b0d508c6 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/data/CacheInfo.java @@ -0,0 +1,28 @@ +package dev.notalpha.dashloader.io.data; + +import dev.notalpha.dashloader.io.data.fragment.CacheFragment; + +import java.util.List; + +public class CacheInfo { + /** + * Information about the different file fragments the cache contains. + */ + public final List fragments; + /** + * Information about the output chunks. + */ + public final List chunks; + /** + * A two-dimensional array containing the sizes of the stages and chunks. + * The first index is the stage index which will yield an array of the chunk sizes, + * The size of this array is the amount of chunks in that stage. + */ + public final int[][] stageSizes; + + public CacheInfo(List fragments, List chunks, int[][] stageSizes) { + this.fragments = fragments; + this.chunks = chunks; + this.stageSizes = stageSizes; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/data/ChunkInfo.java b/src/main/java/dev/notalpha/dashloader/io/data/ChunkInfo.java new file mode 100644 index 00000000..63dc08e7 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/data/ChunkInfo.java @@ -0,0 +1,21 @@ +package dev.notalpha.dashloader.io.data; + +import dev.notalpha.dashloader.registry.data.ChunkFactory; + +public class ChunkInfo { + public final int dashObjectId; + public final int size; + public final String name; + + public ChunkInfo(int dashObjectId, int size, String name) { + this.dashObjectId = dashObjectId; + this.size = size; + this.name = name; + } + + public ChunkInfo(ChunkFactory chunk) { + this.dashObjectId = chunk.dashObject.getDashObjectId(); + this.size = chunk.list.size(); + this.name = chunk.name; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/data/fragment/CacheFragment.java b/src/main/java/dev/notalpha/dashloader/io/data/fragment/CacheFragment.java new file mode 100644 index 00000000..03628dae --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/data/fragment/CacheFragment.java @@ -0,0 +1,24 @@ +package dev.notalpha.dashloader.io.data.fragment; + +import dev.notalpha.dashloader.io.fragment.Fragment; + +import java.util.ArrayList; +import java.util.List; + +public class CacheFragment { + public final List stages; + public final FragmentSlice info; + + public CacheFragment(List stages, FragmentSlice info) { + this.stages = stages; + this.info = info; + } + + public CacheFragment(Fragment fragment) { + this.info = new FragmentSlice(fragment); + this.stages = new ArrayList<>(); + for (Fragment inner : fragment.inner) { + this.stages.add(new StageFragment(inner)); + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/data/fragment/ChunkFragment.java b/src/main/java/dev/notalpha/dashloader/io/data/fragment/ChunkFragment.java new file mode 100644 index 00000000..46b46e2d --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/data/fragment/ChunkFragment.java @@ -0,0 +1,15 @@ +package dev.notalpha.dashloader.io.data.fragment; + +import dev.notalpha.dashloader.io.fragment.Fragment; + +public final class ChunkFragment { + public final FragmentSlice info; + + public ChunkFragment(FragmentSlice info) { + this.info = info; + } + + public ChunkFragment(Fragment fragment) { + this.info = new FragmentSlice(fragment); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/data/fragment/FragmentSlice.java b/src/main/java/dev/notalpha/dashloader/io/data/fragment/FragmentSlice.java new file mode 100644 index 00000000..1c8ddcab --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/data/fragment/FragmentSlice.java @@ -0,0 +1,21 @@ +package dev.notalpha.dashloader.io.data.fragment; + +import dev.notalpha.dashloader.io.fragment.Fragment; + +public class FragmentSlice { + public final int rangeStart; + public final int rangeEnd; + public final long fileSize; + + public FragmentSlice(int rangeStart, int rangeEnd, long fileSize) { + this.rangeStart = rangeStart; + this.rangeEnd = rangeEnd; + this.fileSize = fileSize; + } + + public FragmentSlice(Fragment fragment) { + this.rangeStart = fragment.startIndex; + this.rangeEnd = fragment.endIndex; + this.fileSize = fragment.size; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/data/fragment/StageFragment.java b/src/main/java/dev/notalpha/dashloader/io/data/fragment/StageFragment.java new file mode 100644 index 00000000..110bd111 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/data/fragment/StageFragment.java @@ -0,0 +1,24 @@ +package dev.notalpha.dashloader.io.data.fragment; + +import dev.notalpha.dashloader.io.fragment.Fragment; + +import java.util.ArrayList; +import java.util.List; + +public class StageFragment { + public final List chunks; + public final FragmentSlice info; + + public StageFragment(List chunks, FragmentSlice info) { + this.chunks = chunks; + this.info = info; + } + + public StageFragment(Fragment fragment) { + this.info = new FragmentSlice(fragment); + this.chunks = new ArrayList<>(); + for (Fragment inner : fragment.inner) { + this.chunks.add(new ChunkFragment(inner)); + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/def/DataUnsafeByteBuffer.java b/src/main/java/dev/notalpha/dashloader/io/def/DataUnsafeByteBuffer.java new file mode 100644 index 00000000..28f269b4 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/def/DataUnsafeByteBuffer.java @@ -0,0 +1,14 @@ +package dev.notalpha.dashloader.io.def; + +import dev.notalpha.hyphen.scan.annotations.HyphenAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@HyphenAnnotation +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE}) +public @interface DataUnsafeByteBuffer { +} diff --git a/src/main/java/dev/notalpha/dashloader/io/def/NativeImageData.java b/src/main/java/dev/notalpha/dashloader/io/def/NativeImageData.java new file mode 100644 index 00000000..a19721f2 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/def/NativeImageData.java @@ -0,0 +1,13 @@ +package dev.notalpha.dashloader.io.def; + +import java.nio.ByteBuffer; + +public class NativeImageData { + public final ByteBuffer buffer; + public final boolean stb; + + public NativeImageData(ByteBuffer buffer, boolean stb) { + this.buffer = buffer; + this.stb = stb; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/def/NativeImageDataDef.java b/src/main/java/dev/notalpha/dashloader/io/def/NativeImageDataDef.java new file mode 100644 index 00000000..d864a507 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/def/NativeImageDataDef.java @@ -0,0 +1,93 @@ +package dev.notalpha.dashloader.io.def; + +import dev.notalpha.hyphen.SerializerGenerator; +import dev.notalpha.hyphen.codegen.MethodWriter; +import dev.notalpha.hyphen.codegen.Variable; +import dev.notalpha.hyphen.codegen.def.BufferDef; +import dev.notalpha.hyphen.codegen.def.MethodDef; +import dev.notalpha.hyphen.codegen.statement.IfElse; +import dev.notalpha.hyphen.scan.struct.ClassStruct; +import dev.notalpha.hyphen.scan.struct.Struct; +import org.lwjgl.system.MemoryUtil; +import org.objectweb.asm.Opcodes; + +import java.nio.ByteBuffer; + +public class NativeImageDataDef extends MethodDef { + private ByteBufferDef bytebufferDef; + + public NativeImageDataDef(Struct clazz) { + super(clazz); + } + + @Override + public void scan(SerializerGenerator handler) { + this.bytebufferDef = new ByteBufferDef(new ClassStruct(ByteBuffer.class)); + this.bytebufferDef.scan(handler); + super.scan(handler); + } + + @Override + protected void writeMethodPut(MethodWriter mh, Runnable valueLoad) { + mh.loadIO(); + valueLoad.run(); + mh.visitFieldInsn(Opcodes.GETFIELD, NativeImageData.class, "stb", boolean.class); + mh.putIO(boolean.class); + + bytebufferDef.writePut(mh, () -> { + valueLoad.run(); + mh.visitFieldInsn(Opcodes.GETFIELD, NativeImageData.class, "buffer", ByteBuffer.class); + }); + } + + @Override + protected void writeMethodGet(MethodWriter mh) { + mh.typeOp(Opcodes.NEW, NativeImageData.class); + mh.op(Opcodes.DUP); + + mh.loadIO(); + mh.getIO(boolean.class); + + mh.op(Opcodes.DUP); + Variable stb = mh.addVar("stb", boolean.class); + mh.varOp(Opcodes.ISTORE, stb); + + bytebufferDef.stbVariable = stb; + bytebufferDef.writeGet(mh); + + mh.op(Opcodes.SWAP); + mh.callInst(Opcodes.INVOKESPECIAL, NativeImageData.class, "", Void.TYPE, ByteBuffer.class, boolean.class); + } + + @Override + protected void writeMethodMeasure(MethodWriter mh, Runnable valueLoad) { + bytebufferDef.writeMeasure(mh, () -> { + valueLoad.run(); + mh.visitFieldInsn(Opcodes.GETFIELD, NativeImageData.class, "buffer", ByteBuffer.class); + }); + } + + @Override + public long getStaticSize() { + return bytebufferDef.getStaticSize() + 1; + } + + private static class ByteBufferDef extends BufferDef { + private Variable stbVariable; + + public ByteBufferDef(Struct clazz) { + super(clazz); + } + + @Override + protected void allocateBuffer(MethodWriter mh) { + mh.varOp(Opcodes.ILOAD, stbVariable); + try (var thing = new IfElse(mh, Opcodes.IFEQ)) { + mh.op(Opcodes.ICONST_1, Opcodes.SWAP); + mh.callInst(Opcodes.INVOKESTATIC, MemoryUtil.class, "memCalloc", ByteBuffer.class, int.class, int.class); + thing.elseEnd(); + mh.callInst(Opcodes.INVOKESTATIC, MemoryUtil.class, "memAlloc", ByteBuffer.class, int.class); + } + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/fragment/Fragment.java b/src/main/java/dev/notalpha/dashloader/io/fragment/Fragment.java new file mode 100644 index 00000000..edc2ed65 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/fragment/Fragment.java @@ -0,0 +1,27 @@ +package dev.notalpha.dashloader.io.fragment; + +import java.util.List; + +public class Fragment { + public final long size; + public final int startIndex; + public final int endIndex; + public final List inner; + + public Fragment(long size, int startIndex, int endIndex, List inner) { + this.size = size; + this.startIndex = startIndex; + this.endIndex = endIndex; + this.inner = inner; + } + + @Override + public String toString() { + return "Fragment{" + + "size=" + size + + ", startIndex=" + startIndex + + ", endIndex=" + endIndex + + ", inner=" + inner + + '}'; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/fragment/Piece.java b/src/main/java/dev/notalpha/dashloader/io/fragment/Piece.java new file mode 100644 index 00000000..5d25061f --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/fragment/Piece.java @@ -0,0 +1,50 @@ +package dev.notalpha.dashloader.io.fragment; + +import java.util.ArrayList; +import java.util.List; + +public abstract class Piece { + public final long size; + int elementPos = 0; + + protected Piece(long size) { + this.size = size; + } + + public abstract Piece[] getInner(); + + public boolean isDone() { + Piece[] inner = this.getInner(); + return inner == null || !(elementPos < inner.length); + } + + public Fragment fragment(long sizeRemaining) { + Piece[] inner = this.getInner(); + if (inner == null) { + throw new RuntimeException("Non splitting piece requested fragmentation"); + } else { + int rangeStart = elementPos; + long currentSize = 0; + + List innerOut = new ArrayList<>(); + int rangeEnd = 0; + // Add until we reach the intended size, or we hit the last element. + while ((currentSize < sizeRemaining) && elementPos < inner.length) { + var piece = inner[elementPos]; + rangeEnd = elementPos + 1; + if (piece.getInner() == null) { + currentSize += piece.size; + elementPos += 1; + } else { + Fragment fragment = piece.fragment(sizeRemaining); + innerOut.add(fragment); + currentSize += fragment.size; + if (piece.isDone()) { + elementPos += 1; + } + } + } + return new Fragment(currentSize, rangeStart, rangeEnd, innerOut); + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/fragment/SimplePiece.java b/src/main/java/dev/notalpha/dashloader/io/fragment/SimplePiece.java new file mode 100644 index 00000000..4b612b29 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/fragment/SimplePiece.java @@ -0,0 +1,22 @@ +package dev.notalpha.dashloader.io.fragment; + +import java.util.Arrays; + +public class SimplePiece extends Piece { + public final Piece[] value; + + public SimplePiece(Piece[] value) { + super(Arrays.stream(value).mapToLong(dEntry -> dEntry.size).sum()); + this.value = value; + } + + @Override + public Piece[] getInner() { + return value; + } + + @Override + public String toString() { + return Arrays.toString(value); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/io/fragment/SizePiece.java b/src/main/java/dev/notalpha/dashloader/io/fragment/SizePiece.java new file mode 100644 index 00000000..8d587d6a --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/io/fragment/SizePiece.java @@ -0,0 +1,17 @@ +package dev.notalpha.dashloader.io.fragment; + +public class SizePiece extends Piece { + public SizePiece(long size) { + super(size); + } + + @Override + public Piece[] getInner() { + return null; + } + + @Override + public String toString() { + return ""; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/misc/HahaManager.java b/src/main/java/dev/notalpha/dashloader/misc/HahaManager.java new file mode 100644 index 00000000..f1117cad --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/misc/HahaManager.java @@ -0,0 +1,84 @@ +package dev.notalpha.dashloader.misc; + +import dev.notalpha.dashloader.config.Config; +import dev.notalpha.dashloader.config.ConfigHandler; + +import java.util.ArrayList; +import java.util.List; + +public final class HahaManager { + private static final String[] FACTS = { + "Dash was for the cool kids", + "fun fact: 1 + 1 = 11", + "glisco goes around and yells", + ":froge:", + ":bigfroge:", + ":smolfroge:", + "Frog + Doge = Froge", + "Froges dad is cool", + "Rogger Rogger!", + "Yes commander!", + "I am not the swarm!", + "Get that golden strawberry!", + "Kevin is cool.", + "B-Sides are where I flex.", + "Starting an accelerated backhop", + "Gordon Freeman. I like your tie.", + "The factory must grow.", + "Not the biters.", + "Ya got more red belts?", + "I need more boilers.", + "Throughput of circuits is gud.", + "amogus", + "sus", + "imposter", + "it was red!", + "What does the vent button do?", + "We need more white wine.", + "I season my cuttingboard", + "Do as I say, not as I do", + "Colton is fired", + "Was a banger on the cord.", + "My code thinks different.", + "Make it for 300$ sell it for 1300$", + "Steve is almost chad", + "IKEA is traditional.", + "1 + 1 = 11", + "https://ko-fi.com/notequalalpha", + "USB-C is gud.", + "Modrinth gud.", + "Leocth and Alpha were first.", + "Corn on a jakob is the best.", + "Cornebb is cool.", + "Hyphen is cool.", + "DashLoader kinda banger.", + "MFOTS was a thing.", + ":tnypotat:", + "418 I'm a teapot is a real error", + "mld hrdr - leocth 2022", + // HiItsDevin + "Devin beat 7C after 5 1/2 hours", + // shedaniel + "Look at me, I am vibing up here", + "Doesn't break REI", + // devonk15 + "Come here often?", + // bendy1234 + "We back!", + "No spaghetti code here..." + }; + + public static String getFact() { + Config config = ConfigHandler.INSTANCE.config; + List splashLines = new ArrayList<>(config.customSplashLines); + if (config.addDefaultSplashLines) { + splashLines.addAll(List.of(FACTS)); + } + + if (splashLines.isEmpty()) { + return null; + } + + return splashLines.get((int) (System.currentTimeMillis() % splashLines.size())); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/misc/ObjectDumper.java b/src/main/java/dev/notalpha/dashloader/misc/ObjectDumper.java new file mode 100644 index 00000000..01a3ede7 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/misc/ObjectDumper.java @@ -0,0 +1,115 @@ +package dev.notalpha.dashloader.misc; + +import com.mojang.blaze3d.platform.NativeImage; +import org.apache.commons.lang3.builder.MultilineRecursiveToStringStyle; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; + +import java.lang.reflect.Field; +import java.lang.reflect.InaccessibleObjectException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.*; +import java.util.function.Supplier; + +public class ObjectDumper { + public static String dump(Object object) { + return ReflectionToStringBuilder.toString(object, new Style()); + } + + private static final class Style extends MultilineRecursiveToStringStyle { + public Style() { + setFieldNameValueSeparator(": "); + setUseIdentityHashCode(false); + setUseShortClassName(true); + } + + public void appendDetail(StringBuffer buffer, String fieldName, Object value) { + try { + if (value == null) { + buffer.append(fieldName).append("null"); + return; + } + if (Objects.equals(fieldName, "glRef")) { + buffer.append(""); + return; + } + + switch (value) { + case ThreadLocal local -> appendDetail(buffer, fieldName, local.get()); + case HashMap map -> appendDetail(buffer, fieldName, map); + case ArrayList list -> appendDetail(buffer, fieldName, list); + case NativeImage image -> + buffer.append("Image{ format: ").append(image.format()).append(", size: ").append(image.getWidth()).append("x").append(image.getHeight()).append(" }"); + case IntBuffer buffer1 -> appendBuff(buffer, buffer1, buffer1::get); + case FloatBuffer buffer1 -> appendBuff(buffer, buffer1, buffer1::get); + case ByteBuffer buffer1 -> appendBuff(buffer, buffer1, buffer1::get); + case Enum enumValue -> buffer.append(enumValue.name()); + default -> fallback(buffer, fieldName, value); + } + } catch (Exception e) { + throw new RuntimeException(value == null ? "null" : value.toString(), e); + } + } + + private void fallback(StringBuffer buffer, String fieldName, Object value) { + try { + StringBuffer builder = new StringBuffer(); + super.appendDetail(builder, fieldName, value); + String s = builder.toString(); + String result = s.split("@")[0]; + buffer.append(result); + } catch (InaccessibleObjectException e) { + throw e; + } catch (Exception e) { + e.printStackTrace(); + + buffer.append("unknown"); + try { + Field spaces = MultilineRecursiveToStringStyle.class.getDeclaredField("spaces"); + spaces.setAccessible(true); + spaces.setInt(this, spaces.getInt(this) - 2); + } catch (IllegalAccessException | NoSuchFieldException ex) { + throw new RuntimeException(ex); + } + } + } + + private void appendBuff(StringBuffer buffer, Buffer buff, Supplier supplier) { + buffer.append(buff.getClass().getSimpleName()); + buffer.append("[ "); + int limit = buff.limit(); + if (limit < 50) { + buff.rewind(); + for (int i = 0; i < limit; i++) { + buffer.append(supplier.get()); + buffer.append(", "); + } + } else { + buffer.append("... "); + } + buffer.append("] limit: "); + buffer.append(buff.limit()); + } + + @Override + protected void appendDetail(StringBuffer buffer, String fieldName, Map map) { + buffer.append(this.getArrayStart()); + + // Sort maps to be comparable + List> entries = new ArrayList<>(map.entrySet()); + entries.sort((o1, o2) -> o1.getKey().toString().compareTo(o2.toString())); + entries.forEach((entry) -> { + buffer.append(getArraySeparator()); + this.appendDetail(buffer, String.valueOf(entry.getKey()), entry.getValue()); + }); + buffer.append(this.getArrayEnd()); + } + + @Override + protected void appendIdentityHashCode(StringBuffer buffer, Object object) { + + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/misc/ProfilerUtil.java b/src/main/java/dev/notalpha/dashloader/misc/ProfilerUtil.java new file mode 100644 index 00000000..cadd7117 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/misc/ProfilerUtil.java @@ -0,0 +1,24 @@ +package dev.notalpha.dashloader.misc; + +public final class ProfilerUtil { + public static long RELOAD_START = 0; + + public static String getTimeStringFromStart(long start) { + return getTimeString(System.currentTimeMillis() - start); + } + + public static String getTimeString(long ms) { + if (ms >= 60000) { // 1m + return ((int) ((ms / 60000))) + "m " + ((int) (ms % 60000) / 1000) + "s"; // [4m 42s] + } else if (ms >= 3000) // 3s + { + return printMsToSec(ms) + "s"; // 1293ms = [1.2s] + } else { + return ms + "ms"; // [400ms] + } + } + + private static float printMsToSec(long ms) { + return Math.round(ms / 100f) / 10f; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/misc/TranslationHelper.java b/src/main/java/dev/notalpha/dashloader/misc/TranslationHelper.java new file mode 100644 index 00000000..eadefaa4 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/misc/TranslationHelper.java @@ -0,0 +1,47 @@ +package dev.notalpha.dashloader.misc; + +import net.minecraft.client.Minecraft; +import net.minecraft.locale.Language; + +import java.util.HashMap; +import java.util.Objects; + +public class TranslationHelper { + private static final TranslationHelper INSTANCE = new TranslationHelper(); + private final HashMap translations; + private String langCode; + + private TranslationHelper() { + this.translations = new HashMap<>(); + } + + public static TranslationHelper getInstance() { + var langCode = Minecraft.getInstance().getLanguageManager().getSelected(); + if (!Objects.equals(INSTANCE.langCode, langCode)) { + INSTANCE.langCode = langCode; + INSTANCE.loadLang(langCode); + } + return INSTANCE; + } + + private void loadLang(String langCode) { + this.langCode = langCode; + var stream = this.getClass().getClassLoader().getResourceAsStream("dashloader/lang/" + langCode + ".json"); + if (stream != null) { + Language.loadFromJson(stream, this.translations::put); + } else { + stream = this.getClass().getClassLoader().getResourceAsStream("dashloader/lang/en_us.json"); + if (stream != null) { + Language.loadFromJson(stream, this.translations::put); + } + } + } + + public String get(String text) { + return this.translations.getOrDefault(text, text); + } + + public boolean has(String key) { + return this.translations.containsKey(key); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/misc/UnsafeHelper.java b/src/main/java/dev/notalpha/dashloader/misc/UnsafeHelper.java new file mode 100644 index 00000000..99c33858 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/misc/UnsafeHelper.java @@ -0,0 +1,38 @@ +package dev.notalpha.dashloader.misc; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public final class UnsafeHelper { + public static final sun.misc.Unsafe UNSAFE = getUnsafeInstance(); + + private static sun.misc.Unsafe getUnsafeInstance() { + Class clazz = sun.misc.Unsafe.class; + for (Field field : clazz.getDeclaredFields()) { + if (!field.getType().equals(clazz)) { + continue; + } + final int modifiers = field.getModifiers(); + if (!(Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers))) { + continue; + } + try { + field.setAccessible(true); + return (sun.misc.Unsafe) field.get(null); + } catch (Exception ignored) { + } + break; + } + + throw new IllegalStateException("Unsafe is unavailable."); + } + + @SuppressWarnings("unchecked") + public static O allocateInstance(Class closs) { + try { + return (O) UNSAFE.allocateInstance(closs); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/misc/UnsafeImage.java b/src/main/java/dev/notalpha/dashloader/misc/UnsafeImage.java new file mode 100644 index 00000000..6ced1d2c --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/misc/UnsafeImage.java @@ -0,0 +1,27 @@ +package dev.notalpha.dashloader.misc; + +import dev.notalpha.dashloader.mixin.accessor.NativeImageAccessor; +import com.mojang.blaze3d.platform.NativeImage; +import org.lwjgl.system.MemoryUtil; + +public final class UnsafeImage { + public final NativeImage image; + public final int width; + public final int height; + public final long pointer; + + public UnsafeImage(NativeImage image) { + this.image = image; + this.width = image.getWidth(); + this.height = image.getHeight(); + this.pointer = ((NativeImageAccessor) (Object) image).getPointer(); + } + + public int get(int x, int y) { + return MemoryUtil.memGetInt(this.pointer + ((long) x + (long) y * (long) width) * 4L); + } + + public void set(int x, int y, int value) { + MemoryUtil.memPutInt(this.pointer + ((long) x + (long) y * (long) width) * 4L, value); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/MixinPlugin.java b/src/main/java/dev/notalpha/dashloader/mixin/MixinPlugin.java new file mode 100644 index 00000000..e2edf82f --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/MixinPlugin.java @@ -0,0 +1,46 @@ +package dev.notalpha.dashloader.mixin; + +import dev.notalpha.dashloader.config.ConfigHandler; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +public class MixinPlugin implements IMixinConfigPlugin { + @Override + public void onLoad(String mixinPackage) { + + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + return ConfigHandler.shouldApplyMixin(mixinClassName); + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/accessor/BitmapFontAccessor.java b/src/main/java/dev/notalpha/dashloader/mixin/accessor/BitmapFontAccessor.java new file mode 100644 index 00000000..e657650e --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/accessor/BitmapFontAccessor.java @@ -0,0 +1,22 @@ +package dev.notalpha.dashloader.mixin.accessor; + +import net.minecraft.client.gui.font.CodepointMap; +import net.minecraft.client.gui.font.providers.BitmapProvider; +import com.mojang.blaze3d.platform.NativeImage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(BitmapProvider.class) +public interface BitmapFontAccessor { +@Invoker("") +static BitmapProvider init(NativeImage image, CodepointMap glyphs) { +throw new AssertionError(); +} + +@Accessor +CodepointMap getGlyphs(); + +@Accessor +NativeImage getImage(); +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/accessor/BitmapFontGlyphAccessor.java b/src/main/java/dev/notalpha/dashloader/mixin/accessor/BitmapFontGlyphAccessor.java new file mode 100644 index 00000000..97fe436b --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/accessor/BitmapFontGlyphAccessor.java @@ -0,0 +1,38 @@ +package dev.notalpha.dashloader.mixin.accessor; + +import com.mojang.blaze3d.platform.NativeImage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(targets = "net.minecraft.client.gui.font.providers.BitmapProvider$Glyph") +public interface BitmapFontGlyphAccessor { +@Invoker("") +static Object init(float scaleFactor, NativeImage image, int x, int y, int width, int height, int advance, int ascent) { +throw new AssertionError(); +} + +@Accessor +NativeImage getImage(); + +@Accessor("offsetX") +int getX(); + +@Accessor("offsetY") +int getY(); + +@Accessor("scale") +float getScaleFactor(); + +@Accessor +int getWidth(); + +@Accessor +int getHeight(); + +@Accessor +int getAdvance(); + +@Accessor +int getAscent(); +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/accessor/FilterMapAccessor.java b/src/main/java/dev/notalpha/dashloader/mixin/accessor/FilterMapAccessor.java new file mode 100644 index 00000000..be966684 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/accessor/FilterMapAccessor.java @@ -0,0 +1,13 @@ +package dev.notalpha.dashloader.mixin.accessor; + +import net.minecraft.client.gui.font.FontOption; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(FontOption.Filter.class) +public interface FilterMapAccessor { +@Accessor +Map getValues(); +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/accessor/FontManagerPreparationAccessor.java b/src/main/java/dev/notalpha/dashloader/mixin/accessor/FontManagerPreparationAccessor.java new file mode 100644 index 00000000..93058075 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/accessor/FontManagerPreparationAccessor.java @@ -0,0 +1,24 @@ +package dev.notalpha.dashloader.mixin.accessor; + +import com.mojang.blaze3d.font.GlyphProvider; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.util.List; +import java.util.Map; + +@Mixin(targets = "net.minecraft.client.gui.font.FontManager$Preparation") +public interface FontManagerPreparationAccessor { + @Invoker("") + static Object create(Map> fontSets, List allProviders) { + throw new AssertionError(); + } + + @Accessor("fontSets") + Map> getFontSets(); + + @Accessor("allProviders") + List getAllProviders(); +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/accessor/IdentifierAccessor.java b/src/main/java/dev/notalpha/dashloader/mixin/accessor/IdentifierAccessor.java new file mode 100644 index 00000000..9e2a87a6 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/accessor/IdentifierAccessor.java @@ -0,0 +1,13 @@ +package dev.notalpha.dashloader.mixin.accessor; + +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(ResourceLocation.class) +public interface IdentifierAccessor { + @Invoker("") + static ResourceLocation init(String namespace, String path) { + throw new AssertionError(); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/accessor/ModelLoaderAccessor.java b/src/main/java/dev/notalpha/dashloader/mixin/accessor/ModelLoaderAccessor.java new file mode 100644 index 00000000..2aaeb5e2 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/accessor/ModelLoaderAccessor.java @@ -0,0 +1,25 @@ +package dev.notalpha.dashloader.mixin.accessor; + +import dev.notalpha.hyphen.thr.HyphenException; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.client.resources.model.BlockStateDefinitions; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(BlockStateDefinitions.class) +public interface ModelLoaderAccessor { + @Accessor("ITEM_FRAME_FAKE_DEFINITION") + static StateDefinition getTheItemFrameThing() { + throw new HyphenException("froge", "your dad"); + } + + @Accessor("STATIC_DEFINITIONS") + static Map> getStaticDefinitions() { + throw new HyphenException("froge", "your dad"); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/accessor/NativeImageAccessor.java b/src/main/java/dev/notalpha/dashloader/mixin/accessor/NativeImageAccessor.java new file mode 100644 index 00000000..72f3e639 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/accessor/NativeImageAccessor.java @@ -0,0 +1,20 @@ +package dev.notalpha.dashloader.mixin.accessor; + +import com.mojang.blaze3d.platform.NativeImage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(NativeImage.class) +public interface NativeImageAccessor { + @Invoker("") + static NativeImage init(NativeImage.Format format, int width, int height, boolean useStb, long pointer) { + throw new AssertionError(); + } + + @Accessor("pixels") + long getPointer(); + + @Accessor("useStbFree") + boolean getIsStbImage(); +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/accessor/TrueTypeGlyphProviderAccessor.java b/src/main/java/dev/notalpha/dashloader/mixin/accessor/TrueTypeGlyphProviderAccessor.java new file mode 100644 index 00000000..8f350916 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/accessor/TrueTypeGlyphProviderAccessor.java @@ -0,0 +1,15 @@ +package dev.notalpha.dashloader.mixin.accessor; + +import com.mojang.blaze3d.font.TrueTypeGlyphProvider; +import org.lwjgl.util.freetype.FT_Face; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(TrueTypeGlyphProvider.class) +public interface TrueTypeGlyphProviderAccessor { + @Accessor("face") + FT_Face getFace(); + + @Accessor("oversample") + float getOversample(); +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/accessor/UnihexProviderAccessor.java b/src/main/java/dev/notalpha/dashloader/mixin/accessor/UnihexProviderAccessor.java new file mode 100644 index 00000000..6456bd30 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/accessor/UnihexProviderAccessor.java @@ -0,0 +1,18 @@ +package dev.notalpha.dashloader.mixin.accessor; + +import net.minecraft.client.gui.font.CodepointMap; +import net.minecraft.client.gui.font.providers.UnihexProvider; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(UnihexProvider.class) +public interface UnihexProviderAccessor { + @Invoker("") + static UnihexProvider create(CodepointMap glyphs) { + throw new AssertionError(); + } + + @Accessor("glyphs") + CodepointMap getGlyphs(); +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/main/MainMixin.java b/src/main/java/dev/notalpha/dashloader/mixin/main/MainMixin.java new file mode 100644 index 00000000..3ea2209b --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/main/MainMixin.java @@ -0,0 +1,26 @@ +package dev.notalpha.dashloader.mixin.main; + +import dev.notalpha.dashloader.DashLoader; +import net.minecraft.client.main.Main; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Main.class) +public class MainMixin { + @Unique + private static boolean INITIALIZED = false; + + @Inject( + method = "main*", + at = @At(value = "HEAD") + ) + private static void bootstrap(String[] args, CallbackInfo ci) { + if (!INITIALIZED) { + DashLoader.bootstrap(); + INITIALIZED = true; + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/option/cache/SplashTextResourceSupplierMixin.java b/src/main/java/dev/notalpha/dashloader/mixin/option/cache/SplashTextResourceSupplierMixin.java new file mode 100644 index 00000000..c3154b1a --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/option/cache/SplashTextResourceSupplierMixin.java @@ -0,0 +1,36 @@ +package dev.notalpha.dashloader.mixin.option.cache; + +import dev.notalpha.dashloader.api.cache.CacheStatus; +import dev.notalpha.dashloader.client.splash.SplashModule; +import net.minecraft.client.resources.SplashManager; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.List; + +@Mixin(SplashManager.class) +public class SplashTextResourceSupplierMixin { +@Inject( +method = "prepare(Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)Ljava/util/List;", +at = @At(value = "HEAD"), +cancellable = true +) +private void applySplashCache(ResourceManager resourceManager, ProfilerFiller profiler, CallbackInfoReturnable> cir) { +SplashModule.TEXTS.visit(CacheStatus.LOAD, cir::setReturnValue); +} + +@Inject( +method = "prepare(Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)Ljava/util/List;", +at = @At(value = "RETURN") +) +private void stealSplashCache(ResourceManager resourceManager, ProfilerFiller profiler, CallbackInfoReturnable> cir) { +SplashModule.TEXTS.visit(CacheStatus.SAVE, strings -> { +strings.clear(); +strings.addAll(cir.getReturnValue()); +}); +} +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/option/cache/font/FontManagerOverride.java b/src/main/java/dev/notalpha/dashloader/mixin/option/cache/font/FontManagerOverride.java new file mode 100644 index 00000000..f0a6fd2d --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/option/cache/font/FontManagerOverride.java @@ -0,0 +1,43 @@ +package dev.notalpha.dashloader.mixin.option.cache.font; + +import dev.notalpha.dashloader.DashLoader; +import dev.notalpha.dashloader.api.cache.CacheStatus; +import dev.notalpha.dashloader.client.font.FontModule; +import dev.notalpha.dashloader.mixin.accessor.FontManagerPreparationAccessor; +import net.minecraft.client.gui.font.FontManager; +import net.minecraft.server.packs.resources.ResourceManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +@Mixin(FontManager.class) +public class FontManagerOverride { + @Inject( + method = "prepare", + at = @At(value = "HEAD"), + cancellable = true + ) + private void loadFonts(ResourceManager resourceManager, Executor executor, CallbackInfoReturnable> cir) { + FontModule.DATA.visit(CacheStatus.LOAD, data -> { + DashLoader.LOG.info("Providing fonts"); + cir.setReturnValue(CompletableFuture.completedFuture(FontManagerPreparationAccessor.create(data.providers, data.allProviders))); + }); + } + + @Inject( + method = "apply", + at = @At(value = "HEAD") + ) + private void saveFonts(Object preparation, Object profiler, CallbackInfo ci) { + if (FontModule.DATA.active(CacheStatus.SAVE)) { + DashLoader.LOG.info("Saving fonts"); + FontManagerPreparationAccessor access = (FontManagerPreparationAccessor) preparation; + FontModule.DATA.set(CacheStatus.SAVE, new FontModule.ProviderIndex(access.getFontSets(), access.getAllProviders())); + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/option/misc/AffineTransformationMixin.java b/src/main/java/dev/notalpha/dashloader/mixin/option/misc/AffineTransformationMixin.java new file mode 100644 index 00000000..4eb27fa3 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/option/misc/AffineTransformationMixin.java @@ -0,0 +1,30 @@ +package dev.notalpha.dashloader.mixin.option.misc; + +import com.mojang.math.Transformation; +import org.joml.Matrix4f; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Objects; + +@Mixin(value = Transformation.class, priority = 999) +public class AffineTransformationMixin { + @Shadow + @Final + private Matrix4f matrix; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AffineTransformationMixin that)) return false; + if (!super.equals(o)) return false; + + return Objects.equals(matrix, that.matrix); + } + + @Override + public int hashCode() { + return matrix.hashCode(); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/mixin/option/misc/MipmapHelperMixin.java b/src/main/java/dev/notalpha/dashloader/mixin/option/misc/MipmapHelperMixin.java new file mode 100644 index 00000000..13c9c698 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/mixin/option/misc/MipmapHelperMixin.java @@ -0,0 +1,29 @@ +package dev.notalpha.dashloader.mixin.option.misc; + +import dev.notalpha.dashloader.mixin.accessor.NativeImageAccessor; +import net.minecraft.client.renderer.texture.MipmapGenerator; +import com.mojang.blaze3d.platform.NativeImage; +import org.lwjgl.system.MemoryUtil; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(MipmapGenerator.class) +public abstract class MipmapHelperMixin { + // not using wrapOperation because this is just replacing the call + @Redirect( + method = {"hasAlpha", "getMipmapLevelsImages"}, + at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/NativeImage;getColorArgb(II)I") + ) + private static int getColor(NativeImage instance, int x, int y) { + return MemoryUtil.memGetInt(((NativeImageAccessor) (Object) instance).getPointer() + ((long) x + (long) y * (long) instance.getWidth()) * 4L); + } + + @Redirect( + method = "getMipmapLevelsImages", + at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/NativeImage;setColorArgb(III)V") + ) + private static void setColor(NativeImage instance, int x, int y, int color) { + MemoryUtil.memPutInt(((NativeImageAccessor) (Object) instance).getPointer() + ((long) x + (long) y * (long) instance.getWidth()) * 4L, color); + } +} diff --git a/src/main/java/dev/notalpha/dashloader/registry/FactoryBinding.java b/src/main/java/dev/notalpha/dashloader/registry/FactoryBinding.java new file mode 100644 index 00000000..7f8fbee2 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/registry/FactoryBinding.java @@ -0,0 +1,95 @@ +package dev.notalpha.dashloader.registry; + +import dev.notalpha.dashloader.DashObjectClass; +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import org.jetbrains.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.function.Function; + +public final class FactoryBinding> { + private final MethodHandle method; + private final FactoryFunction creator; + + public FactoryBinding(MethodHandle method, FactoryFunction creator) { + this.method = method; + this.creator = creator; + } + + public static > FactoryBinding create(DashObjectClass dashObject) { + final Class dashClass = dashObject.getDashClass(); + + var factory = tryScanCreators((look, type) -> look.findConstructor(dashClass, type.changeReturnType(void.class)), dashObject); + if (factory == null) { + factory = tryScanCreators((look, type) -> look.findStatic(dashClass, "factory", type), dashObject); + } + if (factory == null) { + factory = tryScanCreators((look, type) -> look.findStatic(dashClass, "factory", type), dashObject); + } + + if (factory == null) { + throw new RuntimeException("Could not find a way to create " + dashClass.getSimpleName() + ". Create the method and/or check if it's accessible."); + } + + return factory; + } + + @Nullable + private static > FactoryBinding tryScanCreators(MethodTester tester, DashObjectClass dashObject) { + for (InvokeType value : InvokeType.values()) { + final Class[] apply = value.parameters.apply(dashObject); + + try { + var method = tester.getMethod( + MethodHandles.publicLookup(), + MethodType.methodType(dashObject.getTargetClass(), apply)); + + if (method != null) { + return new FactoryBinding<>(method, value.creator); + } + } catch (Throwable ignored) { + } + } + return null; + } + + public D create(R raw, RegistryWriter writer) { + try { + //noinspection unchecked + return (D) this.creator.create(this.method, raw, writer); + } catch (Throwable e) { + throw new RuntimeException("Could not create DashObject " + raw.getClass().getSimpleName(), e); + } + } + + // FULL object, writer + // WRITER writer + // RAW object + // EMPTY + private enum InvokeType { + FULL((methodHandle, args, args2) -> methodHandle.invoke(args, args2), doc -> new Class[]{doc.getTargetClass(), RegistryWriter.class}), + WRITER((mh, raw, writer) -> mh.invoke(writer), doc -> new Class[]{RegistryWriter.class}), + RAW((mh, raw, writer) -> mh.invoke(raw), doc -> new Class[]{doc.getTargetClass()}), + EMPTY((mh, raw, writer) -> mh.invoke(), doc -> new Class[0]); + private final FactoryFunction creator; + private final Function, Class[]> parameters; + + InvokeType(FactoryFunction creator, Function, Class[]> parameters) { + this.creator = creator; + this.parameters = parameters; + } + } + + @FunctionalInterface + private interface FactoryFunction { + Object create(MethodHandle method, Object raw, RegistryWriter writer) throws Throwable; + } + + @FunctionalInterface + private interface MethodTester { + MethodHandle getMethod(MethodHandles.Lookup lookup, MethodType parameters) throws Throwable; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/registry/MissingHandler.java b/src/main/java/dev/notalpha/dashloader/registry/MissingHandler.java new file mode 100644 index 00000000..08a423b7 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/registry/MissingHandler.java @@ -0,0 +1,16 @@ +package dev.notalpha.dashloader.registry; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryWriter; + +import java.util.function.BiFunction; + +public class MissingHandler { + public final Class parentClass; + public final BiFunction> func; + + public MissingHandler(Class parentClass, BiFunction> func) { + this.parentClass = parentClass; + this.func = func; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/registry/RegistryReaderImpl.java b/src/main/java/dev/notalpha/dashloader/registry/RegistryReaderImpl.java new file mode 100644 index 00000000..0f875454 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/registry/RegistryReaderImpl.java @@ -0,0 +1,44 @@ +package dev.notalpha.dashloader.registry; + +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.io.data.CacheInfo; +import dev.notalpha.dashloader.registry.data.StageData; +import dev.notalpha.taski.Task; +import dev.notalpha.taski.builtin.StepTask; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +@SuppressWarnings("FinalMethodInFinalClass") +public final class RegistryReaderImpl implements RegistryReader { + private final StageData[] chunkData; + // Holds an array of the exported dataChunks array values. + private final Object[][] data; + + public RegistryReaderImpl(CacheInfo metadata, StageData[] data) { + this.chunkData = data; + this.data = new Object[metadata.chunks.size()][]; + for (int i = 0; i < metadata.chunks.size(); i++) { + this.data[i] = new Object[metadata.chunks.get(i).size]; + } + } + + public final void export(@Nullable Consumer taskConsumer) { + StepTask task = new StepTask("Exporting", Integer.max(this.chunkData.length, 1)); + if (taskConsumer != null) { + taskConsumer.accept(task); + } + + for (StageData chunkData : chunkData) { + chunkData.preExport(this); + chunkData.export(data, this); + chunkData.postExport(this); + } + } + + @SuppressWarnings("unchecked") + public final R get(final int pointer) { + // inlining go brrr + return (R) this.data[pointer & 0x3f][pointer >>> 6]; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/registry/RegistryWriterImpl.java b/src/main/java/dev/notalpha/dashloader/registry/RegistryWriterImpl.java new file mode 100644 index 00000000..6a4ab300 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/registry/RegistryWriterImpl.java @@ -0,0 +1,180 @@ +package dev.notalpha.dashloader.registry; + +import dev.notalpha.dashloader.DashObjectClass; +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryAddException; +import dev.notalpha.dashloader.api.registry.RegistryUtil; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.registry.data.ChunkData; +import dev.notalpha.dashloader.registry.data.ChunkFactory; +import dev.notalpha.dashloader.registry.data.StageData; +import it.unimi.dsi.fastutil.objects.Object2ByteMap; +import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; + +public final class RegistryWriterImpl implements RegistryWriter { + public final ChunkFactory[] chunks; + private final IdentityHashMap dedup = new IdentityHashMap<>(); + private final Object2ByteMap> target2chunkMappings; + private final Object2ByteMap> dash2chunkMappings; + private final List> missingHandlers; + + private RegistryWriterImpl(ChunkFactory[] chunks, List> missingHandlers) { + this.target2chunkMappings = new Object2ByteOpenHashMap<>(); + this.target2chunkMappings.defaultReturnValue((byte) -1); + + this.dash2chunkMappings = new Object2ByteOpenHashMap<>(); + this.dash2chunkMappings.defaultReturnValue((byte) -1); + + this.missingHandlers = missingHandlers; + this.chunks = chunks; + } + + public static > RegistryWriterImpl create(List> missingHandlers, List> dashObjects) { + if (dashObjects.size() > 63) { + throw new RuntimeException("Hit group limit of 63. Please contact notalpha if you hit this limit!"); + } + + //noinspection unchecked + ChunkFactory[] chunks = new ChunkFactory[dashObjects.size()]; + RegistryWriterImpl writer = new RegistryWriterImpl(chunks, missingHandlers); + + for (int i = 0; i < dashObjects.size(); i++) { + final DashObjectClass dashObject = (DashObjectClass) dashObjects.get(i); + + var dashClass = dashObject.getDashClass(); + var targetClass = dashObject.getTargetClass(); + byte old = writer.target2chunkMappings.put(targetClass, (byte) i); + if (old != -1) { + DashObjectClass conflicting = dashObjects.get(old); + throw new IllegalStateException("DashObjects \"" + dashObject.getDashClass() + "\" and \"" + conflicting.getDashClass() + "\" have the same target class \"" + targetClass + "\"."); + } + + writer.dash2chunkMappings.put(dashClass, (byte) i); + var factory = FactoryBinding.create(dashObject); + var name = dashClass.getSimpleName(); + chunks[i] = new ChunkFactory<>((byte) i, name, factory, dashObject); + } + + return writer; + } + + public int add(R object) { + return this.addObject(object); + } + + @SuppressWarnings("unchecked") + private > int addObject(R object) { + if (this.dedup.containsKey(object)) { + return this.dedup.get(object); + } + + if (object == null) { + throw new NullPointerException("Registry add argument is null"); + } + + var targetClass = object.getClass(); + Integer pointer = null; + // If we have a dashObject supporting the target we create using its factory constructor + { + byte chunkPos = this.target2chunkMappings.getByte(targetClass); + if (chunkPos != -1) { + var chunk = (ChunkFactory) this.chunks[chunkPos]; + var entry = TrackingRegistryWriterImpl.create(this, writer -> chunk.create(object, writer)); + pointer = chunk.add(entry, this); + } + } + + // If we cannot find a target matching we go through the missing handlers + if (pointer == null) { + for (MissingHandler missingHandler : this.missingHandlers) { + if (missingHandler.parentClass.isAssignableFrom(targetClass)) { + var entry = TrackingRegistryWriterImpl.create(this, writer -> (D) missingHandler.func.apply(object, writer)); + if (entry.data != null) { + var dashClass = entry.data.getClass(); + byte chunkPos = this.dash2chunkMappings.getByte(dashClass); + if (chunkPos == -1) { + throw new RuntimeException("Could not find a ChunkWriter for DashClass " + dashClass); + } + var chunk = (ChunkFactory) this.chunks[chunkPos]; + pointer = chunk.add(entry, this); + break; + } + } + } + } + + if (pointer == null) { + throw new RegistryAddException(targetClass, object); + } + + ((IdentityHashMap) this.dedup).put(object, pointer); + return pointer; + } + + public ChunkFactory.Entry get(int id) { + return (ChunkFactory.Entry) this.chunks[RegistryUtil.getChunkId(id)].list.get(RegistryUtil.getObjectId(id)); + } + + public StageData[] export() { + // Create a queue with the elements with no references + var exposedQueue = new ArrayDeque>(); + for (ChunkFactory chunk : chunks) { + for (ChunkFactory.Entry entry : chunk.list) { + if (entry.references == 0) { + entry.stage = 0; + exposedQueue.offer(entry); + } + } + } + + // This sets the correct stage for every element + int stages = 1; + // Go through the exposed nodes (ones without edges) + while (!exposedQueue.isEmpty()) { + // Remove the element from the exposed queue. + var element = exposedQueue.poll(); + for (var dependencyId : element.dependencies) { + // Make dependencies a stage above + ChunkFactory.Entry dependency = get(dependencyId); + if (dependency.stage <= element.stage) { + dependency.stage = element.stage + 1; + if (dependency.stage >= stages) { + stages = dependency.stage + 1; + } + } + // Remove the edge, if the dependency no longer has references, add it to the queue. + if (--dependency.references == 0) { + exposedQueue.offer(dependency); + } + } + } + + // Create the output + StageData[] out = new StageData[stages]; + for (int i = 0; i < stages; i++) { + ChunkData[] chunksOut = new ChunkData[this.chunks.length]; + + for (int j = 0; j < this.chunks.length; j++) { + ChunkFactory chunk = this.chunks[j]; + List> dashablesOut = new ArrayList<>(); + for (int k = 0; k < chunk.list.size(); k++) { + ChunkFactory.Entry entry = chunk.list.get(k); + if (entry.stage == i) { + dashablesOut.add(new ChunkData.Entry<>(entry.data, k)); + } + } + + chunksOut[j] = new ChunkData<>(chunk.chunkId, chunk.name, chunk.dashObject, dashablesOut.toArray(ChunkData.Entry[]::new)); + } + + out[stages - (i + 1)] = new StageData(chunksOut); + } + + return out; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/registry/TrackingRegistryWriterImpl.java b/src/main/java/dev/notalpha/dashloader/registry/TrackingRegistryWriterImpl.java new file mode 100644 index 00000000..14030488 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/registry/TrackingRegistryWriterImpl.java @@ -0,0 +1,35 @@ +package dev.notalpha.dashloader.registry; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.registry.data.ChunkFactory; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; + +import java.util.function.Function; + +/** + * The Writers job is to allow dashObject to add dependencies by adding them to the registry and allowing parallelization. + * The logic is actually in RegistryFactory, but we need to be able to track what added what so the writer gets issued on the invocation of the creator. + */ +public final class TrackingRegistryWriterImpl implements RegistryWriter { + private final RegistryWriterImpl factory; + private final IntList dependencies = new IntArrayList(); + + private TrackingRegistryWriterImpl(RegistryWriterImpl factory) { + this.factory = factory; + } + + static > ChunkFactory.Entry create(RegistryWriterImpl factory, Function function) { + TrackingRegistryWriterImpl writer = new TrackingRegistryWriterImpl(factory); + D data = function.apply(writer); + int[] dependencies = writer.dependencies.toIntArray(); + return new ChunkFactory.Entry<>(data, dependencies); + } + + public int add(R object) { + int value = factory.add(object); + dependencies.add(value); + return value; + } +} diff --git a/src/main/java/dev/notalpha/dashloader/registry/data/ChunkData.java b/src/main/java/dev/notalpha/dashloader/registry/data/ChunkData.java new file mode 100644 index 00000000..fbe05aa5 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/registry/data/ChunkData.java @@ -0,0 +1,50 @@ +package dev.notalpha.dashloader.registry.data; + +import dev.notalpha.dashloader.DashObjectClass; +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.thread.ThreadHandler; + +public class ChunkData> { + public final byte chunkId; + public final String name; + public final DashObjectClass dashObject; + public final Entry[] dashables; + + public ChunkData(byte chunkId, String name, DashObjectClass dashObject, Entry[] dashables) { + this.chunkId = chunkId; + this.name = name; + this.dashObject = dashObject; + this.dashables = dashables; + } + + public void preExport(RegistryReader reader) { + for (Entry entry : this.dashables) { + entry.data.preExport(reader); + } + } + + public void export(Object[] data, RegistryReader registry) { + ThreadHandler.INSTANCE.parallelExport(this.dashables, data, registry); + } + + public void postExport(RegistryReader reader) { + for (Entry entry : this.dashables) { + entry.data.postExport(reader); + } + } + + public int getSize() { + return this.dashables.length; + } + + public static final class Entry { + public final D data; + public final int pos; + + public Entry(D data, int pos) { + this.data = data; + this.pos = pos; + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/registry/data/ChunkFactory.java b/src/main/java/dev/notalpha/dashloader/registry/data/ChunkFactory.java new file mode 100644 index 00000000..dd712d55 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/registry/data/ChunkFactory.java @@ -0,0 +1,67 @@ +package dev.notalpha.dashloader.registry.data; + +import dev.notalpha.dashloader.DashObjectClass; +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryUtil; +import dev.notalpha.dashloader.api.registry.RegistryWriter; +import dev.notalpha.dashloader.registry.FactoryBinding; +import dev.notalpha.dashloader.registry.RegistryWriterImpl; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; + +import java.util.ArrayList; +import java.util.List; + +public class ChunkFactory> { + public final byte chunkId; + public final String name; + public final DashObjectClass dashObject; + public final List> list = new ArrayList<>(); + public final Object2IntMap deduplication = new Object2IntOpenHashMap<>(); + private final FactoryBinding factory; + + public ChunkFactory(byte chunkId, String name, FactoryBinding factory, DashObjectClass dashObject) { + this.chunkId = chunkId; + this.name = name; + this.factory = factory; + this.dashObject = dashObject; + } + + public D create(R raw, RegistryWriter writer) { + return this.factory.create(raw, writer); + } + + public int add(Entry entry, RegistryWriterImpl factory) { + int existing = deduplication.getOrDefault(entry.data, -1); + if (existing != -1) { + return RegistryUtil.createId(existing, chunkId); + } + + final int pos = this.list.size(); + this.list.add(entry); + + // Add to deduplication + deduplication.put(entry.data, pos); + + // Increment dependencies + for (int dependency : entry.dependencies) { + ChunkFactory chunk = factory.chunks[RegistryUtil.getChunkId(dependency)]; + ChunkFactory.Entry dependencyEntry = chunk.list.get(RegistryUtil.getObjectId(dependency)); + dependencyEntry.references++; + } + + return RegistryUtil.createId(pos, chunkId); + } + + public static final class Entry { + public final D data; + public final int[] dependencies; + public int references = 0; + public int stage = -1; + + public Entry(D data, int[] dependencies) { + this.data = data; + this.dependencies = dependencies; + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/registry/data/StageData.java b/src/main/java/dev/notalpha/dashloader/registry/data/StageData.java new file mode 100644 index 00000000..bdc40d51 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/registry/data/StageData.java @@ -0,0 +1,30 @@ +package dev.notalpha.dashloader.registry.data; + +import dev.notalpha.dashloader.api.registry.RegistryReader; + +public class StageData { + public final ChunkData[] chunks; + + public StageData(ChunkData[] chunks) { + this.chunks = chunks; + } + + public void preExport(RegistryReader reader) { + for (ChunkData chunk : chunks) { + chunk.preExport(reader); + } + } + + public void export(Object[][] data, RegistryReader registry) { + for (int i = 0; i < chunks.length; i++) { + ChunkData chunk = chunks[i]; + chunk.export(data[i], registry); + } + } + + public void postExport(RegistryReader reader) { + for (ChunkData chunk : chunks) { + chunk.postExport(reader); + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/thread/IndexedArrayMapTask.java b/src/main/java/dev/notalpha/dashloader/thread/IndexedArrayMapTask.java new file mode 100644 index 00000000..70311194 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/thread/IndexedArrayMapTask.java @@ -0,0 +1,48 @@ +package dev.notalpha.dashloader.thread; + +import dev.notalpha.dashloader.registry.data.ChunkData; + +import java.util.concurrent.RecursiveAction; +import java.util.function.Function; + +public final class IndexedArrayMapTask extends RecursiveAction { + private final int threshold; + private final int start; + private final int stop; + private final ChunkData.Entry[] inArray; + private final O[] outArray; + private final Function function; + + private IndexedArrayMapTask(ChunkData.Entry[] inArray, O[] outArray, Function function, int threshold, int start, int stop) { + this.threshold = threshold; + this.start = start; + this.stop = stop; + this.inArray = inArray; + this.outArray = outArray; + this.function = function; + } + + public IndexedArrayMapTask(ChunkData.Entry[] inArray, O[] outArray, Function function) { + this.start = 0; + this.stop = inArray.length; + this.threshold = ThreadHandler.calcThreshold(this.stop); + this.inArray = inArray; + this.outArray = outArray; + this.function = function; + } + + @Override + protected void compute() { + final int size = this.stop - this.start; + if (size < this.threshold) { + for (int i = this.start; i < this.stop; i++) { + var entry = this.inArray[i]; + this.outArray[entry.pos] = this.function.apply(entry.data); + } + } else { + final int middle = this.start + (size / 2); + invokeAll(new IndexedArrayMapTask<>(this.inArray, this.outArray, this.function, this.threshold, this.start, middle), + new IndexedArrayMapTask<>(this.inArray, this.outArray, this.function, this.threshold, middle, this.stop)); + } + } +} diff --git a/src/main/java/dev/notalpha/dashloader/thread/ThreadHandler.java b/src/main/java/dev/notalpha/dashloader/thread/ThreadHandler.java new file mode 100644 index 00000000..f0504447 --- /dev/null +++ b/src/main/java/dev/notalpha/dashloader/thread/ThreadHandler.java @@ -0,0 +1,78 @@ +package dev.notalpha.dashloader.thread; + +import dev.notalpha.dashloader.api.DashObject; +import dev.notalpha.dashloader.api.registry.RegistryReader; +import dev.notalpha.dashloader.registry.data.ChunkData; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntFunction; + +public final class ThreadHandler { + public static final int THREADS = Runtime.getRuntime().availableProcessors(); + public static final ThreadHandler INSTANCE = new ThreadHandler(); + private final ForkJoinPool threadPool = new ForkJoinPool(THREADS, new ForkJoinPool.ForkJoinWorkerThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(0); + + @Override + public ForkJoinWorkerThread newThread(ForkJoinPool pool) { + final ForkJoinWorkerThread dashThread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); + dashThread.setDaemon(true); + dashThread.setName("dlc-thread-" + this.threadNumber.getAndIncrement()); + return dashThread; + } + }, null, true); + + private ThreadHandler() { + } + + public static int calcThreshold(final int tasks) { + return Math.max(tasks / (THREADS * 8), 4); + } + + // Fork Join Methods + public > void parallelExport(ChunkData.Entry[] in, R[] out, RegistryReader reader) { + this.threadPool.invoke(new IndexedArrayMapTask<>(in, out, d -> d.export(reader))); + } + + // Basic Methods + public void parallelRunnable(Runnable... runnables) { + this.parallelRunnable(List.of(runnables)); + } + + public void parallelRunnable(Collection runnables) { + for (Future future : this.threadPool.invokeAll(runnables.stream().map(Executors::callable).toList())) { + this.acquire(future); + } + } + + @SafeVarargs + public final O[] parallelCallable(IntFunction creator, Callable... callables) { + O[] out = creator.apply(callables.length); + var futures = this.threadPool.invokeAll(List.of(callables)); + for (int i = 0, futuresSize = futures.size(); i < futuresSize; i++) { + out[i] = (this.acquire(futures.get(i))); + } + return out; + } + + public Collection parallelCallable(Collection> callables) { + List out = new ArrayList<>(); + var futures = this.threadPool.invokeAll(callables); + for (Future future : futures) { + out.add(this.acquire(future)); + } + return out; + } + + private O acquire(Future future) { + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/resources/dashloader.accesswidener b/src/main/resources/dashloader.accesswidener new file mode 100644 index 00000000..3fde5170 --- /dev/null +++ b/src/main/resources/dashloader.accesswidener @@ -0,0 +1 @@ +accessWidener v1 named diff --git a/src/main/resources/dashloader.mixins.json b/src/main/resources/dashloader.mixins.json new file mode 100644 index 00000000..7dc8c86d --- /dev/null +++ b/src/main/resources/dashloader.mixins.json @@ -0,0 +1,17 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.notalpha.dashloader.mixin", + "plugin": "dev.notalpha.dashloader.mixin.MixinPlugin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "accessor.IdentifierAccessor", + "accessor.ModelLoaderAccessor", + "main.MainMixin", + "option.cache.SplashTextResourceSupplierMixin" + ], + "injectors": { + "defaultRequire": 1 + }, + "client": [] +} diff --git a/src/main/resources/dashloader/lang/en_us.json b/src/main/resources/dashloader/lang/en_us.json new file mode 100644 index 00000000..1e0fb2a8 --- /dev/null +++ b/src/main/resources/dashloader/lang/en_us.json @@ -0,0 +1,41 @@ +{ + "config.CACHE_ATLASES": "Atlas Cache", + "config.CACHE_ATLASES.tooltip": "Caches stitched atlases, significantly reducing GPU upload time", + "config.CACHE_FONT": "Font Cache", + "config.CACHE_FONT.tooltip": "Caches fonts and their images", + "config.CACHE_MODEL_LOADER": "Model Loader Cache", + "config.CACHE_MODEL_LOADER.tooltip": "Caches BakedModels allowing the game to load extremely fast", + "config.CACHE_SHADER": "Shader Cache", + "config.CACHE_SPLASH_TEXT": "Splash Text Cache", + "config.CACHE_SPLASH_TEXT.tooltip": "Caches the splash texts from the main screen", + "config.CACHE_SPRITE_CONTENT": "Sprite Content Cache", + "config.CACHE_SPRITE_STITCHING": "Sprite Stitching Cache", + "config.CACHE_SPRITE_STITCHING.tooltip": "Caches sprite stitching", + "config.FAST_MODEL_IDENTIFIER_EQUALS": "Fast Equals Check", + "config.FAST_MODEL_IDENTIFIER_EQUALS.tooltip": "Use a much faster equals check for ModelIdentifiers", + "config.FAST_WALL_BLOCK": "Fast Wall Blockstates", + "config.FAST_WALL_BLOCK.tooltip": "Caches the two most common blockstates for wall blocks", + "config.UNSAFE_MIPMAP_GENERATION": "Unsafe Mipmap generation", + "config.UNSAFE_MIPMAP_GENERATION.tooltip": "Skips redundant safety checks when generating mipmaps", + "config.caching_toast": "Show Caching Toast", + "config.category.behaviour": "Behavior", + "config.category.features": "Features (restart required)", + "config.category.visuals": "Visuals", + "config.compression": "Compression Level", + "config.compression.tooltip": "The level of compression to use when saving the cache", + "config.custom_splashes": "Custom Splashes", + "config.custom_splashes.tooltip": "Use ; to separate lines and ;; for a semicolon", + "config.default_splashes": "Add Default Splashes", + "config.max_caches": "Max Caches", + "config.single_threaded_reading": "Single Threaded Reading", + "config.title": "Dashloader Config", + "debug": "Debug Mode is active in config.", + "save": "Initializing", + "save.cache": "Caching", + "save.cache.image": "Caching images", + "save.cache.misc": "Caching miscellaneous", + "save.cache.model": "Caching models", + "save.serialize": "Serializing", + "save.serialize.fragment": "Serializing fragments", + "save.serialize.mapping": "Serializing mappings" +} diff --git a/src/main/resources/dashloader/lang/lol_us.json b/src/main/resources/dashloader/lang/lol_us.json new file mode 100644 index 00000000..1e7e32b2 --- /dev/null +++ b/src/main/resources/dashloader/lang/lol_us.json @@ -0,0 +1,35 @@ +{ + "config.CACHE_ATLASES": "Atlas Cache", + "config.CACHE_FONT": "Font Cache", + "config.CACHE_MODEL_LOADER": "Model Loader Cache", + "config.CACHE_MODEL_LOADER.tooltip": "Caches BakedModels allowing the game to load extremely fast", + "config.CACHE_SHADER": "Shader Cache", + "config.CACHE_SPLASH_TEXT": "Splash Text Cache", + "config.CACHE_SPRITE_CONTENT": "Sprite Cache", + "config.CACHE_SPRITE_STITCHING": "Sprite Stitching Cache", + "config.FAST_MODEL_IDENTIFIER_EQUALS": "Fast Equals Check", + "config.FAST_MODEL_IDENTIFIER_EQUALS.tooltip": "Use a much faster equals check for ModelIdentifiers", + "config.FAST_WALL_BLOCK": "Fast Wall Blockstates", + "config.FAST_WALL_BLOCK.tooltip": "Caches the 2 most common blockstates for wall blocks", + "config.caching_toast": "Show Caching Toast", + "config.category.behaviour": "Behavior", + "config.category.features": "Features", + "config.category.visuals": "Visuals", + "config.compression": "Compression Level", + "config.compression.tooltip": "The level of compression to use when saving the cache", + "config.custom_splashes": "Custom Splashes", + "config.custom_splashes.tooltip": "Use ; to separate lines and ;; for a semicolon", + "config.default_splashes": "Add Default Splashes", + "config.max_caches": "Max Caches", + "config.single_threaded_reading": "Single Threaded Reading", + "config.title": "Dashloader Config", + "debug": "eyo debug is on! Check the cribby config.", + "save": "wakin up!", + "save.cache": "owoifying", + "save.cache.model": "uwufying anime", + "save.cache.image": "saving anime", + "save.cache.misc": "savin' stuff", + "save.serialize": "sending dms", + "save.serialize.fragment": "sending hot sha256 keys", + "save.serialize.mapping": "sending private key" +} diff --git a/src/main/resources/dashloader/lang/sv_se.json b/src/main/resources/dashloader/lang/sv_se.json new file mode 100644 index 00000000..cf2da4a0 --- /dev/null +++ b/src/main/resources/dashloader/lang/sv_se.json @@ -0,0 +1,11 @@ +{ + "debug": "Debug läge är på i konfigurations filen.", + "save": "Påbörjar", + "save.cache": "Laddar", + "save.cache.model": "Laddar modeler", + "save.cache.image": "Laddar bilder", + "save.cache.misc": "Laddar blandat", + "save.serialize": "Sparar", + "save.serialize.fragment": "Sparar fragment", + "save.serialize.mapping": "Sparar mappningar" +} \ No newline at end of file diff --git a/src/main/resources/dashloader/textures/icon.png b/src/main/resources/dashloader/textures/icon.png new file mode 100644 index 00000000..5f8fc5f8 Binary files /dev/null and b/src/main/resources/dashloader/textures/icon.png differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 00000000..2490f1f5 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,47 @@ +{ + "schemaVersion": 1, + "id": "dashloader", + "version": "${version}", + "name": "DashLoader", + "description": "Launch at the speed of Light.", + "authors": [ + "!alpha", + "bendy1234", + "leocth" + ], + "contact": { + "homepage": "https://discord.gg/VeFTrtCkrb", + "sources": "https://github.com/QuantumFusionMC/DashLoader-Definition" + }, + "entrypoints": { + "dashloader": [ + "dev.notalpha.dashloader.client.DashLoaderClient" + ], + "modmenu": [ + "dev.notalpha.dashloader.client.ModMenuCompat" + ] + }, + "license": "LGPL-3.0-only", + "icon": "dashloader/textures/icon.png", + "environment": "client", + "accessWidener": "dashloader.accesswidener", + "mixins": [ + "dashloader.mixins.json" + ], + "depends": { + "fabricloader": ">=0.11.3", + "minecraft": "~1.21.10", + "java": ">=21" + }, + "custom": { + "dashloader:disableoption": [ + ], + "sodium:options": { + "mixin.features.shader.uniform": false + } + }, + "breaks": { + "sodium": "<=0.1.0", + "fabric-api": "<0.81.0" + } +}