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
-
-
-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.
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+ 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.
+
+
+
+ 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 extends DashObject, ?>> 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 extends DashObject, ?>> 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 extends DashObject, ?>> 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