diff --git a/.editorconfig b/.editorconfig
index 6fdb37b49c..c2362ce336 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -548,7 +548,7 @@ ij_json_keep_indents_on_empty_lines = false
ij_json_keep_line_breaks = true
ij_json_space_after_colon = true
ij_json_space_after_comma = true
-ij_json_space_before_colon = true
+ij_json_space_before_colon = false
ij_json_space_before_comma = false
ij_json_spaces_within_braces = false
ij_json_spaces_within_brackets = false
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000000..6c889429c3
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,2 @@
+/.github/CODEOWNERS @jellysquid3
+/.github/workflows @jellysquid3
diff --git a/.github/workflows/build-tag.yml b/.github/workflows/build-commit-nopublish.yml
similarity index 63%
rename from .github/workflows/build-tag.yml
rename to .github/workflows/build-commit-nopublish.yml
index 478c3b3d2b..38f050550e 100644
--- a/.github/workflows/build-tag.yml
+++ b/.github/workflows/build-commit-nopublish.yml
@@ -1,11 +1,11 @@
-# Used when a commit is tagged and pushed to the repository
+# Used when a commit is pushed to the repository
# This makes use of caching for faster builds and uploads the resulting artifacts
-name: build-tag
+name: build-commit-nopublish
on:
push:
- tags:
- - '*'
+ branches-ignore:
+ - dev
jobs:
build:
@@ -17,23 +17,26 @@ jobs:
# bash pattern expansion to grab branch name without slashes
run: ref="${GITHUB_REF#refs/heads/}" && echo "branch=${ref////-}" >> $GITHUB_OUTPUT
id: ref
+
- name: Checkout sources
- uses: actions/checkout@v4
- - uses: actions/setup-java@v4
+ uses: actions/checkout@v6
+
+ - name: Setup Java 21
+ uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
- cache-read-only: true
+ cache-read-only: false
- name: Execute Gradle build
- run: ./gradlew build -Pbuild.release=true
+ run: ./gradlew build
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: sodium-artifacts-${{ steps.ref.outputs.branch }}
- path: build/mods/*.jar
\ No newline at end of file
+ path: build/mods/*.jar
diff --git a/.github/workflows/build-commit.yml b/.github/workflows/build-commit.yml
index b73702a561..cd737b6d7a 100644
--- a/.github/workflows/build-commit.yml
+++ b/.github/workflows/build-commit.yml
@@ -2,11 +2,15 @@
# This makes use of caching for faster builds and uploads the resulting artifacts
name: build-commit
-on: [ push ]
+on:
+ push:
+ branches:
+ - dev
jobs:
build:
runs-on: ubuntu-latest
+ environment: staging
steps:
- name: Extract current branch name
@@ -14,20 +18,18 @@ jobs:
# bash pattern expansion to grab branch name without slashes
run: ref="${GITHUB_REF#refs/heads/}" && echo "branch=${ref////-}" >> $GITHUB_OUTPUT
id: ref
- - name: Checkout sources
- uses: actions/checkout@v4
- - name: Validate Gradle Wrapper
- uses: gradle/actions/wrapper-validation@v3
+ - name: Checkout sources
+ uses: actions/checkout@v6
- name: Setup Java 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: false
@@ -35,7 +37,14 @@ jobs:
run: ./gradlew build
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: sodium-artifacts-${{ steps.ref.outputs.branch }}
- path: build/mods/*.jar
\ No newline at end of file
+ path: build/mods/*.jar
+
+ - name: Publish Release Snapshot to CaffeineMC Maven
+ env:
+ ORG_GRADLE_PROJECT_caffeineMCMavenUsername: ${{ secrets.CAFFEINEMC_MAVEN_USERNAME }}
+ ORG_GRADLE_PROJECT_caffeineMCMavenPassword: ${{ secrets.CAFFEINEMC_MAVEN_PASSWORD }}
+ if: ${{ env.ORG_GRADLE_PROJECT_caffeineMCMavenUsername != '' && env.ORG_GRADLE_PROJECT_caffeineMCMavenPassword != '' }}
+ run: ./gradlew publishAllPublicationsToCaffeineMCRepository
diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml
index 045dd53920..2ae9dc38fc 100644
--- a/.github/workflows/build-pull-request.yml
+++ b/.github/workflows/build-pull-request.yml
@@ -9,15 +9,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: gradle/actions/wrapper-validation@v3
- - uses: actions/setup-java@v4
+ - name: Extract current branch name
+ shell: bash
+ # bash pattern expansion to grab branch name without slashes
+ run: ref="${GITHUB_REF#refs/heads/}" && echo "branch=${ref////-}" >> $GITHUB_OUTPUT
+ id: ref
+
+ - name: Checkout sources
+ uses: actions/checkout@v6
+
+ - name: Setup Java 21
+ uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: true
@@ -25,7 +33,7 @@ jobs:
run: ./gradlew build
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: sodium-artifacts-${{ steps.ref.outputs.branch }}
- path: build/mods/*.jar
\ No newline at end of file
+ path: build/mods/*.jar
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
deleted file mode 100644
index ab751d49b9..0000000000
--- a/.github/workflows/build-release.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-# Used when a release is pushed to GitHub
-# This does not make use of any caching as to ensure a clean build
-name: build-release
-
-on:
- release:
- types:
- - published
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-java@v4
- with:
- distribution: temurin
- java-version: 21
-
- - name: Setup Gradle
- uses: gradle/actions/setup-gradle@v4
- with:
- cache-read-only: true
-
- - name: Execute Gradle build
- run: ./gradlew build -Pbuild.release=true
-
- - name: Upload assets to GitHub
- uses: AButler/upload-release-assets@v2.0
- with:
- files: 'build/mods/*.jar'
- repo-token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml
new file mode 100644
index 0000000000..6580cbb219
--- /dev/null
+++ b/.github/workflows/publish-maven.yml
@@ -0,0 +1,47 @@
+# Used when a developer decides to publish a release to the caffeinemc maven
+# This is primarily for when we manually need to publish a release because for some reason publishing broke
+name: Release CaffeineMC Maven
+
+on: [ workflow_dispatch ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ environment:
+ name: prod
+
+ steps:
+ - name: Extract current branch name
+ shell: bash
+ # bash pattern expansion to grab branch name without slashes
+ run: ref="${GITHUB_REF#refs/heads/}" && echo "branch=${ref////-}" >> $GITHUB_OUTPUT
+ id: ref
+
+ - name: Checkout sources
+ uses: actions/checkout@v6
+
+ - uses: actions/setup-java@v5
+ with:
+ distribution: temurin
+ java-version: 21
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v5
+ with:
+ cache-read-only: true
+
+ - name: Execute Gradle build
+ run: ./gradlew build -Pbuild.release=true
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v7
+ with:
+ name: sodium-artifacts-${{ steps.ref.outputs.ref }}
+ path: build/mods/*.jar
+
+ - name: Publish Tag Release to CaffeineMC Maven
+ env:
+ ORG_GRADLE_PROJECT_caffeineMCMavenUsername: ${{ secrets.CAFFEINEMC_MAVEN_USERNAME }}
+ ORG_GRADLE_PROJECT_caffeineMCMavenPassword: ${{ secrets.CAFFEINEMC_MAVEN_PASSWORD }}
+ if: ${{ env.ORG_GRADLE_PROJECT_caffeineMCMavenUsername != '' && env.ORG_GRADLE_PROJECT_caffeineMCMavenPassword != '' }}
+ run: ./gradlew publishAllPublicationsToCaffeineMCRepository -Pbuild.release=true
diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
new file mode 100644
index 0000000000..c16fc4e82d
--- /dev/null
+++ b/.github/workflows/publish-release.yml
@@ -0,0 +1,72 @@
+# Used when a developer decides to publish a release
+# This makes use of caching for faster builds and uploads the resulting artifacts
+name: Release on Platforms
+permissions:
+ contents: write # Needed to publish a GitHub release
+
+on:
+ workflow_dispatch:
+ inputs:
+ platform:
+ type: choice
+ description: Platform
+ options:
+ - both
+ - fabric
+ - neoforge
+ destination:
+ type: choice
+ description: Destination
+ options:
+ - GH+MR+CF
+ - GH+MR
+ - GH
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ environment:
+ name: prod
+
+ steps:
+ - name: Extract current branch name
+ shell: bash
+ # bash pattern expansion to grab branch name without slashes
+ run: ref="${GITHUB_REF#refs/heads/}" && echo "branch=${ref////-}" >> $GITHUB_OUTPUT
+ id: ref
+
+ - name: Checkout sources
+ uses: actions/checkout@v6
+
+ - uses: actions/setup-java@v5
+ with:
+ distribution: temurin
+ java-version: 21
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v5
+ with:
+ cache-read-only: true
+
+ - name: Execute Gradle build
+ run: ./gradlew build -Pbuild.release=true
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v7
+ with:
+ name: sodium-artifacts-${{ steps.ref.outputs.branch }}
+ path: build/mods/*.jar
+
+ - name: Publish Modrinth, Curseforge and Github Releases
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
+ MODRINTH_API_KEY: ${{ secrets.MODRINTH_API_KEY }}
+ run: ./gradlew publishMods -Pbuild.release=true -Pbuild.release.platform=${{ inputs.platform }} -Pbuild.release.destination=${{ inputs.destination }}
+
+ - name: Publish Tag Release to CaffeineMC Maven
+ env:
+ ORG_GRADLE_PROJECT_caffeineMCMavenUsername: ${{ secrets.CAFFEINEMC_MAVEN_USERNAME }}
+ ORG_GRADLE_PROJECT_caffeineMCMavenPassword: ${{ secrets.CAFFEINEMC_MAVEN_PASSWORD }}
+ if: ${{ env.ORG_GRADLE_PROJECT_caffeineMCMavenUsername != '' && env.ORG_GRADLE_PROJECT_caffeineMCMavenPassword != '' }}
+ run: ./gradlew publishAllPublicationsToCaffeineMCRepository -Pbuild.release=true
diff --git a/.gitignore b/.gitignore
index 69cd50978c..ec97f0641b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,6 @@ neoforge/runs
.DS_Store
.AppleDouble
.LSOverride
+
+# Kotlin compiler working directory
+.kotlin
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000..8db93dd5e8
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,28 @@
+[ReleaseTag]() is automatically replaced with the release tag, e.g. mc26.1-0.8.9
+[MCVersion]() is automatically replaced with the minecraft version, e.g. 26.1
+[SodiumVersion]() is automatically replaced with the sodium version, e.g. 0.8.9
+Everything above the line is ignored and not included in the changelog. Everything below will be in the
+changelog on GitHub, Modrinth and CurseForge.
+----------
+### Overview
+Sodium [SodiumVersion]() is a backport of modern Sodium 0.8 to Minecraft [MCVersion]().
+
+- Significantly improved the performance of rendering the world (up to +115%) on some computers.
+- Greatly improved the rendering of transparent objects with complex models, especially when submerged in water.
+- Lots and lots of improvements to the user experience in the Video Settings menu.
+- Reduced latency and micro-stutter when updating chunks in the world.
+- Slightly faster entity rendering, especially for transparent mobs and particles.
+- Improvements for hardware and mod compatibility.
+- ...And many more bug fixes and improvements...
+
+### Using and Building on This Release
+It includes the backport of our Config API and other conventions that will hopefully make it easier for mods to interact with Sodium across multiple versions. This release series doesn't get released at the same cadence as our current releases for Minecraft 26.2 and 26.1, and doesn't follow the same alpha/beta numbering. Mod developers can find our artifacts, such as the Config API, on [our Maven repository](https://maven.caffeinemc.net/).
+
+Mod compatibility:
+- Create Aeronautics works as of 1.3.0
+- Sable works as of 2.0.0
+- Veil works as of 4.1.2
+- Iris works as of 1.8.13
+- Not compatible with any version of "Sodium Options API", which is not affiliated with us, and any mods that depend on it.
+
+Some other mods may still be incompatible with this release. If your modpack doesn't work with this release, downgrade to the version of Sodium it was built with and choose appropriate versions of other mods. New or updated modpacks are encouraged to use the latest version of Sodium and compatible mods.
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 60221b425c..9ca9757920 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -3,6 +3,10 @@
When submitting a pull request, you are granting [JellySquid](https://jellysquid.me) the right to license
your contributions under the [Polyform Shield License (Version 1.0.0)](LICENSE.md).
+Without exception, it is required that all code being submitted to the project is of your original work. We
+will not accept pull requests which contain *any amount* of AI-generated code, regardless of the particular
+circumstances.
+
If you have any questions about these terms, please [get in contact with us](https://caffeinemc.net/discord).
### Code Style
@@ -56,4 +60,4 @@ style guidelines.
If you're adding new Mixin patches to the project, please ensure that you have created appropriate entries to disable
them in the config file. Mixins should always be self-contained and grouped into "patch sets" which are easy to isolate,
-and where that is not possible, they should be placed into the "core" package.
\ No newline at end of file
+and where that is not possible, they should be placed into the "core" package.
diff --git a/README.md b/README.md
index e80d00e483..ab9929ea6a 100644
--- a/README.md
+++ b/README.md
@@ -5,25 +5,49 @@
Sodium is a powerful rendering engine and optimization mod for the Minecraft client which improves frame rates and reduces
micro-stutter, while fixing many graphical issues in Minecraft.
-### 📥 Installation
+**This mod is the result of thousands of hours of development, and is made possible thanks to players like you.** If you
+would like to show a token of your appreciation for my work, and help support the development of Sodium in the process,
+then consider [buying me a coffee](https://caffeinemc.net/donate).
-The latest version of Sodium can be downloaded from our official [Modrinth](https://modrinth.com/mod/sodium) and
-[CurseForge](https://www.curseforge.com/minecraft/mc-mods/sodium) pages.
+
+
+---
+
+### 📥 Downloads
+
+#### Stable builds
+
+The latest stable release of Sodium can be downloaded from our official [Modrinth](https://modrinth.com/mod/sodium) and
+[CurseForge](https://www.curseforge.com/minecraft/mc-mods/sodium) pages.
+
+#### Nightly builds (for developers)
+
+We also provide bleeding-edge builds ("nightlies") which are useful for testing the very latest changes before they're
+packaged into a release. These builds are primarily intended for other mod developers and users with expert skills, and do
+not come with any support or warranty.
+
+For a complete listing of available nightly builds, please see [the wiki page](https://github.com/CaffeineMC/sodium/wiki/Nightly-Builds). We also have a Maven repository for including Sodium in your development workspace or build process, for which you can also find [documentation on our wiki](https://github.com/CaffeineMC/sodium/wiki/CaffeineMC-Maven-&-Config-API).
+
+### 🖥️ Installation
Since the release of Sodium 0.6.0, both the _Fabric_ and _NeoForge_ mod loaders are supported. We generally recommend
that new users prefer to use the _Fabric_ mod loader, since it is more lightweight and stable (for the time being.)
For more information about downloading and installing the mod, please refer to our [Installation Guide](https://github.com/CaffeineMC/sodium/wiki/Installation).
-### 🐛 Reporting Issues
+### 🙇 Getting Help
+
+For technical support (including help with mod installation problems and game crashes), please use our
+[official Discord server](https://caffeinemc.net/discord).
-You can report bugs and crashes by opening an issue on our [issue tracker](https://github.com/CaffeineMC/sodium/issues).
-Before opening a new issue, use the search tool to make sure that your issue has not already been reported and ensure
-that you have completely filled out the issue template. Issues that are duplicates or do not contain the necessary
-information to triage and debug may be closed.
+### 📬 Reporting Issues
+
+If you do not need technical support and would like to report an issue (bug, crash, etc.) or otherwise request changes
+(for mod compatibility, new features, etc.), then we encourage you to open an issue on the
+[project issue tracker](https://github.com/CaffeineMC/sodium/issues).
Please note that while the issue tracker is open to feature requests, development is primarily focused on
-improving hardware compatibility, performance, and finishing any unimplemented features necessary for parity with
+improving compatibility, performance, and finishing any unimplemented features necessary for parity with
the vanilla renderer.
### 💬 Join the Community
@@ -36,45 +60,40 @@ We have an [official Discord community](https://caffeinemc.net/discord) for all
## ✅ Hardware Compatibility
-We only provide support for graphics cards which have up-to-date drivers for OpenGL 4.6. Most graphics cards which have
-been released since year 2010 are supported, such as the...
+We only provide official support for graphics cards which have up-to-date drivers that are compatible with OpenGL 4.5
+or newer. Most graphics cards released in the past 12 years will meet these requirements, including the following:
- AMD Radeon HD 7000 Series (GCN 1) or newer
- NVIDIA GeForce 400 Series (Fermi) or newer
- Intel HD Graphics 500 Series (Skylake) or newer
-In some cases, older graphics cards may also work (so long as they have up-to-date drivers which have support for
-OpenGL 3.3), but they are not officially supported, and may not be compatible with future versions of Sodium.
+Nearly all graphics cards that are already compatible with Minecraft (which requires OpenGL 3.3) should also work
+with Sodium. But our team cannot ensure compatibility or provide support for older graphics cards, and they may
+not work with future versions of Sodium.
#### OpenGL Compatibility Layers
-Devices which need to use OpenGL translation layers (such as GL4ES, ANGLE, etc) are not supported and will very likely
-not work with Sodium. These translation layers do not implement required functionality and they suffer from underlying
+Devices which need to use OpenGL translation layers (such as GL4ES, ANGLE, etc.) are not supported and will very likely
+not work with Sodium. These translation layers do not implement required functionality, and they suffer from underlying
driver bugs which cannot be worked around.
-## 🛠️ Developer Guide
-
-### Building from sources
+## 🛠️ Building from sources
-Sodium uses a typical Gradle project structure and can be compiled by simply running the default `build` task. The build
-artifacts (typical mod binaries, and their sources) can be found in the `build/libs` directory.
+Sodium uses the [Gradle build tool](https://gradle.org/) and can be built with the `gradle build` command. The build
+artifacts (production binaries and their source bundles) can be found in the `build/mods` directory.
-#### Requirements
+The [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html#sec:using_wrapper) is provided for ease of use and will automatically download and install the
+appropriate version of Gradle for the project build. To use the Gradle wrapper, substitute `gradle` in build commands
+with `./gradlew.bat` (Windows) or `./gradlew` (macOS and Linux).
-We recommend using a package manager (such as [SDKMAN](https://sdkman.io/)) to manage toolchain dependencies and keep
-them up to date. For many Linux distributions, these dependencies will be standard packages in your software
-repositories.
+### Build Requirements
- OpenJDK 21
- - We recommend using the [Eclipse Temurin](https://adoptium.net/) distribution, as it's known to be high quality
- and to work without issues.
-- Gradle 8.6.x (optional)
- - The [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html#sec:using_wrapper) is provided
- in this repository can be used instead of installing a suitable version of Gradle yourself. However, if you are
- building many projects, you may prefer to install it yourself through a suitable package manager as to save disk
- space and to avoid many different Gradle daemons sitting around in memory.
+ - We recommend using the [Eclipse Temurin](https://adoptium.net/) distribution as it's regularly tested by our developers and known
+ to be of high quality.
+- Gradle 8.10.x
- Typically, newer versions of Gradle will work without issues, but the build script is only tested against the
- version specified by the wrapper script.
+ version used by the [wrapper script](/gradle/wrapper/gradle-wrapper.properties).
## 📜 License
diff --git a/build.gradle.kts b/build.gradle.kts
index e69de29bb2..7f89a9f9be 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -0,0 +1,97 @@
+import me.modmuss50.mpp.ReleaseType
+import me.modmuss50.mpp.platforms.curseforge.CurseforgeOptions
+import me.modmuss50.mpp.platforms.modrinth.ModrinthOptions
+import java.util.*
+
+plugins {
+ id("me.modmuss50.mod-publish-plugin") version("1.1.0")
+}
+
+gradle.projectsEvaluated {
+ publishMods {
+ if (!project.hasProperty("build.release")) {
+ return@publishMods println("Publishing is disabled, please use the CI publishing workflow")
+ }
+
+ val releasePlatform: String = project.providers.gradleProperty("build.release.platform").orNull
+ ?: return@publishMods println("build.release.platform must be defined (expected: both, fabric, neoforge)")
+
+ val releaseDestination: String = project.providers.gradleProperty("build.release.destination").orNull
+ ?: return@publishMods println("build.release.destination must be defined (expected: GH+MR+CF, GH+MR, GH)")
+ val publishModrinth = releaseDestination.contains("MR")
+ val publishCurseforge = releaseDestination.contains("CF")
+
+ val modVersion = BuildConfig.createVersionString(project);
+
+ type = when {
+ modVersion.contains("alpha") -> ReleaseType.ALPHA
+ modVersion.contains("beta") -> ReleaseType.BETA
+ else -> ReleaseType.STABLE
+ }
+ changelog = BuildConfig.getChangelog(project)
+
+ val curseforgeShared = curseforgeOptions {
+ accessToken = project.providers.environmentVariable("CURSEFORGE_API_KEY")
+ projectId = BuildConfig.CURSEFORGE_PROJECT_ID
+ minecraftVersions.add(BuildConfig.MINECRAFT_VERSION)
+ }
+
+ val modrinthShared = modrinthOptions {
+ accessToken = project.providers.environmentVariable("MODRINTH_API_KEY")
+ projectId = BuildConfig.MODRINTH_PROJECT_ID
+ minecraftVersions.add(BuildConfig.MINECRAFT_VERSION)
+ }
+
+ setupFor("Fabric", releasePlatform, publishCurseforge, publishModrinth, curseforgeShared, modrinthShared)
+ setupFor("NeoForge", releasePlatform, publishCurseforge, publishModrinth, curseforgeShared, modrinthShared)
+
+ github {
+ accessToken = project.providers.environmentVariable("GITHUB_TOKEN")
+ repository = "CaffeineMC/sodium"
+ commitish = BuildConfig.calculateGitHash(project)
+ version = BuildConfig.RELEASE_TAG
+ displayName = "Sodium ${BuildConfig.MOD_VERSION} for Minecraft ${BuildConfig.MINECRAFT_VERSION}"
+ file.unset()
+ file.unsetConvention()
+
+ allowEmptyFiles = true
+ }
+ }
+}
+
+fun me.modmuss50.mpp.ModPublishExtension.setupFor(loaderName: String, releasePlatform: String, publishCurseforge: Boolean, publishModrinth: Boolean, curseforgeOptions: Provider, modrinthOptions: Provider) {
+ val loaderLowercase = loaderName.lowercase(Locale.ROOT)
+
+ if (releasePlatform == "both" || releasePlatform == loaderLowercase) {
+ val taskName = if (loaderLowercase == "fabric") "remapJar" else "jar"
+ val jar = project(":$loaderLowercase").tasks.getByName(taskName).outputs.files.singleFile
+
+ val releaseTitle = "Sodium ${BuildConfig.MOD_VERSION} for $loaderName ${BuildConfig.MINECRAFT_VERSION}"
+ val releaseVersion = "${BuildConfig.RELEASE_TAG}-$loaderLowercase"
+
+ if (publishCurseforge) {
+ curseforge("curseforge$loaderName") {
+ from(curseforgeOptions)
+
+ file.set(jar)
+ displayName = releaseTitle
+ version = releaseVersion
+ modLoaders.add(loaderLowercase)
+
+ clientRequired = true
+ serverRequired = false
+ }
+ }
+
+ if (publishModrinth) {
+ modrinth("modrinth$loaderName") {
+ from(modrinthOptions)
+
+ file.set(jar)
+ displayName = releaseTitle
+ version = releaseVersion
+ modLoaders.add(loaderLowercase)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/BuildConfig.kt b/buildSrc/src/main/kotlin/BuildConfig.kt
index 9d9844cf3b..1a94f09ba8 100644
--- a/buildSrc/src/main/kotlin/BuildConfig.kt
+++ b/buildSrc/src/main/kotlin/BuildConfig.kt
@@ -2,16 +2,25 @@ import org.gradle.api.Project
object BuildConfig {
val MINECRAFT_VERSION: String = "1.21.1"
- val NEOFORGE_VERSION: String = "21.1.46"
- val FABRIC_LOADER_VERSION: String = "0.16.4"
- val FABRIC_API_VERSION: String = "0.103.0+1.21.1"
+ val NEOFORGE_VERSION: String = "21.1.228"
+ val FABRIC_LOADER_VERSION: String = "0.19.2"
+ val FABRIC_API_VERSION: String = "0.116.11+1.21.1"
// This value can be set to null to disable Parchment.
- // TODO: Re-add Parchment
- val PARCHMENT_VERSION: String? = null
+ val PARCHMENT_VERSION: String? = "2024.11.17"
// https://semver.org/
- var MOD_VERSION: String = "0.6.0-beta.2"
+ val MOD_VERSION: String = "0.8.12"
+
+ val MINECRAFT_VERSION_SHORT: String = MINECRAFT_VERSION
+ .replace("-snapshot-", "s")
+ .replace("-pre-", "p")
+ .replace("-rc-", "r")
+
+ val RELEASE_TAG: String = "mc$MINECRAFT_VERSION_SHORT-$MOD_VERSION"
+
+ val CURSEFORGE_PROJECT_ID = "394468"
+ val MODRINTH_PROJECT_ID = "AANobbMI"
fun createVersionString(project: Project): String {
val builder = StringBuilder()
@@ -23,10 +32,10 @@ object BuildConfig {
builder.append(MOD_VERSION)
} else {
builder.append(MOD_VERSION.substringBefore('-'))
- builder.append("-snapshot")
+ builder.append("-SNAPSHOT")
}
- builder.append("+mc").append(MINECRAFT_VERSION)
+ builder.append("+mc").append(MINECRAFT_VERSION_SHORT)
if (!isReleaseBuild) {
if (buildId != null) {
@@ -38,4 +47,21 @@ object BuildConfig {
return builder.toString()
}
-}
\ No newline at end of file
+
+ fun calculateGitHash(project: Project): String = try {
+ val output = project.providers.exec {
+ workingDir(project.projectDir)
+ commandLine("git", "rev-parse", "HEAD")
+ }
+ output.standardOutput.asText.get().trim()
+ } catch (_: Throwable) {
+ "unknown"
+ }
+
+ fun getChangelog(project: Project): String = project.rootProject.file("CHANGELOG.md").readText()
+ .split("----------")[1]
+ .trim()
+ .replace("[ReleaseTag]()", RELEASE_TAG)
+ .replace("[MCVersion]()", MINECRAFT_VERSION)
+ .replace("[SodiumVersion]()", MOD_VERSION)
+}
diff --git a/buildSrc/src/main/kotlin/multiloader-platform.gradle.kts b/buildSrc/src/main/kotlin/multiloader-platform.gradle.kts
index 006f9f3500..55b89b8cba 100644
--- a/buildSrc/src/main/kotlin/multiloader-platform.gradle.kts
+++ b/buildSrc/src/main/kotlin/multiloader-platform.gradle.kts
@@ -16,7 +16,7 @@ tasks {
inputs.property("version", version)
filesMatching(listOf("fabric.mod.json", "META-INF/neoforge.mods.toml")) {
- expand(mapOf("version" to version))
+ expand(mapOf("version" to inputs.properties["version"]))
}
}
@@ -31,13 +31,23 @@ tasks {
}
publishing {
- publications {
- create("maven") {
- groupId = project.group as String
- artifactId = project.name as String
- version = version
-
- from(components["java"])
+ // Each platform is responsible for their own "publications".
+
+ repositories {
+ val isReleaseBuild = project.hasProperty("build.release")
+ val caffeineMCMavenUsername: String? by project // reads from ORG_GRADLE_PROJECT_caffeineMCMavenUsername
+ val caffeineMCMavenPassword: String? by project // reads from ORG_GRADLE_PROJECT_caffeineMCMavenPassword
+
+ maven {
+ name = "CaffeineMC"
+ url = uri("https://maven.caffeinemc.net".let {
+ if (isReleaseBuild) "$it/releases" else "$it/snapshots"
+ })
+
+ credentials {
+ username = caffeineMCMavenUsername
+ password = caffeineMCMavenPassword
+ }
}
}
}
diff --git a/common/build.gradle.kts b/common/build.gradle.kts
index cec4b9fdeb..d9532ec6e6 100644
--- a/common/build.gradle.kts
+++ b/common/build.gradle.kts
@@ -2,7 +2,7 @@ plugins {
id("multiloader-base")
id("java-library")
- id("fabric-loom") version ("1.8.9")
+ id("net.fabricmc.fabric-loom-remap") version ("1.16.1")
}
base {
@@ -16,7 +16,7 @@ val configurationPreLaunch = configurations.create("preLaunchDeps") {
sourceSets {
val main = getByName("main")
val api = create("api")
- val workarounds = create("workarounds")
+ val boot = create("boot")
api.apply {
java {
@@ -24,7 +24,7 @@ sourceSets {
}
}
- workarounds.apply {
+ boot.apply {
java {
compileClasspath += configurationPreLaunch
}
@@ -33,15 +33,19 @@ sourceSets {
main.apply {
java {
compileClasspath += api.output
- compileClasspath += workarounds.output
+ compileClasspath += boot.output
}
}
create("desktop")
}
+repositories {
+ mavenLocal()
+}
+
dependencies {
- minecraft(group = "com.mojang", name = "minecraft", version = BuildConfig.MINECRAFT_VERSION)
+ minecraft("com.mojang:minecraft:${BuildConfig.MINECRAFT_VERSION}")
mappings(loom.layered {
officialMojangMappings()
@@ -50,8 +54,8 @@ dependencies {
}
})
- compileOnly("io.github.llamalad7:mixinextras-common:0.3.5")
- annotationProcessor("io.github.llamalad7:mixinextras-common:0.3.5")
+ compileOnly("io.github.llamalad7:mixinextras-common:0.5.3")
+ annotationProcessor("io.github.llamalad7:mixinextras-common:0.5.3")
compileOnly("net.fabricmc:sponge-mixin:0.13.2+mixin.0.8.5")
compileOnly("net.fabricmc:fabric-loader:${BuildConfig.FABRIC_LOADER_VERSION}")
@@ -69,13 +73,12 @@ dependencies {
// We need to be careful during pre-launch that we don't touch any Minecraft classes, since other mods
// will not yet have an opportunity to apply transformations.
- configurationPreLaunch("org.apache.commons:commons-lang3:3.14.0")
- configurationPreLaunch("commons-io:commons-io:2.15.1")
configurationPreLaunch("org.lwjgl:lwjgl:3.3.3")
+ configurationPreLaunch("org.lwjgl:lwjgl-opengl:3.3.3")
configurationPreLaunch("net.java.dev.jna:jna:5.14.0")
configurationPreLaunch("net.java.dev.jna:jna-platform:5.14.0")
configurationPreLaunch("org.slf4j:slf4j-api:2.0.9")
- configurationPreLaunch("org.jetbrains:annotations:25.0.0")
+ configurationPreLaunch("org.jetbrains:annotations:24.1.0")
}
loom {
@@ -98,6 +101,21 @@ fun exportSourceSetJava(name: String, sourceSet: SourceSet) {
}
}
+fun exportSourceSetSources(name: String, sourceSet: SourceSet) {
+ val configuration = configurations.create("${name}Sources") {
+ isCanBeResolved = true
+ isCanBeConsumed = true
+ }
+
+ val compileTask = tasks.register(sourceSet.getTaskName("process", "sources")) {
+ from(sourceSet.allSource)
+ into(file(project.layout.buildDirectory).resolve("sources").resolve(sourceSet.name))
+ }.get()
+ artifacts.add(configuration.name, compileTask.destinationDir) {
+ builtBy(compileTask)
+ }
+}
+
fun exportSourceSetResources(name: String, sourceSet: SourceSet) {
val configuration = configurations.create("${name}Resources") {
isCanBeResolved = true
@@ -118,13 +136,14 @@ fun exportSourceSetResources(name: String, sourceSet: SourceSet) {
// Exports the compiled output of the source set to the named configuration.
fun exportSourceSet(name: String, sourceSet: SourceSet) {
exportSourceSetJava(name, sourceSet)
+ exportSourceSetSources(name, sourceSet)
exportSourceSetResources(name, sourceSet)
}
exportSourceSet("commonMain", sourceSets["main"])
exportSourceSet("commonApi", sourceSets["api"])
-exportSourceSet("commonEarlyLaunch", sourceSets["workarounds"])
+exportSourceSet("commonBoot", sourceSets["boot"])
exportSourceSet("commonDesktop", sourceSets["desktop"])
tasks.jar { enabled = false }
-tasks.remapJar { enabled = false }
\ No newline at end of file
+tasks.remapJar { enabled = false }
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/blockentity/BlockEntityRenderHandler.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/blockentity/BlockEntityRenderHandler.java
index b5de855a17..5a1e7054af 100644
--- a/common/src/api/java/net/caffeinemc/mods/sodium/api/blockentity/BlockEntityRenderHandler.java
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/blockentity/BlockEntityRenderHandler.java
@@ -1,7 +1,5 @@
package net.caffeinemc.mods.sodium.api.blockentity;
-import java.util.function.Predicate;
-
import net.caffeinemc.mods.sodium.api.internal.DependencyInjection;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPoint.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPoint.java
new file mode 100644
index 0000000000..ecb89e228f
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPoint.java
@@ -0,0 +1,29 @@
+package net.caffeinemc.mods.sodium.api.config;
+
+import net.caffeinemc.mods.sodium.api.config.structure.ConfigBuilder;
+
+/**
+ * Entry point interface for registering configuration structure. Typically, mods will only need the "late" registration method unless they need to do something very early in the mod loading process and need their options to be resolved before that point.
+ *
+ * The {@link ConfigBuilder} instance passed to these methods already knows which mod is registering the configuration, so there is typically no need to specify the mod metadata manually.
+ */
+public interface ConfigEntryPoint {
+ /**
+ * Register configuration structure early in the mod loading process.
+ * This method is called before most mods have had a chance to initialize.
+ * Use this method only if you need your configuration options to be available very early.
+ *
+ * @param builder The configuration builder to register options with.
+ */
+ default void registerConfigEarly(ConfigBuilder builder) {
+ }
+
+ /**
+ * Register configuration structure later in the mod loading process.
+ * This method is called after most mods have initialized.
+ * This is the preferred method for registering configuration options.
+ *
+ * @param builder The configuration builder to register options with.
+ */
+ void registerConfigLate(ConfigBuilder builder);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPointForge.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPointForge.java
new file mode 100644
index 0000000000..b56265a19a
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPointForge.java
@@ -0,0 +1,22 @@
+package net.caffeinemc.mods.sodium.api.config;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a class as a configuration entry point for Sodium on the NeoForge platform.
+ * This annotation should be placed on classes that implement the configuration
+ * entry point interface to associate them with a specific mod id.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ConfigEntryPointForge {
+ /**
+ * The mod id to associate this config entrypoint's "owner" with.
+ *
+ * @return the mod id
+ */
+ String value();
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigState.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigState.java
new file mode 100644
index 0000000000..f823841a46
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigState.java
@@ -0,0 +1,44 @@
+package net.caffeinemc.mods.sodium.api.config;
+
+import net.minecraft.resources.ResourceLocation;
+
+/**
+ * Represents the current state of configuration options. This interface is accessed through dynamic value providers throughout the API. Only declared dependencies of a dynamic value provider are allowed to be queried (and doing otherwise will result in a crash).
+ */
+public interface ConfigState {
+ /**
+ * Special option ID to be passed as a dependency of a dynamic value provider to indicate that the provider should be re-evaluated when the configuration screen is rebuilt. It is unlikely that you need to use this.
+ */
+ ResourceLocation UPDATE_ON_REBUILD = ResourceLocation.parse("__meta__:update_on_rebuild");
+
+ /**
+ * Special option ID to be passed as a dependency of a dynamic value provider to indicate that the provider should be re-evaluated when the value for the parent option is applied. This allows the dynamic value to read the value of the option itself, which would be an error otherwise. It does not allow reading other options unless they are also declared as dependencies.
+ */
+ ResourceLocation UPDATE_ON_APPLY = ResourceLocation.parse("__meta__:update_on_apply");
+
+ /**
+ * Reads a boolean option from the configuration state.
+ *
+ * @param id The ID of the option.
+ * @return The current value of the boolean option.
+ */
+ boolean readBooleanOption(ResourceLocation id);
+
+ /**
+ * Reads an integer option from the configuration state.
+ *
+ * @param id The ID of the option.
+ * @return The current value of the integer option.
+ */
+ int readIntOption(ResourceLocation id);
+
+ /**
+ * Reads an enum option from the configuration state.
+ *
+ * @param id The ID of the option.
+ * @param enumClass The class of the enum.
+ * @param The enum type.
+ * @return The current value of the enum option.
+ */
+ > E readEnumOption(ResourceLocation id, Class enumClass);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/StorageEventHandler.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/StorageEventHandler.java
new file mode 100644
index 0000000000..9555ec63e7
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/StorageEventHandler.java
@@ -0,0 +1,12 @@
+package net.caffeinemc.mods.sodium.api.config;
+
+/**
+ * Functional interface for handling storage events, such as after a configuration has been saved.
+ */
+@FunctionalInterface
+public interface StorageEventHandler {
+ /**
+ * Called after options have been saved to their bindings. This is typically used for flushing changes to disk.
+ */
+ void afterSave();
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/USAGE.md b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/USAGE.md
new file mode 100644
index 0000000000..42e71fc7b1
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/USAGE.md
@@ -0,0 +1,272 @@
+# Usage of the Sodium Config API
+
+The Sodium Config API lets mods add their own pages to the Video Settings screen, which Sodium replaces with its own screen.
+
+If you encounter difficulties using the API, find bugs in it, or it's missing features you need, don't hesitate to contact us or make a contribution directly.
+
+## Scope
+
+The Sodium Config API is intended for mods that add video settings, not as a general purpose config API. For general purpose configuration, use the platform's appropriate mod list and a config library.
+
+As a presentation API, it does not handle loading, parsing, or saving configuration data to files. It is up to your mod to handle that on its own, or use a third-party library dedicated to that task.
+
+## Overview
+
+Sodium redirects Minecraft's "Video Settings" screen to its own screen. Historically, third-party mods have mixed into Sodium to add buttons to their own settings pages or additional options to Sodium's pages.
+
+With this API, these mods will not need to touch Sodium's internals anymore and should be able to operate independently of the GUI's implementation details. The API may not be able to cover all use cases where mods mixed into Sodium's options code, but it should cover most of the common ones.
+
+Registration of options happens in two stages: Early and late. Early registration happens when Sodium initializes its own early options before the window is created. Late registration happens after the game launched. Most mods will only need to use late registration. These stages are independent and only options that are registered in the late stage will show up in the GUI.
+
+### Features of the API
+
+Here's a summary of the features this config API provides:
+
+- a list of option pages per mod
+ - ID, name, and version
+ - a theme color
+ - optionally a square monochrome icon
+- a list of option groups per page
+ - optionally named option groups
+ - external pages that simply open a mod-defined `Screen`
+- a list of options per group
+- options of various types
+ - `ResourceIdentifiers` as IDs
+ - types: integer slider, enum, boolean, external (opens a new `Screen`)
+ - value bindings through callbacks
+ - dynamic values for enablement, default value, allowed values
+ - dynamic values may depend on other option's values
+ - cycle and declared dependency consistency checking
+ - value formatters for presentation
+ - value-dependent tooltips
+ - flags for renderer reload and other graphics events
+- all user facing strings are translatable
+- storage providers are flushed after changes are applied
+- early registration for mods that need to have their options available before the window is created
+- entrypoint-based config registration
+- declarative builder-based style
+- option overlay or replacement to modify existing options
+ - overlaying only changes some properties of another option
+- custom flags to perform actions after certain changes
+
+## Getting Started
+
+### Dependency on Sodium's API
+
+Sodium publishes its api package on a maven repository that you can depend on in your buildscript. It needs `modImplementation` on Fabric 1.21.11, and `implementation` on Fabric 1.21.12+ and NeoForge 1.21.11+.
+
+Fabric:
+
+```groovy
+dependencies {
+ // ... other dependencies
+
+ // using a tagged API build
+ modImplementation "net.caffeinemc:sodium-fabric-api:0.8.0+mc1.21.11"
+ // OR using a snapshot build
+ modImplementation "net.caffeinemc:sodium-fabric-api:0.8.0-SNAPSHOT+mc1.21.11-pre3-build.773"
+}
+```
+
+NeoForge:
+
+```groovy
+dependencies {
+ // ... other dependencies
+
+ // using a tagged API build
+ implementation "net.caffeinemc:sodium-neoforge-api:0.8.0+mc1.21.11"
+ // OR using a snapshot build
+ implementation "net.caffeinemc:sodium-neoforge-api:0.8.0-SNAPSHOT+mc1.21.11-pre3-build.773"
+}
+```
+
+Make sure you have our Maven repository declared:
+
+```groovy
+maven {
+ name "CaffeineMC"
+ url "https://maven.caffeinemc.net/releases" // or /snapshots
+}
+```
+Kotlin:
+```kotlin
+maven {
+ name = "CaffeineMC"
+ url = uri("https://maven.caffeinemc.net/releases") // or /snapshots
+}
+```
+
+The Maven Repository has a web frontend that you can use to view the list of available versions: https://maven.caffeinemc.net
+
+### Creating an Entrypoint
+
+Entrypoint classes that Sodium calls to run your options registration code can be declared either in your mod's metadata file, or on NeoForge with a special annotation.
+
+#### With a Metadata Entry
+
+Metadata-based entrypoints use the key `sodium:config_api_user` and the value is the full reference to a class that implements the `net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint` interface.
+
+Fabric `fabric.mod.json`:
+
+```json5
+{
+ "entrypoints": {
+ // ... other entrypoints
+
+ "sodium:config_api_user": [
+ "com.example.examplemod.ExampleModConfigBuilder"
+ ]
+ }
+}
+```
+
+NeoForge `neoforge.mods.toml`:
+```toml
+[modproperties.examplemod]
+"sodium:config_api_user" = "com.example.examplemod.ExampleModConfigBuilder"
+```
+
+The implementation of the entrypoint can look something like this:
+
+```java
+package com.example.examplemod;
+
+import net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint;
+import net.caffeinemc.mods.sodium.api.config.structure.ConfigBuilder;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+
+public class ExampleConfigUser implements ConfigEntryPoint {
+ private final OptionStorage storage = new OptionStorage();
+ private final Runnable handler = this.storage::flush; // typically gets referenced many times
+
+ @Override
+ public void registerConfigLate(ConfigBuilder builder) {
+ builder.registerOwnModOptions()
+ .setIcon(ResourceLocation.parse("examplemod:textures/gui/icon.png"))
+ .addPage(builder.createOptionPage()
+ .setName(Component.literal("Example Page"))
+ .addOptionGroup(builder.createOptionGroup()
+ .setName(Component.literal("Example Group")) // only if necessary for clarity
+ .addOption(builder.createBooleanOption(ResourceLocation.parse("examplemod:example_option"))
+ .setName(Component.literal("Example Option")) // use translation keys here
+ .setTooltip(Component.literal("Example tooltip"))
+ .setStorageHandler(this.handler)
+ .setBinding(this.storage::setExampleOption, this.storage::getExampleOption)
+ .setDefaultValue(true)
+ )
+ )
+ );
+ }
+}
+
+// OptionStorage.java
+class OptionStorage {
+ private boolean exampleOption = true;
+
+ public boolean getExampleOption() {
+ return this.exampleOption;
+ }
+
+ public void setExampleOption(boolean value) {
+ this.exampleOption = value;
+ }
+
+ public void flush() {
+ // flush options to config file
+ }
+}
+```
+
+#### NeoForge: With an Annotation
+
+Since NeoForge has the convention of using annotations for entrypoints, this option is provided as an alternative. Any classes annotated with `@ConfigEntryPointForge("examplemod")` will be loaded as config entrypoints too. Note that the annotation must be given the mod id that should be associated as the default mod for which a config is registered with `ConfigBuilder.registerOwnModOptions`. This is necessary as it's otherwise impossible to uniquely determine which mod a class is associated with on NeoForge.
+
+```java
+import net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint;
+import net.caffeinemc.mods.sodium.api.config.ConfigEntryPointForge;
+
+@ConfigEntryPointForge("examplemod")
+public class ExampleConfigUser implements ConfigEntryPoint {
+ // class body identical to the above
+}
+```
+
+### Registering Your Options
+
+Each mod adds a page for its options, within each page there are groups of options, and each group contains a list of options. Each option has an id, a name, a tooltip, a storage handler, a binding, and a default value. There are three types of options: boolean (tickbox), integer (slider), and enum. Optionally, all types of options can be disabled, while integer and enum options can have their allowed values restricted. Those two types also require you to set a function that assigns a label to each selected value.
+
+Some attributes of an option can be provided dynamically, meaning the returned value can depend on the state of another option. The default value, option enablement, and the allowed values can be computed dynamically. The methods for setting a dynamic value provider also require you to specify a list of dependencies, which are the resource locations of the options that the dynamic value provider reads from. Since dynamically evaluated attributes may change the state of an option, cyclic dependencies will lead to option registration failing and the game crashing.
+
+Sodium constructs one instance of the entrypoint class, and then calls the early and late registration methods at the right time.
+
+### Icon Design
+
+The UI is designed with monochrome, binary-alpha icons in mind. These icons are tinted the color of each mod's theme. We recommend that API users avoid displaying an icon that isn't appropriately styled. However, users can choose to deviate from this convention by presenting a regular, full-color icon using the means provided in the API.
+
+It is not necessary to explicitly set a theme color, as the system will automatically select one from a predefined set. However, if you set a custom theme color, it should have a minimum level of saturation to remain true to its purpose as an accent color. For aesthetic and accessibility reasons, it is also not reasonable to set the brightness/value of the theme color to be too dark. The theme base color should also not be too bright, otherwise the highlight color, which is brighter, will have no contrast against it.
+
+## API Notes
+
+The API is largely self-explanatory and an example is provided above. Also see Sodium's own options registration for a more in-depth example of the API's usage.
+
+### Using `ConfigBuilder` and `ModOptions`
+
+The `ConfigBuilder` instance passed to the registration method allows quick and easy registration of a mod's own options using `ConfigBuilder.registerOwnModOptions`. The mod's id, name, version or a formatter for the existing version, and the color theme can be configured on the returned `ModOptionsBuilder`. It's also possible to register options for additional mods using `ConfigBuilder.registerModOptions`. Which mod is the "own" mod for `registerOwnModOptions` is determined by the mod that owns the metadata-based entrypoint or the mod id passed to the `@ConfigEntryPointForge("examplemod")` annotation.
+
+Each registered mod gets its own header in the page list. The color of the header and the corresponding entries is randomly selected from a predefined list by default, but can be customized using `ModOptionsBuilder.setColorTheme`. A color theme is created either by specifying three (A)RGB colors or a single base color with the lighter and darker colors getting derived automatically. A mod can also specify an icon with `ModOptionsBuilder.setIcon`, which takes a `ResourceLocation` pointing to a texture, which will be tinted in the theme color and rendered in its entirety as a square. Icons set with `ModOptionsBuilder.setNonTintedIcon` are not tinted.
+
+To simply switch to a new `Screen` when an entry in the video settings screen's page list is clicked, use `ConfigBuilder.createExternalPage` and add the returned page normally after configuring it with a name and a `Consumer` that receives the current screen and switches to your custom screen.
+
+### Using `OptionBuilder`
+
+The storage handler set with `OptionBuilder.setStorageHandler` is called after changes have been made to the options through the bindings. This lets you flush the changes to the config file once, instead of every time an option is changed.
+
+The tooltip set with `OptionBuilder.setTooltip` can optionally be a function that generates a tooltip depending on the option's current value. This is useful for enum options for which the description would be too long otherwise.
+
+Optionally a performance impact can be specified with `OptionBuilder.setImpact` where the impact ranges from low to high (or "varies").
+
+Flags set with `OptionBuilder.setFlags` control what things are reset when this option is applied. They include reloading chunks or reloading resource packs. See `OptionFlag` for the available values.
+
+The default value set with `OptionBuilder.setDefaultValue`, or dynamically with `OptionBuilder.setDefaultProvider`, is used if the value returned by the binding does not fulfill the option's value constraint (in the case of an integer or enum option).
+
+Disabling an option with `OptionBuilder.setEnabled(false)` shows the option as strikethrough and makes it non-interactive. Otherwise, especially with regard to value constraints, it will behave as usual.
+
+The binding configured with `OptionBuilder.setBinding` is called when changes to the options have been made and are applied, or when the value no longer fulfills the option's constraints and is reset to the default value. It's also used to initially load a value during initialization.
+
+The hook given with `OptionBuilder.setApplyHook` is run after the option has been saved if its valued changed. The hook is given access to the current `ConfigState`, without needing to declare dependencies since the hook does not return anything. This is equivalent to making a singleton custom flag for this option and registering a flag hook for it, and it is implemented this way internally.
+
+### Using `? extends OptionBuilder`
+
+Some of the attributes of an option are required and you must set them, or registration will fail. The concrete extensions of `OptionBuilder` for each of the option types have some additional methods for configuring type-specific things, some of which are also required. Notably, `EnumOptionBuilder.setElementNameProvider` and `IntegerOptionBuilder.setValueFormatter` are required in order to display these types of options. The method `setValueFormatter` for integer options takes a `ControlValueFormatter`, which simply formats a number as a `Component`. Many standard value formatters are provided in `ControlValueFormatterImpls` (not part of the API package).
+
+Integer and enum options can have value constraints that restrict the set of allowed values the user can select. For integer options, a `Range` must be configured with `IntegerOptionBuilder.setRange` so that the slider's start, end, and step positions are well-defined. Enum options may be configured to only allow the selection of certain elements with `EnumOptionBuilder.setAllowedValues`.
+
+### Using Option Overlays and Replacement
+
+It's a typical use case to want to change one of Sodium's or another mod's options to have a different range or behavior. This can be achieved in one of two ways: by replacing the option or by applying an overlay to it.
+
+#### Replacement
+
+You can replace another option with `ModOptionsBuilder.registerOptionReplacement` by supplying a target identifier and a new option built as usual. The ID of the new option replaces the ID of the old option, and you can optionally choose to adopt the old option's ID to remain a target for overlays targeting it.
+
+#### Overlays
+
+Changing only some aspects of an option is possible through `ModOptionsBuilder.registerOptionOverlay`. You supply a partial option by using the same option builder as one usually does. The properties of the target options are replaced with the new ones if specified.
+
+### Custom Flags
+
+By registering a flag hook with `ModOptionsBuilder.registerFlagHook` you can run code after a flag from the specified set is triggered. After an option is applied and its value has been saved, all of its attached flags are triggered. The st of trigger flags for `registerFlagHook` can include custom flags, which are simply an arbitrary `Identifier`, or one of the meta flags, such as `OptionFlag.REQUIRES_RENDERER_RELOAD.getId()`.
+
+### Dependent Values
+
+To use a dynamic value method of the form `set___Provider`, you need to pass it a value provider. This provider is called with a reference to the current state of the options store. In order to be allowed to read the values of other options from it, you need to pass the IDs of the options this provider depends on as arguments to the method. Reading from options that are not declared as a dependency throws an exception. The system makes sure no dynamic value of an option depends on its own value or other options' values that indirectly depend on the first option, i.e. cycles are forbidden.
+
+#### Special Dependencies
+
+Providers that declare a dependency on `ConfigState.UPDATE_ON_REBUILD` are refreshed when the UI is rebuilt.
+
+Providers that declare a dependency on `ConfigState.UPDATE_ON_APPLY` are refreshed when the modified value of the option they are attached to is applied. This is the only situation in which a dynamic valued property of an option is allowed to depend on its containing options' value.
+
+Both of these special dependencies are typically not needed and making use of them is most likely user error. Sodium only uses them for making sure the extremely weird GUI Scale option behaves as expected.
\ No newline at end of file
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/ControlValueFormatter.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/ControlValueFormatter.java
new file mode 100644
index 0000000000..e6df0bc8c5
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/ControlValueFormatter.java
@@ -0,0 +1,17 @@
+package net.caffeinemc.mods.sodium.api.config.option;
+
+import net.minecraft.network.chat.Component;
+
+/**
+ * A formatter for control values, converting integer values into display components.
+ */
+@FunctionalInterface
+public interface ControlValueFormatter {
+ /**
+ * Formats the given integer value into a display component.
+ *
+ * @param value the integer value to format
+ * @return the formatted value
+ */
+ Component format(int value);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/FlagHook.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/FlagHook.java
new file mode 100644
index 0000000000..d7784db134
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/FlagHook.java
@@ -0,0 +1,19 @@
+package net.caffeinemc.mods.sodium.api.config.option;
+
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.Collection;
+import java.util.function.BiConsumer;
+
+/**
+ * A hook that is triggered when certain option flags are updated.
+ */
+public interface FlagHook extends BiConsumer, ConfigState> {
+ /**
+ * Gets the identifiers of the flags that trigger this hook.
+ *
+ * @return A collection of flag identifiers.
+ */
+ Collection getTriggers();
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/NameProvider.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/NameProvider.java
new file mode 100644
index 0000000000..be5149f1c4
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/NameProvider.java
@@ -0,0 +1,15 @@
+package net.caffeinemc.mods.sodium.api.config.option;
+
+import net.minecraft.network.chat.Component;
+
+/**
+ * Base interface extended by enums whose members can provide display names.
+ */
+public interface NameProvider {
+ /**
+ * Gets the display name of this item.
+ *
+ * @return the display name
+ */
+ Component getName();
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionBinding.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionBinding.java
new file mode 100644
index 0000000000..e1cf4038bc
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionBinding.java
@@ -0,0 +1,22 @@
+package net.caffeinemc.mods.sodium.api.config.option;
+
+/**
+ * Interface for binding an option to a storage mechanism.
+ *
+ * @param The type of the option value.
+ */
+public interface OptionBinding {
+ /**
+ * Saves the given value to the storage mechanism.
+ *
+ * @param value The value to save.
+ */
+ void save(V value);
+
+ /**
+ * Loads the value from the storage mechanism.
+ *
+ * @return The loaded value.
+ */
+ V load();
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionFlag.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionFlag.java
new file mode 100644
index 0000000000..f999c28a7f
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionFlag.java
@@ -0,0 +1,45 @@
+package net.caffeinemc.mods.sodium.api.config.option;
+
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.Locale;
+
+/**
+ * Flags that indicate specific actions required when an option is changed.
+ */
+public enum OptionFlag {
+ /**
+ * Indicates that the renderer needs to be reloaded. This means all meshes will be discarded and rebuilt. This flag should only be applied if necessary, since rebuilding all meshes is a disruptive operation.
+ */
+ REQUIRES_RENDERER_RELOAD,
+
+ /**
+ * Indicates that the renderer needs to be updated. This means that the culling state may have changed and occlusion culling will be recalculated. This causes no noticeable disruption to the user.
+ */
+ REQUIRES_RENDERER_UPDATE,
+
+ /**
+ * Indicates that assets need to be reloaded. This reloads resource packs, and then reloads the renderer, causing all meshes to be discarded and rebuilt.
+ */
+ REQUIRES_ASSET_RELOAD,
+
+ /**
+ * Indicates that the video mode needs to be updated. This causes a disruption as the video mode is changed.
+ */
+ REQUIRES_VIDEOMODE_RELOAD,
+
+ /**
+ * Indicates that the game needs to be restarted for the option change to take effect.
+ */
+ REQUIRES_GAME_RESTART;
+
+ private final ResourceLocation id = ResourceLocation.fromNamespaceAndPath("sodium", "builtin_option_flag." + this.name().toLowerCase(Locale.ROOT));
+
+ /**
+ * Gets the {@link ResourceLocation} for this option flag.
+ * @return The identifier.
+ */
+ public ResourceLocation getId() {
+ return this.id;
+ }
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionImpact.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionImpact.java
new file mode 100644
index 0000000000..0a7e591e5c
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionImpact.java
@@ -0,0 +1,41 @@
+package net.caffeinemc.mods.sodium.api.config.option;
+
+import net.minecraft.ChatFormatting;
+import net.minecraft.network.chat.Component;
+
+/**
+ * Represents the performance impact level of a configuration option.
+ */
+public enum OptionImpact implements NameProvider {
+ /**
+ * Low impact on performance. Changing this option won't affect performance in a measurable or noticeable way.
+ */
+ LOW(ChatFormatting.GREEN, "sodium.option_impact.low"),
+
+ /**
+ * Medium impact on performance. Changing this option may have a noticeable effect on performance in some scenarios and some systems.
+ */
+ MEDIUM(ChatFormatting.YELLOW, "sodium.option_impact.medium"),
+
+ /**
+ * High impact on performance. Changing this option will likely have a significant effect on performance in most scenarios.
+ */
+ HIGH(ChatFormatting.GOLD, "sodium.option_impact.high"),
+
+ /**
+ * Varies in impact on performance. The effect of changing this option on performance is highly dependent on the specific scenario and system.
+ */
+ VARIES(ChatFormatting.WHITE, "sodium.option_impact.varies");
+
+ private final Component text;
+
+ OptionImpact(ChatFormatting formatting, String text) {
+ this.text = Component.translatable(text)
+ .withStyle(formatting);
+ }
+
+ @Override
+ public Component getName() {
+ return this.text;
+ }
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/Range.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/Range.java
new file mode 100644
index 0000000000..e48662e7fc
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/Range.java
@@ -0,0 +1,19 @@
+package net.caffeinemc.mods.sodium.api.config.option;
+
+/**
+ * A record representing a range of integer values with a specified step. When validating a value, it uses the default value if the value is out of range or does not conform to the step.
+ *
+ * @param min The minimum value of the range (inclusive).
+ * @param max The maximum value of the range (inclusive).
+ * @param step The step increment between valid values in the range.
+ */
+public record Range(int min, int max, int step) implements SteppedValidator {
+ public Range {
+ if (min > max) {
+ throw new IllegalArgumentException("Min must be less than or equal to max");
+ }
+ if (step <= 0) {
+ throw new IllegalArgumentException("Step must be greater than 0");
+ }
+ }
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/SteppedValidator.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/SteppedValidator.java
new file mode 100644
index 0000000000..9bbc5db47d
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/SteppedValidator.java
@@ -0,0 +1,46 @@
+package net.caffeinemc.mods.sodium.api.config.option;
+
+import java.util.function.Supplier;
+
+/**
+ * Common interface for validators that define a stepped range of integer values.
+ */
+public interface SteppedValidator extends Validator {
+ /**
+ * Gets the minimum value of the range.
+ * @return The minimum value.
+ */
+ int min();
+
+ /**
+ * Gets the maximum value of the range.
+ * @return The maximum value.
+ */
+ int max();
+
+ /**
+ * Gets the step increment between valid values in the range.
+ *
+ * @return The step increment.
+ */
+ int step();
+
+ /**
+ * Checks if a given value is valid within this range.
+ *
+ * @param value The value to check.
+ * @return True if the value is valid, false otherwise.
+ */
+ default boolean isValueValid(int value) {
+ int min = this.min();
+ return value >= min && value <= this.max() && (value - min) % this.step() == 0;
+ }
+
+ @Override
+ default Integer getValidatedValue(Integer value, Supplier defaultValueSupplier) {
+ if (this.isValueValid(value)) {
+ return value;
+ }
+ return defaultValueSupplier.get();
+ }
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/Validator.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/Validator.java
new file mode 100644
index 0000000000..b96e033b03
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/Validator.java
@@ -0,0 +1,12 @@
+package net.caffeinemc.mods.sodium.api.config.option;
+
+import java.util.function.Supplier;
+
+/**
+ * Common interface for validators that validate values of type V.
+ *
+ * @param The type of value to be validated.
+ */
+public interface Validator {
+ V getValidatedValue(V value, Supplier defaultValueSupplier);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/BooleanOptionBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/BooleanOptionBuilder.java
new file mode 100644
index 0000000000..f59c5143fc
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/BooleanOptionBuilder.java
@@ -0,0 +1,63 @@
+package net.caffeinemc.mods.sodium.api.config.structure;
+
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.caffeinemc.mods.sodium.api.config.StorageEventHandler;
+import net.caffeinemc.mods.sodium.api.config.option.OptionBinding;
+import net.caffeinemc.mods.sodium.api.config.option.OptionFlag;
+import net.caffeinemc.mods.sodium.api.config.option.OptionImpact;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Builder interface for defining boolean options. Refines builder methods to return this class instead of the base interface and have a {@link Boolean} value type.
+ */
+public interface BooleanOptionBuilder extends StatefulOptionBuilder {
+ @Override
+ BooleanOptionBuilder setName(Component name);
+
+ @Override
+ BooleanOptionBuilder setEnabled(boolean available);
+
+ @Override
+ BooleanOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies);
+
+ @Override
+ BooleanOptionBuilder setStorageHandler(StorageEventHandler storage);
+
+ @Override
+ BooleanOptionBuilder setTooltip(Component tooltip);
+
+ @Override
+ BooleanOptionBuilder setTooltip(Function tooltip);
+
+ @Override
+ BooleanOptionBuilder setImpact(OptionImpact impact);
+
+ @Override
+ BooleanOptionBuilder setFlags(OptionFlag... flags);
+
+ @Override
+ BooleanOptionBuilder setFlags(ResourceLocation... flags);
+
+ @Override
+ BooleanOptionBuilder setDefaultValue(Boolean value);
+
+ @Override
+ BooleanOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies);
+
+ @Override
+ BooleanOptionBuilder setControlHiddenWhenDisabled(boolean hidden);
+
+ @Override
+ BooleanOptionBuilder setBinding(Consumer save, Supplier load);
+
+ @Override
+ BooleanOptionBuilder setBinding(OptionBinding binding);
+
+ @Override
+ BooleanOptionBuilder setApplyHook(Consumer hook);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ColorThemeBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ColorThemeBuilder.java
new file mode 100644
index 0000000000..546421110b
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ColorThemeBuilder.java
@@ -0,0 +1,26 @@
+package net.caffeinemc.mods.sodium.api.config.structure;
+
+/**
+ * Builder interface for defining color themes.
+ *
+ * Colors use RGB integers (bits 0 to 24). This is ARGB format with alpha bits ignored.
+ */
+public interface ColorThemeBuilder {
+ /**
+ * Sets the base theme color.
+ *
+ * @param theme Theme color as an RGB integer.
+ * @return The current builder instance.
+ */
+ ColorThemeBuilder setBaseThemeRGB(int theme);
+
+ /**
+ * Sets the full theme colors.
+ *
+ * @param theme Theme color as an RGB integer.
+ * @param themeHighlight Theme highlight color as an RGB integer.
+ * @param themeDisabled Theme disabled color as an RGB integer.
+ * @return The current builder instance.
+ */
+ ColorThemeBuilder setFullThemeRGB(int theme, int themeHighlight, int themeDisabled);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ConfigBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ConfigBuilder.java
new file mode 100644
index 0000000000..da258aba8d
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ConfigBuilder.java
@@ -0,0 +1,97 @@
+package net.caffeinemc.mods.sodium.api.config.structure;
+
+import net.minecraft.resources.ResourceLocation;
+
+/**
+ * Root builder interface for defining configuration structures. An implementation of this class is passed to your entry point and lets you create builders for everything in the API. Builders do not need to be built manually, as they are automatically closed when the enclosing scope ends.
+ *
+ * Refer to USAGE.md for usage instructions.
+ */
+public interface ConfigBuilder {
+ /**
+ * Registers a new mod options structure with the given configId, name, and version. Note that a mod may register multiple mod options structures under different configIds if desired.
+ *
+ * @param configId The configId of these mod options.
+ * @param name The human-readable name of the mod.
+ * @param version The version of the mod.
+ * @return A builder for defining the mod's configuration structure.
+ */
+ ModOptionsBuilder registerModOptions(String configId, String name, String version);
+
+ /**
+ * Registers a new mod options structure for the mod with the given configId. The mod's name and version will be looked up automatically.
+ *
+ * @param configId The configId of these mod options.
+ * @return A builder for defining the mod's configuration structure.
+ */
+ ModOptionsBuilder registerModOptions(String configId);
+
+ /**
+ * Registers a new mod options structure for the mod of the current entrypoint. The mod's name and version will be looked up automatically.
+ *
+ * @return A builder for defining the mod's configuration structure.
+ */
+ ModOptionsBuilder registerOwnModOptions();
+
+ /**
+ * Creates a new color theme builder.
+ *
+ * @return A builder for defining a color theme.
+ */
+ ColorThemeBuilder createColorTheme();
+
+ /**
+ * Creates a new option page builder.
+ *
+ * @return A builder for defining an option page.
+ */
+ OptionPageBuilder createOptionPage();
+
+ /**
+ * Creates a new external page builder.
+ *
+ * @return A builder for defining an external page.
+ */
+ ExternalPageBuilder createExternalPage();
+
+ /**
+ * Creates a new option group builder.
+ *
+ * @return A builder for defining an option group.
+ */
+ OptionGroupBuilder createOptionGroup();
+
+ /**
+ * Creates a new boolean option builder.
+ *
+ * @param id The unique identifier for this option.
+ * @return A builder for defining a boolean option.
+ */
+ BooleanOptionBuilder createBooleanOption(ResourceLocation id);
+
+ /**
+ * Creates a new integer option builder.
+ *
+ * @param id The unique identifier for this option.
+ * @return A builder for defining an integer option.
+ */
+ IntegerOptionBuilder createIntegerOption(ResourceLocation id);
+
+ /**
+ * Creates a new enum option builder.
+ *
+ * @param id The unique identifier for this option.
+ * @param enumClass The enum class for this option.
+ * @param The enum type.
+ * @return A builder for defining an enum option.
+ */
+ > EnumOptionBuilder createEnumOption(ResourceLocation id, Class enumClass);
+
+ /**
+ * Creates a new external button option builder.
+ *
+ * @param id The unique identifier for this option.
+ * @return A builder for defining an external button option.
+ */
+ ExternalButtonOptionBuilder createExternalButtonOption(ResourceLocation id);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/EnumOptionBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/EnumOptionBuilder.java
new file mode 100644
index 0000000000..06ccaf7f56
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/EnumOptionBuilder.java
@@ -0,0 +1,102 @@
+package net.caffeinemc.mods.sodium.api.config.structure;
+
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.caffeinemc.mods.sodium.api.config.StorageEventHandler;
+import net.caffeinemc.mods.sodium.api.config.option.OptionBinding;
+import net.caffeinemc.mods.sodium.api.config.option.OptionFlag;
+import net.caffeinemc.mods.sodium.api.config.option.OptionImpact;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Builder interface for defining enum options. Refines builder methods to return this class instead of the base interface and have an {@link Enum} value type.
+ *
+ * @param The enum type for this option.
+ */
+public interface EnumOptionBuilder> extends StatefulOptionBuilder {
+ /**
+ * Creates a name provider function that maps enum constants to the provided names based on their ordinal values.
+ *
+ * @param names The array of names corresponding to the enum constants.
+ * @param The enum type.
+ * @return A function that provides names for enum constants.
+ */
+ static > Function nameProviderFrom(Component... names) {
+ return e -> names[e.ordinal()];
+ }
+
+ @Override
+ EnumOptionBuilder setName(Component name);
+
+ @Override
+ EnumOptionBuilder setEnabled(boolean available);
+
+ @Override
+ EnumOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies);
+
+ @Override
+ EnumOptionBuilder setStorageHandler(StorageEventHandler storage);
+
+ @Override
+ EnumOptionBuilder setTooltip(Component tooltip);
+
+ @Override
+ EnumOptionBuilder setTooltip(Function tooltip);
+
+ @Override
+ EnumOptionBuilder setImpact(OptionImpact impact);
+
+ @Override
+ EnumOptionBuilder setFlags(OptionFlag... flags);
+
+ @Override
+ EnumOptionBuilder setFlags(ResourceLocation... flags);
+
+ @Override
+ EnumOptionBuilder setDefaultValue(E value);
+
+ @Override
+ EnumOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies);
+
+ @Override
+ EnumOptionBuilder setControlHiddenWhenDisabled(boolean hidden);
+
+ @Override
+ EnumOptionBuilder setBinding(Consumer save, Supplier load);
+
+ @Override
+ EnumOptionBuilder setBinding(OptionBinding binding);
+
+ @Override
+ EnumOptionBuilder setApplyHook(Consumer hook);
+
+ /**
+ * Sets the allowed values for this enum option.
+ *
+ * @param allowedValues The set of allowed enum values.
+ * @return This builder instance.
+ */
+ EnumOptionBuilder setAllowedValues(Set allowedValues);
+
+ /**
+ * Sets a provider function to determine the allowed values for this enum option based on the current configuration state.
+ *
+ * @param provider The function that provides the set of allowed enum values.
+ * @param dependencies The options that this provider depends on.
+ * @return This builder instance.
+ */
+ EnumOptionBuilder setAllowedValuesProvider(Function> provider, ResourceLocation... dependencies);
+
+ /**
+ * Sets a provider function to determine the display name for each enum constant.
+ *
+ * @param provider The function that provides the display name for each enum constant.
+ * @return This builder instance.
+ */
+ EnumOptionBuilder setElementNameProvider(Function provider);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ExternalButtonOptionBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ExternalButtonOptionBuilder.java
new file mode 100644
index 0000000000..257c7d56fc
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ExternalButtonOptionBuilder.java
@@ -0,0 +1,33 @@
+package net.caffeinemc.mods.sodium.api.config.structure;
+
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Builder interface for defining external button options.
+ */
+public interface ExternalButtonOptionBuilder extends OptionBuilder {
+ @Override
+ ExternalButtonOptionBuilder setName(Component name);
+
+ @Override
+ ExternalButtonOptionBuilder setTooltip(Component tooltip);
+
+ @Override
+ ExternalButtonOptionBuilder setEnabled(boolean available);
+
+ @Override
+ ExternalButtonOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies);
+
+ /** Sets the screen consumer for the external button option.
+ *
+ * @param currentScreenConsumer A consumer that accepts the current screen and opens the external configuration screen.
+ * @return The current builder instance.
+ */
+ ExternalButtonOptionBuilder setScreenConsumer(Consumer currentScreenConsumer);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ExternalPageBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ExternalPageBuilder.java
new file mode 100644
index 0000000000..198ebff7e8
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ExternalPageBuilder.java
@@ -0,0 +1,27 @@
+package net.caffeinemc.mods.sodium.api.config.structure;
+
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.Component;
+
+import java.util.function.Consumer;
+
+/**
+ * Builder interface for defining external configuration pages.
+ */
+public interface ExternalPageBuilder extends PageBuilder {
+ /**
+ * Sets the name of the external configuration page.
+ *
+ * @param name The name component.
+ * @return The current builder instance.
+ */
+ ExternalPageBuilder setName(Component name);
+
+ /**
+ * Sets the screen provider for the external configuration page.
+ *
+ * @param currentScreenConsumer A consumer that accepts the current screen and opens the external configuration screen.
+ * @return The current builder instance.
+ */
+ ExternalPageBuilder setScreenConsumer(Consumer currentScreenConsumer);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/IntegerOptionBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/IntegerOptionBuilder.java
new file mode 100644
index 0000000000..85965a8508
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/IntegerOptionBuilder.java
@@ -0,0 +1,113 @@
+package net.caffeinemc.mods.sodium.api.config.structure;
+
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.caffeinemc.mods.sodium.api.config.StorageEventHandler;
+import net.caffeinemc.mods.sodium.api.config.option.*;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Builder interface for defining integer options. Refines builder methods to return this class instead of the base interface and have an {@link Integer} value type.
+ */
+public interface IntegerOptionBuilder extends StatefulOptionBuilder {
+ @Override
+ IntegerOptionBuilder setName(Component name);
+
+ @Override
+ IntegerOptionBuilder setEnabled(boolean available);
+
+ @Override
+ IntegerOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies);
+
+ @Override
+ IntegerOptionBuilder setStorageHandler(StorageEventHandler storage);
+
+ @Override
+ IntegerOptionBuilder setTooltip(Component tooltip);
+
+ @Override
+ IntegerOptionBuilder setTooltip(Function tooltip);
+
+ @Override
+ IntegerOptionBuilder setImpact(OptionImpact impact);
+
+ @Override
+ IntegerOptionBuilder setFlags(OptionFlag... flags);
+
+ @Override
+ IntegerOptionBuilder setFlags(ResourceLocation... flags);
+
+ @Override
+ IntegerOptionBuilder setDefaultValue(Integer value);
+
+ @Override
+ IntegerOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies);
+
+ @Override
+ IntegerOptionBuilder setControlHiddenWhenDisabled(boolean hidden);
+
+ @Override
+ IntegerOptionBuilder setBinding(Consumer save, Supplier load);
+
+ @Override
+ IntegerOptionBuilder setBinding(OptionBinding binding);
+
+ @Override
+ IntegerOptionBuilder setApplyHook(Consumer hook);
+
+ /**
+ * Sets the range for this integer option.
+ *
+ * @param min The minimum value (inclusive).
+ * @param max The maximum value (inclusive).
+ * @param step The step value for increments.
+ * @return The current builder instance.
+ */
+ IntegerOptionBuilder setRange(int min, int max, int step);
+
+ /**
+ * Sets the range for this integer option.
+ *
+ * @param range The range object defining min, max, and step.
+ * @return The current builder instance.
+ */
+ IntegerOptionBuilder setRange(Range range);
+
+ /**
+ * Sets a provider function to determine the range for this integer option based on the current configuration state.
+ *
+ * @param provider The function that provides the range.
+ * @param dependencies The options that this provider depends on.
+ * @return The current builder instance.
+ */
+ IntegerOptionBuilder setRangeProvider(Function provider, ResourceLocation... dependencies);
+
+ /**
+ * Sets a validator for this integer option. A {@link Range} is a type of stepped validator.
+ *
+ * @param validator The validator to set.
+ * @return The current builder instance.
+ */
+ IntegerOptionBuilder setValidator(SteppedValidator validator);
+
+ /**
+ * Sets a provider function to determine the validator for this integer option based on the current configuration state.
+ *
+ * @param provider The function that provides the validator.
+ * @param dependencies The options that this provider depends on.
+ * @return The current builder instance.
+ */
+ IntegerOptionBuilder setValidatorProvider(Function provider, ResourceLocation... dependencies);
+
+ /**
+ * Sets the value formatter for this integer option.
+ *
+ * @param formatter The formatter to format the integer value of this option.
+ * @return The current builder instance.
+ */
+ IntegerOptionBuilder setValueFormatter(ControlValueFormatter formatter);
+}
\ No newline at end of file
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ModOptionsBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ModOptionsBuilder.java
new file mode 100644
index 0000000000..d4e8d6e76c
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ModOptionsBuilder.java
@@ -0,0 +1,123 @@
+package net.caffeinemc.mods.sodium.api.config.structure;
+
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.caffeinemc.mods.sodium.api.config.option.FlagHook;
+import net.caffeinemc.mods.sodium.api.config.option.OptionFlag;
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.Collection;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+/**
+ * Builder interface for defining options belonging to a mod and its metadata. A set of mod options contains some list of option pages, option overrides, and option overlays. At least one page, override, or overlay must be defined.
+ */
+public interface ModOptionsBuilder {
+ /**
+ * Sets the display name of the mod.
+ *
+ * @param name The mod name.
+ * @return The current builder instance.
+ */
+ ModOptionsBuilder setName(String name);
+
+ /**
+ * Sets the version string of the mod. This value is typically automatically populated from the mod metadata known to the mod loader.
+ *
+ * @param version The version string.
+ * @return The current builder instance.
+ */
+ ModOptionsBuilder setVersion(String version);
+
+ /**
+ * Sets a formatter function for the mod version string. This converts from the raw version string to a display version string.
+ *
+ * @param versionFormatter The version formatter function.
+ * @return The current builder instance.
+ */
+ ModOptionsBuilder formatVersion(Function versionFormatter);
+
+ /**
+ * Sets the color theme for the mod options UI. A color theme is optional and a theme will be chosen at random (deterministically) from a predetermined set of reasonable colors if none is provided.
+ *
+ * Basic validation is applied to the color theme to ensure it's not grayscale and not too dark.
+ *
+ * @param colorTheme The color theme builder.
+ * @return The current builder instance.
+ */
+ ModOptionsBuilder setColorTheme(ColorThemeBuilder colorTheme);
+
+ /**
+ * Sets the icon texture for the mod. See the documentation for more information about appropriate icon design. The summary is as follows. An icon should:
+ *
+ *
+ *
Be a square texture (e.g. 64x64, 128x128, etc).
+ *
Have a transparent background unless the main content of the icon fills the entire square.
+ *
Have binary alpha (fully opaque or fully transparent).
+ *
Have limited detail since it will be rendered at a small size.
+ *
+ *
+ * Icons set with this method are tinted monochrome in the mod's theme color. This means the texture itself should be fully white to result in the theme when tinted.
+ *
+ * No icon will be shown if none is provided and the layout adjusted accordingly.
+ *
+ * @param texture The ID of the icon texture.
+ * @return The current builder instance.
+ */
+ ModOptionsBuilder setIcon(ResourceLocation texture);
+
+ /**
+ * Sets the icon texture for the mod. Same as {@link #setIcon(ResourceLocation)}, but the texture will be rendered in its original color instead of being tinted.
+ *
+ * @param texture The ID of the icon texture.
+ * @return The current builder instance.
+ */
+ ModOptionsBuilder setNonTintedIcon(ResourceLocation texture);
+
+ /**
+ * Adds a configuration page to the mod options.
+ *
+ * @param page The page builder.
+ * @return The current builder instance.
+ */
+ ModOptionsBuilder addPage(PageBuilder page);
+
+ /**
+ * Registers an option override provided by this mod. Overrides allow modifying the behavior or appearance of options defined by other mods.
+ *
+ * The ID of the provided replacement option can match the original option to allow other mods to apply overlays targeting the original option ID. If the replacement option has a different ID, overlays must target the new ID.
+ *
+ * @param target The ID of the option to override.
+ * @param replacement The option builder that defines the replacement option.
+ * @return The current builder instance.
+ */
+ ModOptionsBuilder registerOptionReplacement(ResourceLocation target, OptionBuilder replacement);
+
+ /**
+ * Registers an option overlay provided by this mod. Overlays allow partially changing an option instead of replacing it entirely.
+ *
+ * The target option ID must match the ID of an existing option, either defined by another mod or by a replacement option defined by this mod. If the target option has been replaced, overlays must target the ID of the replacement option, which may or may not be the same as the original option ID.
+ *
+ * @param target The ID of the option to overlay.
+ * @param overlay The option builder that defines the overlay changes.
+ * @return The current builder instance.
+ */
+ ModOptionsBuilder registerOptionOverlay(ResourceLocation target, OptionBuilder overlay);
+
+ /**
+ * Registers a hook that will be called after an option which has any of the specified flags changed. This can be used to implement custom behavior in response to option changes. To hook on built-in flags, use the identifiers given by {@link OptionFlag#getId()}. The hook is given an array of all flags that triggered the hook. Note that the hook may be called with a set of flags larger than the set of flags it is interested in for performance reasons, since this lets us avoid generating a different flag set for every hook.
+ *
+ * @param hook The hook to run, with the set of all triggered flags.
+ * @param triggers The flags to listen for.
+ * @return The current builder instance.
+ */
+ ModOptionsBuilder registerFlagHook(BiConsumer, ConfigState> hook, ResourceLocation... triggers);
+
+ /**
+ * Registers a hook just like {@link #registerFlagHook(BiConsumer, ResourceLocation...)}, but using a {@link FlagHook}.
+ *
+ * @param hook The flag hook to register.
+ * @return The current builder instance.
+ */
+ ModOptionsBuilder registerFlagHook(FlagHook hook);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionBuilder.java
new file mode 100644
index 0000000000..e07e734d7b
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionBuilder.java
@@ -0,0 +1,45 @@
+package net.caffeinemc.mods.sodium.api.config.structure;
+
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.function.Function;
+
+/**
+ * The base interface for option builders.
+ */
+public interface OptionBuilder {
+ /**
+ * Sets the name of the option.
+ *
+ * @param name The display name of the option.
+ * @return The current builder instance.
+ */
+ OptionBuilder setName(Component name);
+
+ /**
+ * Sets the tooltip of the option.
+ *
+ * @param tooltip The tooltip component.
+ * @return The current builder instance.
+ */
+ OptionBuilder setTooltip(Component tooltip);
+
+ /**
+ * Sets whether the option is enabled.
+ *
+ * @param available True if the option is enabled, false otherwise.
+ * @return The current builder instance.
+ */
+ OptionBuilder setEnabled(boolean available);
+
+ /**
+ * Sets a provider function to determine whether the option is enabled based on the current configuration state.
+ *
+ * @param provider The function that provides the enabled state.
+ * @param dependencies The options that this provider depends on.
+ * @return The current builder instance.
+ */
+ OptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionGroupBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionGroupBuilder.java
new file mode 100644
index 0000000000..71feff80ae
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionGroupBuilder.java
@@ -0,0 +1,26 @@
+package net.caffeinemc.mods.sodium.api.config.structure;
+
+import net.minecraft.network.chat.Component;
+
+/**
+ * Builder interface for defining option groups, which are collections of related options.
+ *
+ * Option groups only exist to visually group options together in the configuration UI, and a title is not required and won't be displayed unless set.
+ */
+public interface OptionGroupBuilder {
+ /**
+ * Sets the name of this option group. This is optional and won't be displayed unless set. We recommend only using this if necessary, as usually option groups without names are enough grouping and option names provide sufficiently detailed labels of the option categories.
+ *
+ * @param name This option group's display name.
+ * @return The current builder instance.
+ */
+ OptionGroupBuilder setName(Component name);
+
+ /**
+ * Adds an option to this option group.
+ *
+ * @param option The option to add.
+ * @return The current builder instance.
+ */
+ OptionGroupBuilder addOption(OptionBuilder option);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionPageBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionPageBuilder.java
new file mode 100644
index 0000000000..48d27a8fdf
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/OptionPageBuilder.java
@@ -0,0 +1,32 @@
+package net.caffeinemc.mods.sodium.api.config.structure;
+
+import net.minecraft.network.chat.Component;
+
+/**
+ * Builder interface for defining option pages, which are lists of option groups.
+ */
+public interface OptionPageBuilder extends PageBuilder {
+ /**
+ * Sets the name of the option page.
+ *
+ * @param name The name component.
+ * @return The current builder instance.
+ */
+ OptionPageBuilder setName(Component name);
+
+ /**
+ * Adds an option group to the option page.
+ *
+ * @param group The option group builder.
+ * @return The current builder instance.
+ */
+ OptionPageBuilder addOptionGroup(OptionGroupBuilder group);
+
+ /**
+ * Adds an option directly to the option page. Options added directly to the page are grouped into an implicit unnamed option group at the bottom of the page.
+ *
+ * @param option The option to add.
+ * @return The current builder instance.
+ */
+ OptionPageBuilder addOption(OptionBuilder option);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/PageBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/PageBuilder.java
new file mode 100644
index 0000000000..3fa2370c2f
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/PageBuilder.java
@@ -0,0 +1,7 @@
+package net.caffeinemc.mods.sodium.api.config.structure;
+
+/**
+ * Base interface for all option page builders.
+ */
+public interface PageBuilder {
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/StatefulOptionBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/StatefulOptionBuilder.java
new file mode 100644
index 0000000000..60d5ef010d
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/StatefulOptionBuilder.java
@@ -0,0 +1,124 @@
+package net.caffeinemc.mods.sodium.api.config.structure;
+
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.caffeinemc.mods.sodium.api.config.StorageEventHandler;
+import net.caffeinemc.mods.sodium.api.config.option.OptionBinding;
+import net.caffeinemc.mods.sodium.api.config.option.OptionFlag;
+import net.caffeinemc.mods.sodium.api.config.option.OptionImpact;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Builder interface for defining stateful options.
+ *
+ * @param The type of the option's value.
+ */
+public interface StatefulOptionBuilder extends OptionBuilder {
+ @Override
+ StatefulOptionBuilder setName(Component name);
+
+ @Override
+ OptionBuilder setEnabled(boolean available);
+
+ @Override
+ OptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies);
+
+ /**
+ * Sets the storage handler for this option.
+ *
+ * @param storage The storage event handler.
+ * @return The current builder instance.
+ */
+ StatefulOptionBuilder setStorageHandler(StorageEventHandler storage);
+
+ @Override
+ StatefulOptionBuilder setTooltip(Component tooltip);
+
+ /**
+ * Sets a functional tooltip for this option that changes the text based on the selected value.
+ *
+ * @param tooltip The function that provides the tooltip based on the option's value.
+ * @return The current builder instance.
+ */
+ StatefulOptionBuilder setTooltip(Function tooltip);
+
+ /**
+ * Sets the performance impact level of this option.
+ *
+ * @param impact The option's performance impact.
+ * @return The current builder instance.
+ */
+ StatefulOptionBuilder setImpact(OptionImpact impact);
+
+ /**
+ * Sets flags for this option.
+ *
+ * @param flags The option flags.
+ * @return The current builder instance.
+ */
+ StatefulOptionBuilder setFlags(OptionFlag... flags);
+
+ /**
+ * Sets flags for this option using {@link ResourceLocation} instances.
+ *
+ * @param flags The flags as identifiers.
+ * @return The current builder instance.
+ */
+ StatefulOptionBuilder setFlags(ResourceLocation... flags);
+
+ /**
+ * Sets the default value for this option. The default value is used when the binding returns an invalid value, such as during the first load.
+ *
+ * @param value The default value.
+ * @return The current builder instance.
+ */
+ StatefulOptionBuilder setDefaultValue(V value);
+
+ /**
+ * Sets a provider function to determine the default value for this option based on the current configuration state.
+ *
+ * @param provider The function that provides the default value.
+ * @param dependencies The options that this provider depends on.
+ * @return The current builder instance.
+ */
+ StatefulOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies);
+
+ /**
+ * Sets whether the control for this option should be hidden when the option is disabled. This should only be set to false when the user should know what the state of the option is even when it is disabled, and they cannot interact with it.
+ *
+ * By default, controls are hidden when disabled.
+ *
+ * @param hidden True to hide the control when disabled, false to show it.
+ * @return The current builder instance.
+ */
+ StatefulOptionBuilder setControlHiddenWhenDisabled(boolean hidden);
+
+ /**
+ * Sets a binding for this option using save and load functions.
+ *
+ * @param save The function to save the option's value.
+ * @param load The function to load the option's value.
+ * @return The current builder instance.
+ */
+ StatefulOptionBuilder setBinding(Consumer save, Supplier load);
+
+ /**
+ * Sets a binding for this option using an {@link OptionBinding} instance.
+ *
+ * @param binding The option binding.
+ * @return The current builder instance.
+ */
+ StatefulOptionBuilder setBinding(OptionBinding binding);
+
+ /**
+ * Sets a hook that is triggered after the options' value has been saved if it changed.
+ *
+ * @param hook The hook to be executed after applying the option.
+ * @return The current builder instance.
+ */
+ StatefulOptionBuilder setApplyHook(Consumer hook);
+}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/math/MatrixHelper.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/math/MatrixHelper.java
index df8806b7f2..9d2bb6920d 100644
--- a/common/src/api/java/net/caffeinemc/mods/sodium/api/math/MatrixHelper.java
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/math/MatrixHelper.java
@@ -10,7 +10,7 @@
/**
* Implements optimized utilities for transforming vectors with a given matrix.
- *
+ *
* Note: Brackets must be used carefully in the transform functions to ensure that floating-point errors are
* the same as those produced by JOML, otherwise Z-fighting will occur.
*/
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/texture/SpriteUtil.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/texture/SpriteUtil.java
new file mode 100644
index 0000000000..4f33c26a7f
--- /dev/null
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/texture/SpriteUtil.java
@@ -0,0 +1,31 @@
+package net.caffeinemc.mods.sodium.api.texture;
+
+import net.caffeinemc.mods.sodium.api.internal.DependencyInjection;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Utility functions for querying sprite information and updating per-frame information about sprite visibility.
+ */
+@ApiStatus.Experimental
+public interface SpriteUtil {
+ SpriteUtil INSTANCE = DependencyInjection.load(SpriteUtil.class,
+ "net.caffeinemc.mods.sodium.client.render.texture.SpriteUtilImpl");
+
+ /**
+ * Marks the sprite as "active", meaning that it is visible during this frame and should have the animation
+ * state updated. Mods which perform their own rendering without the use of Minecraft's helpers will need to
+ * call this method once every frame, when their sprite is actively being used in rendering.
+ * @param sprite The sprite to mark as active
+ */
+ void markSpriteActive(@NotNull TextureAtlasSprite sprite);
+
+ /**
+ * Returns if the provided sprite has an animation.
+ *
+ * @param sprite The sprite to query an animation for
+ * @return {@code true} if the provided sprite has an animation, otherwise {@code false}
+ */
+ boolean hasAnimation(@NotNull TextureAtlasSprite sprite);
+}
\ No newline at end of file
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorABGR.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorABGR.java
index a7e86c52c9..b9e4437cd4 100644
--- a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorABGR.java
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorABGR.java
@@ -1,19 +1,26 @@
package net.caffeinemc.mods.sodium.api.util;
+import java.nio.ByteOrder;
+
/**
* Provides some utilities for packing and unpacking color components from packed integer colors in ABGR format, which
* is used by OpenGL for color vectors.
- *
+ *
* | 32 | 24 | 16 | 8 |
* | 0110 1100 | 0110 1100 | 0110 1100 | 0110 1100 |
* | Alpha | Blue | Green | Red |
*/
public class ColorABGR implements ColorU8 {
- private static final int RED_COMPONENT_OFFSET = 0;
+ private static final int RED_COMPONENT_OFFSET = 0;
private static final int GREEN_COMPONENT_OFFSET = 8;
- private static final int BLUE_COMPONENT_OFFSET = 16;
+ private static final int BLUE_COMPONENT_OFFSET = 16;
private static final int ALPHA_COMPONENT_OFFSET = 24;
+ private static final int RED_COMPONENT_MASK = COMPONENT_MASK << RED_COMPONENT_OFFSET;
+ private static final int GREEN_COMPONENT_MASK = COMPONENT_MASK << GREEN_COMPONENT_OFFSET;
+ private static final int BLUE_COMPONENT_MASK = COMPONENT_MASK << BLUE_COMPONENT_OFFSET;
+ private static final int ALPHA_COMPONENT_MASK = COMPONENT_MASK << ALPHA_COMPONENT_OFFSET;
+
/**
* Packs the specified color components into ABGR format. The alpha component is fully opaque.
* @param r The red component of the color
@@ -98,4 +105,51 @@ public static int unpackBlue(int color) {
public static int unpackAlpha(int color) {
return (color >> ALPHA_COMPONENT_OFFSET) & COMPONENT_MASK;
}
+
+ /**
+ * Multiplies the RGB components of the color with the provided factor. The alpha component is not modified.
+ *
+ * @param color The packed 32-bit ABGR color to be multiplied
+ * @param factor The darkening factor (in the range of 0..255) to multiply with
+ */
+ public static int mulRGB(int color, int factor) {
+ return (ColorMixer.mul(color, factor) & ~ALPHA_COMPONENT_MASK) | (color & ALPHA_COMPONENT_MASK);
+ }
+
+ /**
+ * See {@link #mulRGB(int, int)}. This function is identical, but it accepts a float in [0.0, 1.0] instead, which
+ * is then mapped to [0, 255].
+ *
+ * @param color The packed 32-bit ABGR color to be multiplied
+ * @param factor The darkening factor (in the range of 0.0..1.0) to multiply with
+ */
+ public static int mulRGB(int color, float factor) {
+ return mulRGB(color, ColorU8.normalizedFloatToByte(factor));
+ }
+
+ private static final boolean BIG_ENDIAN = ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN;
+
+ /**
+ * Shuffles the ordering of the ABGR color so that it can then be written out to memory in the platform's native
+ * byte ordering. This should be used when writing the packed color to memory (in native-order) as a 32-bit word.
+ */
+ public static int fromNativeByteOrder(int color) {
+ if (BIG_ENDIAN) {
+ return Integer.reverseBytes(color);
+ } else {
+ return color;
+ }
+ }
+
+ /**
+ * Shuffles the ordering of the ABGR color from the platform's native byte ordering. This should be used when reading
+ * the packed color from memory (in native-order) as a 32-bit word.
+ */
+ public static int toNativeByteOrder(int color) {
+ if (BIG_ENDIAN) {
+ return Integer.reverseBytes(color);
+ } else {
+ return color;
+ }
+ }
}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorARGB.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorARGB.java
index f93880555e..8786262a31 100644
--- a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorARGB.java
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorARGB.java
@@ -4,7 +4,7 @@
* Provides some utilities for packing and unpacking color components from packed integer colors in ARGB format. This
* packed format is used by most of Minecraft, but special care must be taken to pack it into ABGR format before passing
* it to OpenGL attributes.
- *
+ *
* | 32 | 24 | 16 | 8 |
* | 0110 1100 | 0110 1100 | 0110 1100 | 0110 1100 |
* | Alpha | Red | Green | Blue |
@@ -15,8 +15,14 @@ public class ColorARGB implements ColorU8 {
private static final int GREEN_COMPONENT_OFFSET = 8;
private static final int BLUE_COMPONENT_OFFSET = 0;
+ private static final int RED_COMPONENT_MASK = COMPONENT_MASK << RED_COMPONENT_OFFSET;
+ private static final int GREEN_COMPONENT_MASK = COMPONENT_MASK << GREEN_COMPONENT_OFFSET;
+ private static final int BLUE_COMPONENT_MASK = COMPONENT_MASK << BLUE_COMPONENT_OFFSET;
+ private static final int ALPHA_COMPONENT_MASK = COMPONENT_MASK << ALPHA_COMPONENT_OFFSET;
+
/**
* Packs the specified color components into big-endian format for consumption by OpenGL.
+ *
* @param r The red component of the color
* @param g The green component of the color
* @param b The blue component of the color
@@ -32,6 +38,7 @@ public static int pack(int r, int g, int b, int a) {
/**
* Packs the specified color components into big-endian format for consumption by OpenGL. The alpha
* channel is fully opaque.
+ *
* @param r The red component of the color
* @param g The green component of the color
* @param b The blue component of the color
@@ -73,29 +80,155 @@ public static int unpackBlue(int color) {
}
/**
- * Re-packs the ARGB color into an ABGR color with the specified alpha component.
+ * Swizzles from ARGB format into ABGR format, replacing the alpha component with {@param alpha}.
*/
- public static int toABGR(int color, float alpha) {
- return Integer.reverseBytes(color << 8 | ColorU8.normalizedFloatToByte(alpha));
+ public static int toABGR(int color, int alpha) {
+ // shl(ARGB, 8) -> RGB0
+ // or(RGB0, 000A) -> RGBA
+ return Integer.reverseBytes(color << 8 | alpha);
}
/**
- * Re-packs the ARGB color into a aBGR color with the specified alpha component.
+ * Swizzles from ARGB format into ABGR format, replacing the alpha component with {@param alpha}. The alpha
+ * component is mapped from [0.0, 1.0] to [0, 255].
*/
- public static int toABGR(int color, int alpha) {
- return Integer.reverseBytes(color << 8 | alpha);
+ public static int toABGR(int color, float alpha) {
+ return toABGR(color, ColorU8.normalizedFloatToByte(alpha));
}
+ /**
+ * Swizzles from ARGB format into ABGR format.
+ */
public static int toABGR(int color) {
+ // rotateLeft(ARGB, 8) -> RGBA
+ // reverseBytes(RGBA) -> ABGR
return Integer.reverseBytes(Integer.rotateLeft(color, 8));
}
+ /**
+ * Swizzles from ABGR format into ARGB format.
+ */
+ public static int fromABGR(int color) {
+ // reverseBytes(ABGR) -> RGBA
+ // rotateRight(RGBA, 8) -> ARGB
+ return Integer.rotateRight(Integer.reverseBytes(color), 8);
+ }
+
/**
* Packs the specified color components into ARGB format.
- * @param rgb The red/green/blue component of the color
+ *
+ * @param rgb The red/green/blue component of the color
* @param alpha The alpha component of the color
*/
public static int withAlpha(int rgb, int alpha) {
return (alpha << ALPHA_COMPONENT_OFFSET) | (rgb & ~(COMPONENT_MASK << ALPHA_COMPONENT_OFFSET));
}
+
+ /**
+ * Replaces the alpha component of the specified color with the alpha component of another color.
+ *
+ * @param color The packed 32-bit ARGB color to modify
+ * @param alphaColor The packed 32-bit ARGB color to take the alpha component from
+ * @return The modified packed 32-bit ARGB color
+ */
+ public static int transferAlpha(int color, int alphaColor) {
+ return withAlpha(color, unpackAlpha(alphaColor));
+ }
+
+ /**
+ * Multiplies the RGB components of the color with the provided factor. The alpha component is not modified.
+ *
+ * @param color The packed 32-bit ABGR color to be multiplied
+ * @param factor The darkening factor (in the range of 0..255) to multiply with
+ */
+ public static int mulRGB(int color, int factor) {
+ return (ColorMixer.mul(color, factor) & ~ALPHA_COMPONENT_MASK) | (color & ALPHA_COMPONENT_MASK);
+ }
+
+ /**
+ * See {@link #mulRGB(int, int)}. This function is identical, but it accepts a float in [0.0, 1.0] instead, which
+ * is then mapped to [0, 255].
+ *
+ * @param color The packed 32-bit ABGR color to be multiplied
+ * @param factor The darkening factor (in the range of 0.0..1.0) to multiply with
+ */
+ public static int mulRGB(int color, float factor) {
+ return mulRGB(color, ColorU8.normalizedFloatToByte(factor));
+ }
+
+ /**
+ * Converts the specified packed ARGB color into HSV color space.
+ *
+ * Used with permission from patbox.
+ *
+ * @param color The packed 32-bit ARGB color to convert
+ * @return An array containing the hue, saturation and value components in [0,1] in that order.
+ */
+ public static float[] toHSV(int color) {
+ float r = (float) unpackRed(color) / 255;
+ float g = (float) unpackGreen(color) / 255;
+ float b = (float) unpackBlue(color) / 255;
+
+ float cmax = Math.max(r, Math.max(g, b));
+ float cmin = Math.min(r, Math.min(g, b));
+ float diff = cmax - cmin;
+ float h = -1, s = -1;
+
+ if (cmax == cmin) {
+ h = 0;
+ } else if (cmax == r) {
+ h = (0.1666f * ((g - b) / diff) + 1) % 1;
+ } else if (cmax == g) {
+ h = (0.1666f * ((b - r) / diff) + 0.333f) % 1;
+ } else if (cmax == b) {
+ h = (0.1666f * ((r - g) / diff) + 0.666f) % 1;
+ }
+ if (cmax == 0) {
+ s = 0;
+ } else {
+ s = (diff / cmax);
+ }
+
+ return new float[] { h, s, cmax };
+ }
+
+ private static int pack(float r, float g, float b) {
+ return pack(
+ ColorU8.normalizedFloatToByte(r),
+ ColorU8.normalizedFloatToByte(g),
+ ColorU8.normalizedFloatToByte(b)
+ );
+ }
+
+ /**
+ * Converts the specified HSV color components into a packed ARGB color.
+ *
+ * Used with permission from patbox
+ *
+ * @param hue The hue component in [0,1]
+ * @param saturation The saturation component in [0,1]
+ * @param value The value component in [0,1]
+ * @return The packed 32-bit ARGB color
+ */
+ public static int fromHSV(float hue, float saturation, float value) {
+ int h = (int) (hue * 6) % 6;
+ float f = hue * 6 - h;
+ float p = value * (1 - saturation);
+ float q = value * (1 - f * saturation);
+ float t = value * (1 - (1 - f) * saturation);
+
+ return switch (h) {
+ case 0 -> pack(value, t, p);
+ case 1 -> pack(q, value, p);
+ case 2 -> pack(p, value, t);
+ case 3 -> pack(p, q, value);
+ case 4 -> pack(t, p, value);
+ case 5 -> pack(value, p, q);
+ default -> 0;
+ };
+ }
+
+ public static int fromHSV(float[] hsv) {
+ return fromHSV(hsv[0], hsv[1], hsv[2]);
+ }
}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorMixer.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorMixer.java
index a1934c8b44..220eae17ca 100644
--- a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorMixer.java
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/ColorMixer.java
@@ -1,77 +1,146 @@
package net.caffeinemc.mods.sodium.api.util;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * A collection of optimized color mixing functions which directly operate on packed color values. These functions are
+ * agnostic to the ordering of color channels, and the output value will always use the same channel ordering as
+ * the input values.
+ */
public class ColorMixer {
- private static final int CHANNEL_MASK = 0x00FF00FF;
+ /**
+ *
Linearly interpolate between the {@param start} and {@param end} points, represented as packed unsigned 8-bit
+ * values within a 32-bit integer. The result is computed as
(start * weight) + (end * (255 - weight))
+ * using fixed-point arithmetic and round-to-nearest behavior.
+ *
+ *
The results are undefined if {@param weight} is not within the interval [0, 255].
+ *
+ *
If {@param start} and {@param end} are the same value, the result of this function will always be that value,
+ * regardless of {@param weight}.
+ *
+ * @param start The value at the start of the range to interpolate
+ * @param end The value at the end of the range to interpolate
+ * @param weight The weight value used to interpolate between color values (in 0..255 range)
+ * @return The color that was interpolated between the start and end points
+ */
+ public static int mix(int start, int end, int weight) {
+ // De-interleave the 8-bit component lanes into high and low halves for each point.
+ // Multiply the start point by alpha, and the end point by 1-alpha, to produce Q8.8 fixed-point intermediates.
+ // Add the Q8.8 fixed-point intermediaries together to obtain the mixed values.
+ final long hi = ((start & 0x00FF00FFL) * weight) + ((end & 0x00FF00FFL) * (ColorU8.COMPONENT_MASK - weight));
+ final long lo = ((start & 0xFF00FF00L) * weight) + ((end & 0xFF00FF00L) * (ColorU8.COMPONENT_MASK - weight));
+
+ // Round the fixed-point values to the nearest integer, and interleave the high and low halves to
+ // produce the final packed result.
+ final long result =
+ (((hi + 0x00FF00FFL) >>> 8) & 0x00FF00FFL) |
+ (((lo + 0xFF00FF00L) >>> 8) & 0xFF00FF00L);
+
+ return (int) result;
+ }
/**
- * Mixes a 32-bit color (with packed 8-bit components) into another using the given ratio. This is equivalent to
- * (color1 * ratio) + (color2 * (1.0 - ratio)) but uses bitwise trickery for maximum performance.
- *
- * The order of the channels within the packed color does not matter, and the output color will always
- * have the same ordering as the input colors.
+ *
This function is identical to {@link ColorMixer#mix(int, int, int)}, but {@param weight} is a normalized
+ * floating-point value within the interval of [0.0, 1.0].
+ *
+ *
The results are undefined if {@param weight} is not within the interval [0.0, 1.0].
*
- * @param aColor The color to mix towards
- * @param bColor The color to mix away from
- * @param ratio The percentage (in 0.0..1.0 range) to mix the first color into the second color
- * @return The mixed color in packed 32-bit format
+ * @param start The start of the range to interpolate
+ * @param end The end of the range to interpolate
+ * @param weight The weight value used to interpolate between color values (in 0.0..1.0 range)
+ * @return The color that was interpolated between the start and end points
*/
- public static int mix(int aColor, int bColor, float ratio) {
- int aRatio = (int) (256 * ratio); // int(ratio)
- int bRatio = 256 - aRatio; // int(1.0 - ratio)
-
- // Mask off and shift two components from each color into a packed vector of 16-bit components, where the
- // high 8 bits are all zeroes.
- int a1 = (aColor >> 0) & CHANNEL_MASK;
- int b1 = (bColor >> 0) & CHANNEL_MASK;
- int a2 = (aColor >> 8) & CHANNEL_MASK;
- int b2 = (bColor >> 8) & CHANNEL_MASK;
-
- // Multiply the packed 16-bit components against each mix factor, and add the components of each color
- // to produce the mixed result. This will never overflow since both 16-bit integers are in 0..255 range.
-
- // Then, shift the high 8 bits of each packed 16-bit component into the low 8 bits, and mask off the high bits of
- // each 16-bit component to produce a vector of packed 8-bit components, where every other component is empty.
- int c1 = (((a1 * aRatio) + (b1 * bRatio)) >> 8) & CHANNEL_MASK;
- int c2 = (((a2 * aRatio) + (b2 * bRatio)) >> 8) & CHANNEL_MASK;
-
- // Join the color components into the original order
- return ((c1 << 0) | (c2 << 8));
+ public static int mix(int start, int end, float weight) {
+ return mix(start, end, ColorU8.normalizedFloatToByte(weight));
}
/**
- * Multiplies the 32-bit colors (with packed 8-bit components) together.
- *
- * The order of the channels within the packed color does not matter, and the output color will always
- * have the same ordering as the input colors.
+ *
Performs bi-linear interpolation on a 2x2 matrix of color values to derive the point (x, y). This is more
+ * efficient than chaining {@link #mul(int, float)} calls.
*
- * @param a The first color to multiply
- * @param b The second color to multiply
- * @return The multiplied color in packed 32-bit format
+ *
The results are undefined if {@param x} and {@param y} are not within the interval [0.0, 1.0].
+ *
+ * @param m00 The packed color value for (0, 0)
+ * @param m01 The packed color value for (0, 1)
+ * @param m10 The packed color value for (1, 0)
+ * @param m11 The packed color value for (1, 1)
+ * @param x The amount to interpolate between x=0 and x=1
+ * @param y The amount to interpolate between y=0 and y=1
+ * @return The interpolated color value
*/
- public static int mul(int a, int b) {
- // Take each 8-bit component pair, multiply them together to create intermediate 16-bit integers,
- // and then shift the high half of each 16-bit integer into 8-bit integers.
- int c0 = (((a >> 0) & 0xFF) * ((b >> 0) & 0xFF)) >> 8;
- int c1 = (((a >> 8) & 0xFF) * ((b >> 8) & 0xFF)) >> 8;
- int c2 = (((a >> 16) & 0xFF) * ((b >> 16) & 0xFF)) >> 8;
- int c3 = (((a >> 24) & 0xFF) * ((b >> 24) & 0xFF)) >> 8;
-
- // Pack the components
- return (c0 << 0) | (c1 << 8) | (c2 << 16) | (c3 << 24);
+ @ApiStatus.Experimental
+ public static int mix2d(int m00, int m01, int m10, int m11, float x, float y) {
+ // The weights for each row and column in the matrix
+ int x1 = ColorU8.normalizedFloatToByte(x), x0 = 255 - x1;
+ int y1 = ColorU8.normalizedFloatToByte(y), y0 = 255 - y1;
+
+ // Blend across the X-axis
+ // (M00 * X0) + (M10 * X1)
+ long row0a = ((((m00 & 0x00FF00FFL) * x0) + (((m10 & 0x00FF00FFL) * x1)) + 0x00FF00FFL) >>> 8) & 0x00FF00FFL;
+ long row0b = ((((m00 & 0xFF00FF00L) * x0) + (((m10 & 0xFF00FF00L) * x1)) + 0xFF00FF00L) >>> 8) & 0xFF00FF00L;
+
+ // (M10 * X0) + (M11 * X1)
+ long row1a = ((((m01 & 0x00FF00FFL) * x0) + (((m11 & 0x00FF00FFL) * x1)) + 0x00FF00FFL) >>> 8) & 0x00FF00FFL;
+ long row1b = ((((m01 & 0xFF00FF00L) * x0) + (((m11 & 0xFF00FF00L) * x1)) + 0xFF00FF00L) >>> 8) & 0xFF00FF00L;
+
+ // Blend across the Y-axis
+ // (ROW0 * Y0) + (ROW1 * Y1)
+ long result = ((((row0a * y0) + ((row1a * y1)) + 0x00FF00FFL) >>> 8) & 0x00FF00FFL) |
+ ((((row0b * y0) + ((row1b * y1)) + 0xFF00FF00L) >>> 8) & 0xFF00FF00L);
+
+ return (int) result;
}
/**
- * Multiplies the 32-bit colors with one component
+ *
Multiplies the packed 8-bit values component-wise to produce 16-bit intermediaries, and then round to the
+ * nearest 8-bit representation (similar to floating-point.)
+ *
+ * @param color0 The first color to multiply
+ * @param color1 The second color to multiply
+ * @return The product of the two colors
+ */
+ public static int mulComponentWise(int color0, int color1) {
+ int comp0 = ((((color0 >>> 0) & 0xFF) * ((color1 >>> 0) & 0xFF)) + 0xFF) >>> 8;
+ int comp1 = ((((color0 >>> 8) & 0xFF) * ((color1 >>> 8) & 0xFF)) + 0xFF) >>> 8;
+ int comp2 = ((((color0 >>> 16) & 0xFF) * ((color1 >>> 16) & 0xFF)) + 0xFF) >>> 8;
+ int comp3 = ((((color0 >>> 24) & 0xFF) * ((color1 >>> 24) & 0xFF)) + 0xFF) >>> 8;
+
+ return (comp0 << 0) | (comp1 << 8) | (comp2 << 16) | (comp3 << 24);
+ }
+
+ /**
+ *
Multiplies each 8-bit component against the factor to produce 16-bit intermediaries, and then round to the
+ * nearest 8-bit representation (similar to floating-point.)
+ *
+ *
The results are undefined if {@param factor} is not within the interval [0, 255].
+ *
+ * @param color The packed color values
+ * @param factor The multiplication factor (in 0..255 range)
+ * @return The result of the multiplication
+ */
+ public static int mul(int color, int factor) {
+ // De-interleave the 8-bit component lanes into high and low halves.
+ // Perform 8-bit multiplication to produce Q8.8 fixed-point intermediaries.
+ final long hi = (color & 0x00FF00FFL) * factor;
+ final long lo = (color & 0xFF00FF00L) * factor;
+
+ // Round the Q8.8 fixed-point values to the nearest integer, and interleave the high and low halves to
+ // produce the packed result.
+ final long result =
+ (((hi + 0x00FF00FFL) >>> 8) & 0x00FF00FFL) |
+ (((lo + 0xFF00FF00L) >>> 8) & 0xFF00FF00L);
+
+ return (int) result;
+
+ }
+
+ /**
+ * See {@link #mul(int, int)}, which this function is identical to, except that it takes a floating point value in
+ * the interval of [0.0, 1.0] and maps it to [0, 255].
+ *
+ *
The results are undefined if {@param factor} is not within the interval [0.0, 1.0].
*/
- public static int mulSingle(int a, int b) {
- // Take each 8-bit component pair, multiply them together to create intermediate 16-bit integers,
- // and then shift the high half of each 16-bit integer into 8-bit integers.
- int c0 = (((a) & 0xFF) * b) >> 8;
- int c1 = (((a >> 8) & 0xFF) * b) >> 8;
- int c2 = (((a >> 16) & 0xFF) * b) >> 8;
- int c3 = (((a >> 24) & 0xFF) * b) >> 8;
-
- // Pack the components
- return (c0 << 0) | (c1 << 8) | (c2 << 16) | (c3 << 24);
+ public static int mul(int color, float factor) {
+ return mul(color, ColorU8.normalizedFloatToByte(factor));
}
}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/NormI8.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/NormI8.java
index 142e560113..5ab9042091 100644
--- a/common/src/api/java/net/caffeinemc/mods/sodium/api/util/NormI8.java
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/util/NormI8.java
@@ -7,7 +7,7 @@
/**
* Provides some utilities for working with packed normal vectors. Each normal component provides 8 bits of
* precision in the range of [-1.0,1.0].
- *
+ *
* | 32 | 24 | 16 | 8 |
* | 0000 0000 | 0110 1100 | 0110 1100 | 0110 1100 |
* | Padding | X | Y | Z |
@@ -94,7 +94,7 @@ public static int flipPacked(int norm) {
/**
* Returns true if the two packed normals are opposite directions.
- *
+ *
* TODO: this could possibly be faster by using normA == (~normB + 0x010101) but
* that has to special case when a component is zero since that wouldn't
* overflow correctly back to zero. (~0+1 == 0 but not if it's somewhere inside
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/ColorAttribute.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/ColorAttribute.java
index 0f484805a4..16373c6373 100644
--- a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/ColorAttribute.java
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/ColorAttribute.java
@@ -4,7 +4,7 @@
public class ColorAttribute {
public static void set(long ptr, int color) {
- MemoryUtil.memPutInt(ptr + 0, color);
+ MemoryUtil.memPutInt(ptr, color);
}
public static int get(long ptr) {
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/NormalAttribute.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/NormalAttribute.java
index 4f58ce9436..dc15bb9359 100644
--- a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/NormalAttribute.java
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/NormalAttribute.java
@@ -4,7 +4,7 @@
public class NormalAttribute {
public static void set(long ptr, int normal) {
- MemoryUtil.memPutInt(ptr + 0, normal);
+ MemoryUtil.memPutInt(ptr, normal);
}
public static int get(long ptr) {
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/PositionAttribute.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/PositionAttribute.java
index 9a597597b8..0aafec17ad 100644
--- a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/PositionAttribute.java
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/attributes/common/PositionAttribute.java
@@ -4,20 +4,20 @@
public class PositionAttribute {
public static void put(long ptr, float x, float y, float z) {
- MemoryUtil.memPutFloat(ptr + 0, x);
- MemoryUtil.memPutFloat(ptr + 4, y);
- MemoryUtil.memPutFloat(ptr + 8, z);
+ MemoryUtil.memPutFloat(ptr + 0L, x);
+ MemoryUtil.memPutFloat(ptr + 4L, y);
+ MemoryUtil.memPutFloat(ptr + 8L, z);
}
public static float getX(long ptr) {
- return MemoryUtil.memGetFloat(ptr + 0);
+ return MemoryUtil.memGetFloat(ptr + 0L);
}
public static float getY(long ptr) {
- return MemoryUtil.memGetFloat(ptr + 4);
+ return MemoryUtil.memGetFloat(ptr + 4L);
}
public static float getZ(long ptr) {
- return MemoryUtil.memGetFloat(ptr + 8);
+ return MemoryUtil.memGetFloat(ptr + 8L);
}
}
diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/buffer/VertexBufferWriter.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/buffer/VertexBufferWriter.java
index 002a2bd986..b50a500cbe 100644
--- a/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/buffer/VertexBufferWriter.java
+++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/vertex/buffer/VertexBufferWriter.java
@@ -38,6 +38,24 @@ static VertexBufferWriter tryOf(VertexConsumer consumer) {
return null;
}
+ /**
+ * Converts a {@link VertexConsumer} into a {@link VertexBufferWriter} if possible and only if it can use optimized
+ * writes with the requested vertex format.
+ *
+ * @param consumer The vertex consumer to create a writer for
+ * @param format The vertex format that will be written
+ * @return An implementation of {@link VertexBufferWriter}, or null if the vertex consumer does not support optimized
+ * writes for the requested vertex format
+ */
+ @Nullable
+ static VertexBufferWriter tryOf(VertexConsumer consumer, VertexFormat format) {
+ if (consumer instanceof VertexBufferWriter writer && writer.canUseIntrinsics(format)) {
+ return writer;
+ }
+
+ return null;
+ }
+
private static RuntimeException createUnsupportedVertexConsumerThrowable(VertexConsumer consumer) {
var clazz = consumer.getClass();
var name = clazz.getName();
@@ -73,6 +91,16 @@ default boolean canUseIntrinsics() {
return true;
}
+ /**
+ * Checks whether this writer can use optimized writes for vertices with the specified format.
+ *
+ * @param format The vertex format that will be written
+ * @return true if this writer and any nested consumers can use intrinsics for the vertex format
+ */
+ default boolean canUseIntrinsics(VertexFormat format) {
+ return this.canUseIntrinsics();
+ }
+
/**
* Creates a copy of the source data and pushes it into the specified {@param writer}. This is useful for when
* you need to use to re-use the source data after the call, and do not want the {@param writer} to modify
diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/BugChecks.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/BugChecks.java
new file mode 100644
index 0000000000..e42457e2fb
--- /dev/null
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/BugChecks.java
@@ -0,0 +1,75 @@
+package net.caffeinemc.mods.sodium.client.compatibility.checks;
+
+/**
+ * "Checks" are used to determine whether the environment we are running within is actually reasonable. Most often,
+ * failing checks will crash the game and prompt the user for intervention.
+ */
+class BugChecks {
+ /**
+ * Some older drivers for Intel Gen7 Graphics on Windows are defective and will never return from
+ * a call to
glClientWaitSync
. As there is no way to recover the main thread after the
+ * deadlock occurs, trying to work around this seems to be impossible. Updating the driver to version
+ * 15.33.53.5161 resolves the problem, and is the currently recommended solution.
+ * GitHub Issue
+ */
+ public static final boolean ISSUE_899 = configureCheck("issue899", true);
+
+ /**
+ * Since version 526.47 of the NVIDIA Graphics Driver, the OpenGL user-mode driver will attempt to detect
+ * Minecraft with hard-coded logic in the driver, and enable broken optimizations (referred to as "Threaded
+ * Optimizations"). This will cause crashes during framebuffer initialization (and some draw calls) for unclear
+ * reasons, especially on systems with hybrid graphics. Furthermore, performance is often severely degraded
+ * due to bubbles in the NVIDIA command submission thread, especially when other mods query context state.
+ *
+ * Sodium can prevent the detection of Minecraft and the enablement of these unstable optimizations, but the logic
+ * is extensive and depends heavily on the exact operating system and graphics driver version used. Some older
+ * graphics driver versions have no workaround available presently.
+ * GitHub Issue
+ */
+ public static final boolean ISSUE_1486 = configureCheck("issue1486", true);
+
+ /**
+ * Older versions of the RivaTuner Statistics Server will attempt to use legacy OpenGL functions in a Core
+ * profile, which causes either a crash or excessive log spam, depending on the OpenGL implementation. This is
+ * because RivaTuner relies on replacing the OpenGL context immediately after the application initializes it,
+ * but does so improperly when a No Error Context is used. This problem can't be avoided by simply configuring
+ * RivaTuner to not inject into Minecraft, as it *always* injects first to modify the context, and *then* disables
+ * itself.
+ * GitHub Issue
+ */
+ public static final boolean ISSUE_2048 = configureCheck("issue2048", true);
+
+ /**
+ * LWJGL does not provide API stability guarantees for other libraries using it to create their own C bindings.
+ * Because of this, the game will crash due to breaking method/type signature changes very early at startup. We
+ * should not be using these "internal" classes in LWJGL, but the amount of work involved doing everything ourselves
+ * is astronomical. When Minecraft ships a version of OpenJDK with the Foreign Function & Memory API, we can replace
+ * our dependency on LWJGL and remove this check.
+ * GitHub Issue
+ */
+ public static final boolean ISSUE_2561 = configureCheck("issue2561", true);
+
+ /**
+ * ASUS's GPU Tweak III does not correctly restore OpenGL context state after rendering its in-game overlay,
+ * which frequently causes graphical corruption, excessive log file spam, and crashes. These problems are
+ * actually reproducible without any mods installed, but it seems to be exacerbated with Sodium due to how different
+ * the rendering pipeline is. This problem can be avoided by configuring the application to not inject into
+ * Minecraft (or Java applications).
+ * GitHub Issue
+ */
+ public static final boolean ISSUE_2637 = configureCheck("issue2637", true);
+
+ private static boolean configureCheck(String name, boolean defaultValue) {
+ var propertyValue = System.getProperty(getPropertyKey(name), null);
+
+ if (propertyValue == null) {
+ return defaultValue;
+ }
+
+ return Boolean.parseBoolean(propertyValue);
+ }
+
+ private static String getPropertyKey(String name) {
+ return "sodium.checks." + name;
+ }
+}
diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/GraphicsDriverChecks.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/GraphicsDriverChecks.java
new file mode 100644
index 0000000000..d37572b0f4
--- /dev/null
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/GraphicsDriverChecks.java
@@ -0,0 +1,63 @@
+package net.caffeinemc.mods.sodium.client.compatibility.checks;
+
+import net.caffeinemc.mods.sodium.client.compatibility.environment.GlContextInfo;
+import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterVendor;
+import net.caffeinemc.mods.sodium.client.compatibility.workarounds.intel.IntelWorkarounds;
+import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaDriverVersion;
+import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds;
+import net.caffeinemc.mods.sodium.client.platform.NativeWindowHandle;
+import net.caffeinemc.mods.sodium.client.platform.PlatformHelper;
+
+class GraphicsDriverChecks {
+ static void postContextInit(NativeWindowHandle window, GlContextInfo context) {
+ var vendor = GraphicsAdapterVendor.fromContext(context);
+
+ if (vendor == GraphicsAdapterVendor.UNKNOWN) {
+ return;
+ }
+
+ if (vendor == GraphicsAdapterVendor.INTEL && BugChecks.ISSUE_899) {
+ var installedVersion = IntelWorkarounds.findIntelDriverMatchingBug899();
+
+ if (installedVersion != null) {
+ var installedVersionString = installedVersion.toString();
+
+ PlatformHelper.showCriticalErrorAndClose(window,
+ "Sodium Renderer - Unsupported Driver",
+ """
+ The game failed to start because the currently installed Intel Graphics Driver is not \
+ compatible.
+
+ Installed version: ###CURRENT_DRIVER###
+ Required version: 10.18.10.5161 (or newer)
+
+ Please click the 'Help' button to read more about how to fix this problem."""
+ .replace("###CURRENT_DRIVER###", installedVersionString),
+ "https://link.caffeinemc.net/help/sodium/graphics-driver/windows/intel/gh-899");
+ }
+ }
+
+ if (vendor == GraphicsAdapterVendor.NVIDIA && BugChecks.ISSUE_1486) {
+ var installedVersion = NvidiaWorkarounds.findNvidiaDriverMatchingBug1486();
+
+ if (installedVersion != null) {
+ var installedVersionString = NvidiaDriverVersion.parse(installedVersion)
+ .toString();
+
+ PlatformHelper.showCriticalErrorAndClose(window,
+ "Sodium Renderer - Unsupported Driver",
+ """
+ The game failed to start because the currently installed NVIDIA Graphics Driver is not \
+ compatible.
+
+ Installed version: ###CURRENT_DRIVER###
+ Required version: 536.23 (or newer)
+
+ Please click the 'Help' button to read more about how to fix this problem."""
+ .replace("###CURRENT_DRIVER###", installedVersionString),
+ "https://link.caffeinemc.net/help/sodium/graphics-driver/windows/nvidia/gh-1486");
+
+ }
+ }
+ }
+}
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner.java
similarity index 93%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner.java
index 8107366ef7..8af1a8d70f 100644
--- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner.java
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/ModuleScanner.java
@@ -105,10 +105,10 @@ You appear to be using an older version of RivaTuner Statistics Server (RTSS) wh
You must either update to a newer version (7.3.4 and later) or close the RivaTuner Statistics Server application.
For more information on how to solve this problem, click the 'Help' button.""",
- "https://github.com/CaffeineMC/sodium/wiki/Known-Issues#rtss-incompatible");
+ "https://link.caffeinemc.net/help/sodium/incompatible-software/rivatuner-statistics-server/gh-2048");
throw new RuntimeException("The installed version of RivaTuner Statistics Server (RTSS) is not compatible with Sodium, " +
- "see here for more details: https://github.com/CaffeineMC/sodium/wiki/Known-Issues#rtss-incompatible");
+ "see here for more details: https://link.caffeinemc.net/help/sodium/incompatible-software/rivatuner-statistics-server/gh-2048");
}
}
@@ -133,10 +133,10 @@ private static void checkASUSGpuTweakIII(NativeWindowHandle window) {
b) Completely uninstall the ASUS GPU Tweak III application.
For more information on how to solve this problem, click the 'Help' button.""",
- "https://github.com/CaffeineMC/sodium/wiki/Known-Issues#asus-gtiii-incompatible");
+ "https://link.caffeinemc.net/help/sodium/incompatible-software/asus-gtiii/gh-2637");
throw new RuntimeException("ASUS GPU Tweak III is not compatible with Minecraft, " +
- "see here for more details: https://github.com/CaffeineMC/sodium/wiki/Known-Issues#asus-gtiii-incompatible");
+ "see here for more details: https://link.caffeinemc.net/help/sodium/incompatible-software/asus-gtiii/gh-2637");
}
private static @Nullable WindowsFileVersion findRTSSModuleVersion() {
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PostLaunchChecks.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PostLaunchChecks.java
similarity index 74%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PostLaunchChecks.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PostLaunchChecks.java
index 85f5d92488..79be746ff2 100644
--- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PostLaunchChecks.java
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PostLaunchChecks.java
@@ -1,7 +1,8 @@
package net.caffeinemc.mods.sodium.client.compatibility.checks;
-import net.caffeinemc.mods.sodium.client.console.Console;
-import net.caffeinemc.mods.sodium.client.console.message.MessageLevel;
+import net.caffeinemc.mods.sodium.client.compatibility.environment.GlContextInfo;
+import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds;
+import net.caffeinemc.mods.sodium.client.platform.NativeWindowHandle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -12,13 +13,14 @@
public class PostLaunchChecks {
private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-PostlaunchChecks");
- public static void onContextInitialized() {
+ public static void onContextInitialized(NativeWindowHandle window, GlContextInfo context) {
+ GraphicsDriverChecks.postContextInit(window, context);
+ NvidiaWorkarounds.applyContextChanges(context);
+
// FIXME: This can be determined earlier, but we can't access the GUI classes in pre-launch
if (isUsingPojavLauncher()) {
- Console.instance().logMessage(MessageLevel.SEVERE, "sodium.console.pojav_launcher", true, 30.0);
- LOGGER.error("It appears that PojavLauncher is being used with an OpenGL compatibility layer. This will " +
- "likely cause severe performance issues, graphical issues, and crashes when used with Sodium. This " +
- "configuration is not supported -- you are on your own!");
+ throw new RuntimeException("It appears that you are using PojavLauncher, which is not supported when " +
+ "using Sodium. Please check your mods list.");
}
}
diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PreLaunchChecks.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PreLaunchChecks.java
new file mode 100644
index 0000000000..044c49fb64
--- /dev/null
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/checks/PreLaunchChecks.java
@@ -0,0 +1,162 @@
+package net.caffeinemc.mods.sodium.client.compatibility.checks;
+
+import net.caffeinemc.mods.sodium.client.platform.PlatformHelper;
+import org.lwjgl.Version;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.URL;
+import java.security.CodeSource;
+import java.security.ProtectionDomain;
+
+/**
+ * Performs OpenGL driver validation before the game creates an OpenGL context. This runs during the earliest possible
+ * opportunity at game startup, and uses a custom hardware prober to search for problematic drivers.
+ */
+public class PreLaunchChecks {
+ private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-PreLaunchChecks");
+
+ // These version constants are inlined at compile time.
+ private static final String REQUIRED_LWJGL_VERSION =
+ Version.VERSION_MAJOR + "." + Version.VERSION_MINOR + "." + Version.VERSION_REVISION;
+
+ public static void checkEnvironment() {
+ if (BugChecks.ISSUE_2561) {
+ checkLwjglRuntimeVersion();
+ }
+ }
+
+ private static void checkLwjglRuntimeVersion() {
+ if (isUsingKnownCompatibleLwjglVersion()) {
+ return;
+ }
+
+
+ String launcher = getLauncherBrand();
+ String codeSource = getLwjglCodeSource();
+ String codeSourceFilename = null;
+ if (codeSource != null) {
+ String[] components = codeSource.split("/");
+ codeSourceFilename = components[components.length-1];
+ }
+
+ if (codeSource != null) {
+ LOGGER.info("Problematic LWJGL version source: {}", codeSource);
+ }
+
+ boolean isCustomLauncher = !launcher.equals("minecraft-launcher") && !launcher.equals("unknown");
+ boolean isLikelyCausedByLauncher = false;
+ String isLikelyCausedByMod = null;
+
+ if (isCustomLauncher) {
+ if (codeSourceFilename != null && (codeSourceFilename.startsWith("lwjgl-") || codeSource.contains("/lwjgl/"))) {
+ isLikelyCausedByLauncher = true;
+ }
+ }
+ if (!isLikelyCausedByLauncher) {
+ if (codeSource != null && codeSource.endsWith("/mods/"+codeSourceFilename)) {
+ isLikelyCausedByMod = codeSourceFilename;
+ }
+ }
+
+ String advice;
+ if (isLikelyCausedByMod != null) {
+ advice = """
+ This issue seems to be caused by ###MOD###.
+
+ Removing ###MOD### from your mods folder may fix this issue."""
+ .replace("###MOD###", isLikelyCausedByMod);
+ } else if (launcher.equalsIgnoreCase("prismlauncher")) {
+ advice = """
+ It appears you are using Prism Launcher to start the game. You can \
+ likely fix this problem by opening your instance settings and navigating to the Version \
+ section in the sidebar.""";
+ } else if (isLikelyCausedByLauncher) {
+ advice = """
+ You seem to be using ###LAUNCHER###. This issue is likely caused by ###LAUNCHER###.
+
+ You must change the LWJGL version in your launcher to continue. \
+ This is usually controlled by the settings for a profile or instance in your launcher.
+
+ If you need assistance fixing the LWJGL version, you should contact ###LAUNCHER###, not Sodium."""
+ .replace("###LAUNCHER###", launcher);
+ } else if (isCustomLauncher) {
+ advice = """
+ You seem to be using ###LAUNCHER###.
+
+ You must change the LWJGL version in your launcher to continue. \
+ This is usually controlled by the settings for a profile or instance in your launcher."""
+ .replace("###LAUNCHER###", launcher);
+ } else {
+ advice = """
+ You must change the LWJGL version in your launcher to continue. \
+ This is usually controlled by the settings for a profile or instance in your launcher.""";
+ }
+
+ String message = """
+ The game failed to start because the currently active LWJGL version is not \
+ compatible.
+
+ Installed version: ###CURRENT_VERSION###
+ Required version: ###REQUIRED_VERSION###
+
+ ###ADVICE_STRING###"""
+ .replace("###CURRENT_VERSION###", Version.getVersion())
+ .replace("###REQUIRED_VERSION###", REQUIRED_LWJGL_VERSION)
+ .replace("###ADVICE_STRING###", advice);
+
+ PlatformHelper.showCriticalErrorAndClose(null, "Sodium Renderer - Unsupported LWJGL", message,
+ "https://link.caffeinemc.net/help/sodium/runtime-issue/lwjgl3/gh-2561");
+ }
+
+ private static String getLwjglCodeSource() {
+ try {
+ ProtectionDomain domain = Version.class.getProtectionDomain();
+ if (domain != null) {
+ CodeSource source = domain.getCodeSource();
+ if (source != null) {
+ URL location = source.getLocation();
+ if (location != null) {
+ String path = location.getPath();
+ if (path != null) {
+ path = path.replace('\\', '/');
+ path = path.split("!")[0];
+ return path;
+ }
+ }
+ }
+ }
+ } catch (Throwable t) {
+ LOGGER.error("Error while checking code source of LWJGL", t);
+ }
+ return null;
+ }
+
+ private static boolean isUsingKnownCompatibleLwjglVersion() {
+ return Version.getVersion()
+ .startsWith(REQUIRED_LWJGL_VERSION);
+ }
+
+ private static String getLauncherBrand() {
+ String brand = System.getProperty("minecraft.launcher.brand", "unknown");
+ if (brand.equals("unknown")) {
+ // Lunar Client uses a custom set of launch arguments
+ // which don't include minecraft.launcher.brand
+ if (isClassLoaded("com.moonsworth.lunar.genesis.Genesis")) {
+ return "Lunar Client";
+ } else if (System.getProperty("lunar.webosr.url") != null) {
+ return "Lunar Client";
+ }
+ }
+ return brand;
+ }
+
+ private static boolean isClassLoaded(String className) {
+ try {
+ Class.forName(className);
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+}
diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/GlContextInfo.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/GlContextInfo.java
new file mode 100644
index 0000000000..f4aa02f263
--- /dev/null
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/GlContextInfo.java
@@ -0,0 +1,18 @@
+package net.caffeinemc.mods.sodium.client.compatibility.environment;
+
+import org.lwjgl.opengl.GL11C;
+
+import java.util.Objects;
+
+public record GlContextInfo(String vendor, String renderer, String version) {
+ public static GlContextInfo create() {
+ String vendor = Objects.requireNonNull(GL11C.glGetString(GL11C.GL_VENDOR),
+ "GL_VENDOR is NULL");
+ String renderer = Objects.requireNonNull(GL11C.glGetString(GL11C.GL_RENDERER),
+ "GL_RENDERER is NULL");
+ String version = Objects.requireNonNull(GL11C.glGetString(GL11C.GL_VERSION),
+ "GL_VERSION is NULL");
+
+ return new GlContextInfo(vendor, renderer, version);
+ }
+}
diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/OsUtils.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/OsUtils.java
new file mode 100644
index 0000000000..c232c0faa9
--- /dev/null
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/OsUtils.java
@@ -0,0 +1,36 @@
+package net.caffeinemc.mods.sodium.client.compatibility.environment;
+
+import java.util.Locale;
+
+public class OsUtils {
+ private static final OperatingSystem OS = determineOs();
+
+ public static OperatingSystem determineOs() {
+ var name = System.getProperty("os.name");
+
+ if (name != null) {
+ var normalized = name.toLowerCase(Locale.ROOT);
+
+ if (normalized.startsWith("windows")) {
+ return OperatingSystem.WIN;
+ } else if (normalized.startsWith("mac")) {
+ return OperatingSystem.MAC;
+ } else if (normalized.startsWith("linux")) {
+ return OperatingSystem.LINUX;
+ }
+ }
+
+ return OperatingSystem.UNKNOWN;
+ }
+
+ public static OperatingSystem getOs() {
+ return OS;
+ }
+
+ public enum OperatingSystem {
+ WIN,
+ MAC,
+ LINUX,
+ UNKNOWN
+ }
+}
\ No newline at end of file
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterInfo.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterInfo.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterInfo.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterInfo.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe.java
similarity index 99%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe.java
index 4bfb59179a..64a251bb54 100644
--- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe.java
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterProbe.java
@@ -2,7 +2,6 @@
import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils;
import net.caffeinemc.mods.sodium.client.platform.windows.api.d3dkmt.D3DKMT;
-import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterVendor.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterVendor.java
new file mode 100644
index 0000000000..f18f4c4cb9
--- /dev/null
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/environment/probe/GraphicsAdapterVendor.java
@@ -0,0 +1,77 @@
+package net.caffeinemc.mods.sodium.client.compatibility.environment.probe;
+
+import net.caffeinemc.mods.sodium.client.compatibility.environment.GlContextInfo;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.regex.Pattern;
+
+public enum GraphicsAdapterVendor {
+ NVIDIA,
+ AMD,
+ INTEL,
+ UNKNOWN;
+
+ // Intel Gen 4, 5, 6 - ig4icd
+ // Intel Gen 7 - ig7icd
+ // Intel Gen 7.5 - ig75icd
+ // Intel Gen 8 - ig8icd
+ // Intel Gen 9, 9.5 - ig9icd
+ // Intel Gen 11 - ig11icd
+ // Intel Xe-LP - ig12icd (early drivers) or igxelpicd (later drivers)
+ // Intel Xe-HP - igxehpicd
+ // Intel Xe-HPG - igxehpgicd
+ // Intel Xe2-LPG - igxe2lpgicd
+ // Intel Xe2-HPG - igxe2hpgicd
+ private static final Pattern INTEL_ICD_PATTERN =
+ Pattern.compile("ig(4|7|75|8|9|11|12|(xe2?(hpg?|lpg?)))icd(32|64)\\.dll", Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern NVIDIA_ICD_PATTERN =
+ Pattern.compile("nvoglv(32|64)\\.dll", Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern AMD_ICD_PATTERN =
+ Pattern.compile("(atiglpxx|atig6pxx)\\.dll", Pattern.CASE_INSENSITIVE);
+
+ @NotNull
+ static GraphicsAdapterVendor fromPciVendorId(String vendor) {
+ if (vendor.contains("0x1002")) {
+ return AMD;
+ } else if (vendor.contains("0x10de")) {
+ return NVIDIA;
+ } else if (vendor.contains("0x8086")) {
+ return INTEL;
+ }
+
+ return UNKNOWN;
+ }
+
+ @NotNull
+ public static GraphicsAdapterVendor fromIcdName(String name) {
+ if (matchesPattern(INTEL_ICD_PATTERN, name)) {
+ return INTEL;
+ } else if (matchesPattern(NVIDIA_ICD_PATTERN, name)) {
+ return NVIDIA;
+ } else if (matchesPattern(AMD_ICD_PATTERN, name)) {
+ return AMD;
+ } else {
+ return UNKNOWN;
+ }
+ }
+
+ @NotNull
+ public static GraphicsAdapterVendor fromContext(GlContextInfo context) {
+ var vendor = context.vendor();
+
+ return switch (vendor) {
+ case "NVIDIA Corporation" -> NVIDIA;
+ case "Intel", "Intel Open Source Technology Center" -> INTEL;
+ case "AMD", "ATI Technologies Inc." -> AMD;
+ default -> UNKNOWN;
+ };
+
+ }
+
+ private static boolean matchesPattern(Pattern pattern, String name) {
+ return pattern.matcher(name)
+ .matches();
+ }
+}
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/Workarounds.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/Workarounds.java
similarity index 57%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/Workarounds.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/Workarounds.java
index 1e6afc0cad..57571c3f14 100644
--- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/Workarounds.java
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/Workarounds.java
@@ -1,15 +1,15 @@
package net.caffeinemc.mods.sodium.client.compatibility.workarounds;
import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils;
-import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterInfo;
-import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterProbe;
-import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterVendor;
-import net.caffeinemc.mods.sodium.client.platform.windows.api.d3dkmt.D3DKMT;
-import org.jetbrains.annotations.Nullable;
+import net.caffeinemc.mods.sodium.client.compatibility.workarounds.amd.AmdWorkarounds;
+import net.caffeinemc.mods.sodium.client.compatibility.workarounds.intel.IntelWorkarounds;
+import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.*;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@@ -37,59 +37,26 @@ private static Set findNecessaryWorkarounds() {
var workarounds = EnumSet.noneOf(Reference.class);
var operatingSystem = OsUtils.getOs();
- var graphicsAdapters = GraphicsAdapterProbe.getAdapters();
+ if (NvidiaWorkarounds.isNvidiaGraphicsCardPresent()) {
+ workarounds.add(Reference.NVIDIA_THREADED_OPTIMIZATIONS_BROKEN);
+ }
- if (isUsingNvidiaGraphicsCard(operatingSystem, graphicsAdapters)) {
- workarounds.add(Reference.NVIDIA_THREADED_OPTIMIZATIONS);
+ if (AmdWorkarounds.isAmdGraphicsCardPresent() && operatingSystem == OsUtils.OperatingSystem.WIN) {
+ workarounds.add(Reference.AMD_GAME_OPTIMIZATION_BROKEN);
}
- if (isUsingIntelGen8OrOlder()) {
+ if (IntelWorkarounds.isUsingIntelGen8OrOlder()) {
workarounds.add(Reference.INTEL_FRAMEBUFFER_BLIT_CRASH_WHEN_UNFOCUSED);
workarounds.add(Reference.INTEL_DEPTH_BUFFER_COMPARISON_UNRELIABLE);
}
if (operatingSystem == OsUtils.OperatingSystem.LINUX) {
- var session = System.getenv("XDG_SESSION_TYPE");
-
- if (session == null) {
- LOGGER.warn("Unable to determine desktop session type because the environment variable XDG_SESSION_TYPE " +
- "is not set! Your user session may not be configured correctly.");
- }
-
- if (Objects.equals(session, "wayland")) {
- // This will also apply under Xwayland, even though the problem does not happen there
- workarounds.add(Reference.NO_ERROR_CONTEXT_UNSUPPORTED);
- }
+ workarounds.add(Reference.NO_ERROR_CONTEXT_UNSUPPORTED);
}
return Collections.unmodifiableSet(workarounds);
}
- private static boolean isUsingIntelGen8OrOlder() {
- if (OsUtils.getOs() != OsUtils.OperatingSystem.WIN) {
- return false;
- }
-
- for (var adapter : GraphicsAdapterProbe.getAdapters()) {
- if (adapter instanceof D3DKMT.WDDMAdapterInfo wddmAdapterInfo) {
- @Nullable var driverName = wddmAdapterInfo.getOpenGlIcdName();
-
- // Intel OpenGL ICD for legacy GPUs
- if (driverName != null && driverName.matches("ig(7|75|8)icd(32|64)")) {
- return true;
- }
- }
- }
-
- return false;
- }
-
- private static boolean isUsingNvidiaGraphicsCard(OsUtils.OperatingSystem operatingSystem, Collection extends GraphicsAdapterInfo> adapters) {
-
- return (operatingSystem == OsUtils.OperatingSystem.WIN || operatingSystem == OsUtils.OperatingSystem.LINUX) &&
- adapters.stream().anyMatch(adapter -> adapter.vendor() == GraphicsAdapterVendor.NVIDIA);
- }
-
public static boolean isWorkaroundEnabled(Reference id) {
return ACTIVE_WORKAROUNDS.get()
.contains(id);
@@ -101,10 +68,11 @@ public enum Reference {
* performance issues and crashes.
* GitHub Issue
*/
- NVIDIA_THREADED_OPTIMIZATIONS,
+ NVIDIA_THREADED_OPTIMIZATIONS_BROKEN,
/**
- * Requesting a No Error Context causes a crash at startup when using a Wayland session.
+ * Requesting a No Error Context causes a crash at startup when using a Wayland session on GLFW
+ <3.4.
* GitHub Issue
*/
NO_ERROR_CONTEXT_UNSUPPORTED,
@@ -113,6 +81,7 @@ public enum Reference {
* Intel's graphics driver for Gen8 and older seems to be faulty and causes a crash when calling
* glFramebufferBlit after the window loses focus.
* GitHub Issue
+ * GitHub Issue
*/
INTEL_FRAMEBUFFER_BLIT_CRASH_WHEN_UNFOCUSED,
@@ -122,6 +91,15 @@ public enum Reference {
* the base model.
* GitHub Issue
*/
- INTEL_DEPTH_BUFFER_COMPARISON_UNRELIABLE
+ INTEL_DEPTH_BUFFER_COMPARISON_UNRELIABLE,
+
+ /**
+ * AMD's graphics driver starting at 25.10.2 does not correctly handle glMapBufferRange
+ * when minecraft is detected, causing terrain rendering to go invisible if the launcher allows minecraft
+ * to be detected. Most commonly this happens with some third party PVP clients,
+ * but can happen with other launchers.
+ * GitHub Issue
+ */
+ AMD_GAME_OPTIMIZATION_BROKEN
}
}
diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/amd/AmdWorkarounds.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/amd/AmdWorkarounds.java
new file mode 100644
index 0000000000..9b6ddbc396
--- /dev/null
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/amd/AmdWorkarounds.java
@@ -0,0 +1,74 @@
+package net.caffeinemc.mods.sodium.client.compatibility.workarounds.amd;
+
+import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils;
+import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterProbe;
+import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterVendor;
+import net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia.NvidiaWorkarounds;
+import net.caffeinemc.mods.sodium.client.platform.windows.WindowsCommandLine;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AmdWorkarounds {
+ private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-AmdWorkarounds");
+
+ public static boolean isAmdGraphicsCardPresent() {
+ return GraphicsAdapterProbe.getAdapters()
+ .stream()
+ .anyMatch(adapter -> adapter.vendor() == GraphicsAdapterVendor.AMD);
+ }
+
+ public static void undoEnvironmentChanges() {
+ if (OsUtils.getOs() == OsUtils.OperatingSystem.WIN) {
+ undoEnvironmentChanges$Windows();
+ }
+ }
+
+ private static void undoEnvironmentChanges$Windows() {
+ WindowsCommandLine.resetCommandLine();
+ }
+
+ public static void applyEnvironmentChanges() {
+ // We can't know if the OpenGL context will actually be initialized using the AMD ICD, but we need to
+ // modify the process environment *now* otherwise the driver will initialize with bad settings. For non-AMD
+ // drivers, these workarounds are not likely to cause issues.
+ if (!isAmdGraphicsCardPresent()) {
+ return;
+ }
+
+ // Skip applying the AMD workaround if the user also has an Nvidia gpu, to avoid attempting to overwrite the
+ // process command line twice.
+ if (NvidiaWorkarounds.isNvidiaGraphicsCardPresent()) {
+ return;
+ }
+
+ try {
+ if (OsUtils.getOs() == OsUtils.OperatingSystem.WIN) {
+ LOGGER.info("Modifying process environment to apply workarounds for the AMD graphics driver...");
+
+ applyEnvironmentChanges$Windows();
+ }
+ } catch (Throwable t) {
+ LOGGER.error("Failed to modify the process environment", t);
+ logWarning();
+ }
+ }
+
+
+ private static void applyEnvironmentChanges$Windows() {
+ // The new AMD drivers rely on parsing the command line arguments to detect Minecraft.
+ // When they do they apply an optimization that is broken with sodium present
+ // This stops AMD drivers from detecting the game
+ WindowsCommandLine.setCommandLine("net.caffeinemc.sodium / net.minecraft.client.main.Main /");
+ }
+
+ private static void logWarning() {
+ LOGGER.error("READ ME!");
+ LOGGER.error("READ ME! The workarounds for the AMD Graphics Driver did not apply correctly!");
+ LOGGER.error("READ ME! You may run into unexplained graphical issues.");
+ LOGGER.error("READ ME! More information about what went wrong can be found above this message.");
+ LOGGER.error("READ ME!");
+ LOGGER.error("READ ME! Please help us understand why this problem occurred by opening a bug report on our issue tracker:");
+ LOGGER.error("READ ME! https://github.com/CaffeineMC/sodium/issues");
+ LOGGER.error("READ ME!");
+ }
+}
diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/intel/IntelWorkarounds.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/intel/IntelWorkarounds.java
new file mode 100644
index 0000000000..94058d1ded
--- /dev/null
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/intel/IntelWorkarounds.java
@@ -0,0 +1,59 @@
+package net.caffeinemc.mods.sodium.client.compatibility.workarounds.intel;
+
+import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils;
+import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterProbe;
+import net.caffeinemc.mods.sodium.client.platform.windows.WindowsFileVersion;
+import net.caffeinemc.mods.sodium.client.platform.windows.api.d3dkmt.D3DKMT;
+import org.jetbrains.annotations.Nullable;
+
+public class IntelWorkarounds {
+ // https://github.com/CaffeineMC/sodium/issues/899
+ public static @Nullable WindowsFileVersion findIntelDriverMatchingBug899() {
+ if (OsUtils.getOs() != OsUtils.OperatingSystem.WIN) {
+ return null;
+ }
+
+ for (var adapter : GraphicsAdapterProbe.getAdapters()) {
+ if (adapter instanceof D3DKMT.WDDMAdapterInfo wddmAdapterInfo) {
+ String driverName = wddmAdapterInfo.getOpenGlIcdName();
+
+ if (driverName == null) {
+ continue;
+ }
+
+ var driverVersion = wddmAdapterInfo.openglIcdVersion();
+
+ // Intel OpenGL ICD for Generation 7 GPUs
+ if (driverName.matches("ig7icd(32|64).dll")) {
+ // https://www.intel.com/content/www/us/en/support/articles/000005654/graphics.html
+ // Anything which matches the 15.33 driver scheme (WDDM x.y.10.w) should be checked
+ // Drivers before build 5161 are assumed to have bugs with synchronization primitives
+ if (driverVersion.z() == 10 && driverVersion.w() < 5161) {
+ return driverVersion;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static boolean isUsingIntelGen8OrOlder() {
+ if (OsUtils.getOs() != OsUtils.OperatingSystem.WIN) {
+ return false;
+ }
+
+ for (var adapter : GraphicsAdapterProbe.getAdapters()) {
+ if (adapter instanceof D3DKMT.WDDMAdapterInfo wddmAdapterInfo) {
+ var driverName = wddmAdapterInfo.getOpenGlIcdName();
+
+ // Intel OpenGL ICD for legacy GPUs
+ if (driverName != null && driverName.matches("ig(7|75|8)icd(32|64)\\.dll")) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaDriverVersion.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaDriverVersion.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaDriverVersion.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaDriverVersion.java
diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaWorkarounds.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaWorkarounds.java
new file mode 100644
index 0000000000..572dc8b088
--- /dev/null
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/compatibility/workarounds/nvidia/NvidiaWorkarounds.java
@@ -0,0 +1,152 @@
+package net.caffeinemc.mods.sodium.client.compatibility.workarounds.nvidia;
+
+import net.caffeinemc.mods.sodium.client.compatibility.environment.GlContextInfo;
+import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils;
+import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils.OperatingSystem;
+import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterProbe;
+import net.caffeinemc.mods.sodium.client.compatibility.environment.probe.GraphicsAdapterVendor;
+import net.caffeinemc.mods.sodium.client.compatibility.workarounds.Workarounds;
+import net.caffeinemc.mods.sodium.client.platform.unix.Libc;
+import net.caffeinemc.mods.sodium.client.platform.windows.WindowsCommandLine;
+import net.caffeinemc.mods.sodium.client.platform.windows.WindowsFileVersion;
+import net.caffeinemc.mods.sodium.client.platform.windows.api.d3dkmt.D3DKMT;
+import org.jetbrains.annotations.Nullable;
+import org.lwjgl.opengl.GL;
+import org.lwjgl.opengl.GL32C;
+import org.lwjgl.opengl.KHRDebug;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NvidiaWorkarounds {
+ private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-NvidiaWorkarounds");
+
+ public static boolean isNvidiaGraphicsCardPresent() {
+ return GraphicsAdapterProbe.getAdapters()
+ .stream()
+ .anyMatch(adapter -> adapter.vendor() == GraphicsAdapterVendor.NVIDIA);
+ }
+
+ // https://github.com/CaffeineMC/sodium/issues/1486
+ // The way which NVIDIA tries to detect the Minecraft process could not be circumvented until fairly recently
+ // So we require that an up-to-date graphics driver is installed so that our workarounds can disable the Threaded
+ // Optimizations driver hack.
+ public static @Nullable WindowsFileVersion findNvidiaDriverMatchingBug1486() {
+ // The Linux driver has two separate branches which have overlapping version numbers, despite also having
+ // different feature sets. As a result, we can't reliably determine which Linux drivers are broken...
+ if (OsUtils.getOs() != OperatingSystem.WIN) {
+ return null;
+ }
+
+ for (var adapter : GraphicsAdapterProbe.getAdapters()) {
+ if (adapter.vendor() != GraphicsAdapterVendor.NVIDIA) {
+ continue;
+ }
+
+ if (adapter instanceof D3DKMT.WDDMAdapterInfo wddmAdapterInfo) {
+ var driverVersion = wddmAdapterInfo.openglIcdVersion();
+
+ if (driverVersion.z() == 15) { // Only match 5XX.XX drivers
+ // Broken in x.y.15.2647 (526.47)
+ // Fixed in x.y.15.3623 (536.23)
+ if (driverVersion.w() >= 2647 && driverVersion.w() < 3623) {
+ return driverVersion;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static void applyEnvironmentChanges() {
+ // We can't know if the OpenGL context will actually be initialized using the NVIDIA ICD, but we need to
+ // modify the process environment *now* otherwise the driver will initialize with bad settings. For non-NVIDIA
+ // drivers, these workarounds are not likely to cause issues.
+ if (!isNvidiaGraphicsCardPresent()) {
+ return;
+ }
+
+ LOGGER.info("Modifying process environment to apply workarounds for the NVIDIA graphics driver...");
+
+ try {
+ if (OsUtils.getOs() == OperatingSystem.WIN) {
+ applyEnvironmentChanges$Windows();
+ } else if (OsUtils.getOs() == OperatingSystem.LINUX) {
+ applyEnvironmentChanges$Linux();
+ }
+ } catch (Throwable t) {
+ LOGGER.error("Failed to modify the process environment", t);
+ logWarning();
+ }
+ }
+
+
+ private static void applyEnvironmentChanges$Windows() {
+ // The NVIDIA drivers rely on parsing the command line arguments to detect Minecraft. We need to
+ // make sure that it detects the game so that *some* important optimizations are applied. Later,
+ // we will try to enable GL_DEBUG_OUTPUT_SYNCHRONOUS so that "Threaded Optimizations" cannot
+ // be enabled.
+ WindowsCommandLine.setCommandLine("net.caffeinemc.sodium / net.minecraft.client.main.Main /");
+ }
+
+ private static void applyEnvironmentChanges$Linux() {
+ // Unlike Windows, we can just request that it not use threaded optimizations instead.
+ Libc.setEnvironmentVariable("__GL_THREADED_OPTIMIZATIONS", "0");
+ }
+
+ public static void undoEnvironmentChanges() {
+ if (OsUtils.getOs() == OperatingSystem.WIN) {
+ undoEnvironmentChanges$Windows();
+ }
+ }
+
+ private static void undoEnvironmentChanges$Windows() {
+ WindowsCommandLine.resetCommandLine();
+ }
+
+ public static void applyContextChanges(GlContextInfo context) {
+ // The context may not have been initialized with the NVIDIA ICD, even if we think there is an NVIDIA
+ // graphics adapter in use. Because enabling these workarounds have the potential to severely hurt performance
+ // on other drivers, make sure we exit now.
+ if (GraphicsAdapterVendor.fromContext(context) != GraphicsAdapterVendor.NVIDIA) {
+ return;
+ }
+
+ LOGGER.info("Modifying OpenGL context to apply workarounds for the NVIDIA graphics driver...");
+
+ if (Workarounds.isWorkaroundEnabled(Workarounds.Reference.NVIDIA_THREADED_OPTIMIZATIONS_BROKEN)) {
+ if (OsUtils.getOs() == OperatingSystem.WIN) {
+ applyContextChanges$Windows();
+ }
+ }
+ }
+
+ private static void applyContextChanges$Windows() {
+ // On Windows, the NVIDIA drivers do not have any environment variable to control whether
+ // "Threaded Optimizations" are enabled. But we can enable the "GL_DEBUG_OUTPUT_SYNCHRONOUS" option to
+ // achieve the same effect.
+ var capabilities = GL.getCapabilities();
+
+ if (capabilities.GL_KHR_debug) {
+ LOGGER.info("Enabling GL_DEBUG_OUTPUT_SYNCHRONOUS to force the NVIDIA driver to disable threaded " +
+ "command submission");
+ GL32C.glEnable(KHRDebug.GL_DEBUG_OUTPUT_SYNCHRONOUS);
+ } else {
+ LOGGER.error("GL_KHR_debug does not appear to be supported, unable to disable threaded " +
+ "command submission!");
+ logWarning();
+ }
+ }
+
+ private static void logWarning() {
+ LOGGER.error("READ ME!");
+ LOGGER.error("READ ME! The workarounds for the NVIDIA Graphics Driver did not apply correctly!");
+ LOGGER.error("READ ME! You are very likely going to run into unexplained crashes and severe performance issues.");
+ LOGGER.error("READ ME! More information about what went wrong can be found above this message.");
+ LOGGER.error("READ ME!");
+ LOGGER.error("READ ME! Please help us understand why this problem occurred by opening a bug report on our issue tracker:");
+ LOGGER.error("READ ME! https://github.com/CaffeineMC/sodium/issues");
+ LOGGER.error("READ ME!");
+
+ }
+}
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/Console.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/console/Console.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/Console.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/console/Console.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/ConsoleSink.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/console/ConsoleSink.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/ConsoleSink.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/console/ConsoleSink.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/message/Message.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/console/message/Message.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/message/Message.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/console/message/Message.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/message/MessageLevel.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/console/message/MessageLevel.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/console/message/MessageLevel.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/console/message/MessageLevel.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/MessageBox.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/MessageBox.java
similarity index 92%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/MessageBox.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/MessageBox.java
index bf0cfb47cb..7aa49791f3 100644
--- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/MessageBox.java
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/MessageBox.java
@@ -1,6 +1,7 @@
package net.caffeinemc.mods.sodium.client.platform;
import net.caffeinemc.mods.sodium.client.compatibility.environment.OsUtils;
+import net.caffeinemc.mods.sodium.client.platform.windows.api.Shell32;
import net.caffeinemc.mods.sodium.client.platform.windows.api.User32;
import net.caffeinemc.mods.sodium.client.platform.windows.api.msgbox.MsgBoxCallback;
import net.caffeinemc.mods.sodium.client.platform.windows.api.msgbox.MsgBoxParamSw;
@@ -8,9 +9,6 @@
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
-import java.awt.*;
-import java.io.IOException;
-import java.net.URI;
import java.nio.ByteBuffer;
import java.util.Objects;
@@ -58,11 +56,7 @@ public void showMessageBox(NativeWindowHandle window,
if (helpUrl != null) {
msgBoxCallback = MsgBoxCallback.create(lpHelpInfo -> {
- try {
- Desktop.getDesktop().browse(URI.create(helpUrl));
- } catch (IOException e) {
- System.out.println("Failed to open! Giving up.");
- }
+ Shell32.browseUrl(window, helpUrl);
});
} else {
msgBoxCallback = null;
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/NativeWindowHandle.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/NativeWindowHandle.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/NativeWindowHandle.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/NativeWindowHandle.java
diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/PlatformHelper.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/PlatformHelper.java
new file mode 100644
index 0000000000..dfc1f2b77f
--- /dev/null
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/PlatformHelper.java
@@ -0,0 +1,29 @@
+package net.caffeinemc.mods.sodium.client.platform;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PlatformHelper {
+ private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-EarlyDriverScanner");
+
+ public static void showCriticalErrorAndClose(
+ @Nullable NativeWindowHandle window,
+ @NotNull String messageTitle,
+ @NotNull String messageBody,
+ @NotNull String helpUrl)
+ {
+ // Always print the information to the log file first, just in case we can't show the message box.
+ LOGGER.error("""
+ ###ERROR_DESCRIPTION###
+
+ For more information, please see: ###HELP_URL###"""
+ .replace("###ERROR_DESCRIPTION###", messageBody)
+ .replace("###HELP_URL###", helpUrl));
+
+ // Try to show a graphical message box (if the platform supports it) and shut down the game.
+ MessageBox.showMessageBox(window, MessageBox.IconType.ERROR, messageTitle, messageBody, helpUrl);
+ System.exit(1 /* failure code */);
+ }
+}
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/unix/Libc.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/unix/Libc.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/unix/Libc.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/unix/Libc.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsCommandLine.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsCommandLine.java
similarity index 75%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsCommandLine.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsCommandLine.java
index 3165ad6d1e..721bb63263 100644
--- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsCommandLine.java
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsCommandLine.java
@@ -2,6 +2,7 @@
import net.caffeinemc.mods.sodium.client.platform.windows.api.Kernel32;
import org.lwjgl.system.MemoryUtil;
+
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.Objects;
@@ -17,11 +18,15 @@ public static void setCommandLine(String modifiedCmdline) {
// Pointer into the command-line arguments stored within the Windows process structure
// We do not own this memory, and it should not be freed.
var pCmdline = Kernel32.getCommandLine();
+ var pCmdlineA = Kernel32.getCommandLineA();
// The original command-line the process was started with.
var cmdline = MemoryUtil.memUTF16(pCmdline);
var cmdlineLen = MemoryUtil.memLengthUTF16(cmdline, true);
+ var cmdlineA = MemoryUtil.memASCII(pCmdlineA);
+ var cmdLineLenA = MemoryUtil.memLengthASCII(cmdlineA, true);
+
if (MemoryUtil.memLengthUTF16(modifiedCmdline, true) > cmdlineLen) {
// We can never write a string which is larger than what we were given, as there
// may not be enough space remaining. Realistically, this should never happen, since
@@ -30,12 +35,18 @@ public static void setCommandLine(String modifiedCmdline) {
throw new BufferOverflowException();
}
+ if (MemoryUtil.memLengthASCII(modifiedCmdline, true) > cmdLineLenA) {
+ throw new BufferOverflowException();
+ }
+
ByteBuffer buffer = MemoryUtil.memByteBuffer(pCmdline, cmdlineLen);
+ ByteBuffer bufferA = MemoryUtil.memByteBuffer(pCmdlineA, cmdLineLenA);
// Write the new command line arguments into the process structure.
// The Windows API documentation explicitly says this is forbidden, but it *does* give us a pointer
// directly into the PEB structure, so...
MemoryUtil.memUTF16(modifiedCmdline, true, buffer);
+ MemoryUtil.memASCII(modifiedCmdline, true, bufferA);
// Make sure we can actually see our changes in the process structure
// We don't know if this could ever actually happen, but since we're doing something pretty hacky
@@ -43,8 +54,11 @@ public static void setCommandLine(String modifiedCmdline) {
if (!Objects.equals(modifiedCmdline, MemoryUtil.memUTF16(pCmdline))) {
throw new RuntimeException("Sanity check failed, the command line arguments did not appear to change");
}
+ if (!Objects.equals(modifiedCmdline, MemoryUtil.memASCII(pCmdlineA))) {
+ throw new RuntimeException("Sanity check failed, the command line arguments did not appear to change");
+ }
- ACTIVE_COMMAND_LINE_HOOK = new CommandLineHook(cmdline, buffer);
+ ACTIVE_COMMAND_LINE_HOOK = new CommandLineHook(cmdline, cmdlineA, buffer, bufferA);
}
public static void resetCommandLine() {
@@ -56,13 +70,17 @@ public static void resetCommandLine() {
private static class CommandLineHook {
private final String cmdline;
+ private final String cmdlineA;
private final ByteBuffer cmdlineBuf;
+ private final ByteBuffer cmdlineBufA;
private boolean active = true;
- private CommandLineHook(String cmdline, ByteBuffer cmdlineBuf) {
+ private CommandLineHook(String cmdline, String cmdlineA, ByteBuffer cmdlineBuf, ByteBuffer cmdlineBufA) {
this.cmdline = cmdline;
+ this.cmdlineA = cmdlineA;
this.cmdlineBuf = cmdlineBuf;
+ this.cmdlineBufA = cmdlineBufA;
}
public void uninstall() {
@@ -73,6 +91,7 @@ public void uninstall() {
// Restore the original value of the command line arguments
// Must be null-terminated (as it was given to us)
MemoryUtil.memUTF16(this.cmdline, true, this.cmdlineBuf);
+ MemoryUtil.memASCII(this.cmdlineA, true, this.cmdlineBufA);
this.active = false;
}
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsFileVersion.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsFileVersion.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsFileVersion.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/WindowsFileVersion.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Gdi32.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Gdi32.java
similarity index 80%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Gdi32.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Gdi32.java
index 3341963a38..680c071807 100644
--- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Gdi32.java
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Gdi32.java
@@ -1,6 +1,5 @@
package net.caffeinemc.mods.sodium.client.platform.windows.api;
-import org.apache.commons.lang3.Validate;
import org.lwjgl.system.JNI;
import org.lwjgl.system.SharedLibrary;
@@ -21,19 +20,24 @@ public static boolean isD3DKMTSupported() {
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/nf-d3dkmthk-d3dkmtqueryadapterinfo
public static int /* NTSTATUS */ nd3dKmtQueryAdapterInfo(long ptr /* D3DKMT_QUERYADAPTERINFO */) {
- Validate.isTrue(PFN_D3DKMTQueryAdapterInfo != NULL);
- return JNI.callPI(ptr, PFN_D3DKMTQueryAdapterInfo);
+ return JNI.callPI(ptr, checkPfn(PFN_D3DKMTQueryAdapterInfo));
}
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/nf-d3dkmthk-d3dkmtenumadapters2
public static int /* NTSTATUS */ nD3DKMTEnumAdapters(long ptr /* D3DKMT_ENUMADAPTERS */) {
- Validate.isTrue(PFN_D3DKMTEnumAdapters != NULL);
- return JNI.callPI(ptr, PFN_D3DKMTEnumAdapters);
+ return JNI.callPI(ptr, checkPfn(PFN_D3DKMTEnumAdapters));
}
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/nf-d3dkmthk-d3dkmtcloseadapter
public static int /* NTSTATUS */ nD3DKMTCloseAdapter(long ptr /* D3DKMT_CLOSEADAPTER */) {
- Validate.isTrue(PFN_D3DKMTCloseAdapter != NULL);
- return JNI.callPI(ptr, PFN_D3DKMTCloseAdapter);
+ return JNI.callPI(ptr, checkPfn(PFN_D3DKMTCloseAdapter));
+ }
+
+ private static long checkPfn(long pfn) {
+ if (pfn == NULL) {
+ throw new NullPointerException("Function pointer not available");
+ }
+
+ return pfn;
}
}
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Kernel32.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Kernel32.java
similarity index 94%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Kernel32.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Kernel32.java
index 4b1c62ae3a..f8b6fef62a 100644
--- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Kernel32.java
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Kernel32.java
@@ -3,6 +3,7 @@
import org.jetbrains.annotations.Nullable;
import org.lwjgl.PointerBuffer;
import org.lwjgl.system.*;
+
import java.nio.ByteBuffer;
public class Kernel32 {
@@ -14,6 +15,7 @@ public class Kernel32 {
private static final int GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS = 1 << 2;
private static final long PFN_GetCommandLineW;
+ private static final long PFN_GetCommandLineA;
private static final long PFN_SetEnvironmentVariableW;
private static final long PFN_GetModuleHandleExW;
@@ -24,6 +26,7 @@ public class Kernel32 {
static {
PFN_GetCommandLineW = APIUtil.apiGetFunctionAddress(LIBRARY, "GetCommandLineW");
+ PFN_GetCommandLineA = APIUtil.apiGetFunctionAddress(LIBRARY, "GetCommandLineA");
PFN_SetEnvironmentVariableW = APIUtil.apiGetFunctionAddress(LIBRARY, "SetEnvironmentVariableW");
PFN_GetModuleHandleExW = APIUtil.apiGetFunctionAddress(LIBRARY, "GetModuleHandleExW");
PFN_GetLastError = APIUtil.apiGetFunctionAddress(LIBRARY, "GetLastError");
@@ -50,6 +53,10 @@ public static long getCommandLine() {
return JNI.callP(PFN_GetCommandLineW);
}
+ public static long getCommandLineA() {
+ return JNI.callP(PFN_GetCommandLineA);
+ }
+
public static long getModuleHandleByNames(String[] names) {
for (String name : names) {
var handle = getModuleHandleByName(name);
diff --git a/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Shell32.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Shell32.java
new file mode 100644
index 0000000000..5409b7fcea
--- /dev/null
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/Shell32.java
@@ -0,0 +1,57 @@
+package net.caffeinemc.mods.sodium.client.platform.windows.api;
+
+import net.caffeinemc.mods.sodium.client.platform.NativeWindowHandle;
+import org.jetbrains.annotations.Nullable;
+import org.lwjgl.system.JNI;
+import org.lwjgl.system.MemoryStack;
+import org.lwjgl.system.SharedLibrary;
+
+import java.util.Objects;
+
+import static org.lwjgl.system.APIUtil.apiCreateLibrary;
+import static org.lwjgl.system.APIUtil.apiGetFunctionAddressOptional;
+import static org.lwjgl.system.MemoryUtil.NULL;
+
+public class Shell32 {
+ private static final SharedLibrary LIBRARY = apiCreateLibrary("shell32");
+
+ private static final long PFN_ShellExecuteW = apiGetFunctionAddressOptional(LIBRARY, "ShellExecuteW");
+
+ public static void browseUrl(@Nullable NativeWindowHandle window, String url) {
+ Objects.requireNonNull(url, "URL parameter must be non-null");
+
+ try (var stack = MemoryStack.stackPush()) {
+ stack.nUTF16("open", true);
+ var lpOperation = stack.getPointerAddress();
+
+ stack.nUTF16(url, true);
+ var lpFile = stack.getPointerAddress();
+
+ nShellExecuteW(window != null ? window.getWin32Handle() : NULL,
+ lpOperation,
+ lpFile,
+ NULL,
+ NULL,
+ 0x1 /* SW_NORMAL */);
+ }
+ }
+
+ public static long nShellExecuteW(
+ /* HWND */ long hwnd,
+ /* LPCWSTR */ long lpOperation,
+ /* LPCWSTR */ long lpFile,
+ /* LPCWSTR */ long lpParameters,
+ /* LPCWSTR */ long lpDirectory,
+ /* INT */ int nShowCmd
+ ) {
+ return JNI.invokePPPPPP(hwnd, lpOperation, lpFile, lpParameters, lpDirectory, nShowCmd, checkPfn(PFN_ShellExecuteW));
+ }
+
+ private static long checkPfn(long pfn) {
+ if (pfn == NULL) {
+ throw new NullPointerException("Function pointer not available");
+ }
+
+ return pfn;
+ }
+}
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/User32.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/User32.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/User32.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/User32.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMT.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMT.java
similarity index 91%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMT.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMT.java
index 102fe183cf..28cb79b0ac 100644
--- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMT.java
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMT.java
@@ -5,7 +5,6 @@
import net.caffeinemc.mods.sodium.client.platform.windows.WindowsFileVersion;
import net.caffeinemc.mods.sodium.client.platform.windows.api.Gdi32;
import net.caffeinemc.mods.sodium.client.platform.windows.api.version.Version;
-import org.apache.commons.io.FilenameUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.system.MemoryStack;
@@ -13,7 +12,7 @@
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
-import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
@@ -47,7 +46,7 @@ public static List findGraphicsAdapters() {
}
}
- private static @NotNull ArrayList queryAdapters(@NotNull D3DKMTAdapterInfoStruct.Buffer adapterInfoBuffer) {
+ private static @NotNull ArrayList queryAdapters(D3DKMTAdapterInfoStruct.@NotNull Buffer adapterInfoBuffer) {
var results = new ArrayList();
for (int adapterIndex = adapterInfoBuffer.position(); adapterIndex < adapterInfoBuffer.limit(); adapterIndex++) {
@@ -64,7 +63,7 @@ public static List findGraphicsAdapters() {
return results;
}
- private static void freeAdapters(@NotNull D3DKMTAdapterInfoStruct.Buffer adapterInfoBuffer) {
+ private static void freeAdapters(D3DKMTAdapterInfoStruct.@NotNull Buffer adapterInfoBuffer) {
for (int adapterIndex = adapterInfoBuffer.position(); adapterIndex < adapterInfoBuffer.limit(); adapterIndex++) {
var adapterInfo = adapterInfoBuffer.get(adapterIndex);
apiCheckError("D3DKMTCloseAdapter",
@@ -72,7 +71,7 @@ private static void freeAdapters(@NotNull D3DKMTAdapterInfoStruct.Buffer adapter
}
}
- private static @Nullable D3DKMT.WDDMAdapterInfo getAdapterInfo(int adapter) {
+ private static D3DKMT.@Nullable WDDMAdapterInfo getAdapterInfo(int adapter) {
int adapterType = queryAdapterType(adapter);
if (!isSupportedAdapterType(adapterType)) {
@@ -81,8 +80,8 @@ private static void freeAdapters(@NotNull D3DKMTAdapterInfoStruct.Buffer adapter
String adapterName = queryFriendlyName(adapter);
- @Nullable String driverFileName = queryDriverFileName(adapter);
- @Nullable WindowsFileVersion driverVersion = null;
+ String driverFileName = queryDriverFileName(adapter);
+ WindowsFileVersion driverVersion = null;
GraphicsAdapterVendor driverVendor = GraphicsAdapterVendor.UNKNOWN;
@@ -201,8 +200,18 @@ public String toString() {
}
}
- // Returns (null) if input is (null).
- private static String getOpenGlIcdName(String path) {
- return FilenameUtils.removeExtension(FilenameUtils.getName(path));
+ private static String getOpenGlIcdName(@Nullable String filePath) {
+ if (filePath == null) {
+ return null;
+ }
+
+ var fileName = Paths.get(filePath)
+ .getFileName();
+
+ if (fileName == null) {
+ return null;
+ }
+
+ return fileName.toString();
}
}
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterInfoStruct.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterInfoStruct.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterInfoStruct.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterInfoStruct.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterRegistryInfoStruct.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterRegistryInfoStruct.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterRegistryInfoStruct.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTAdapterRegistryInfoStruct.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTEnumAdaptersStruct.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTEnumAdaptersStruct.java
similarity index 97%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTEnumAdaptersStruct.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTEnumAdaptersStruct.java
index de36d44164..ad07d37ea8 100644
--- a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTEnumAdaptersStruct.java
+++ b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTEnumAdaptersStruct.java
@@ -8,8 +8,6 @@
import java.nio.ByteBuffer;
-import static org.lwjgl.system.MemoryUtil.*;
-
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmthk/ns-d3dkmthk-_d3dkmt_enumadapters
// typedef struct _D3DKMT_ENUMADAPTERS {
// [in] ULONG NumAdapters;
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTOpenGLInfoStruct.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTOpenGLInfoStruct.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTOpenGLInfoStruct.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTOpenGLInfoStruct.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoStruct.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoStruct.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoStruct.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoStruct.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoType.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoType.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoType.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/d3dkmt/D3DKMTQueryAdapterInfoType.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallback.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallback.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallback.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallback.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallbackI.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallbackI.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallbackI.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxCallbackI.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxParamSw.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxParamSw.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxParamSw.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/msgbox/MsgBoxParamSw.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/LanguageCodePage.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/LanguageCodePage.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/LanguageCodePage.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/LanguageCodePage.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/QueryResult.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/QueryResult.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/QueryResult.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/QueryResult.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/Version.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/Version.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/Version.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/Version.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionFixedFileInfoStruct.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionFixedFileInfoStruct.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionFixedFileInfoStruct.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionFixedFileInfoStruct.java
diff --git a/common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionInfo.java b/common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionInfo.java
similarity index 100%
rename from common/src/workarounds/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionInfo.java
rename to common/src/boot/java/net/caffeinemc/mods/sodium/client/platform/windows/api/version/VersionInfo.java
diff --git a/common/src/workarounds/resources/META-INF/MANIFEST.MF b/common/src/boot/resources/META-INF/MANIFEST.MF
similarity index 100%
rename from common/src/workarounds/resources/META-INF/MANIFEST.MF
rename to common/src/boot/resources/META-INF/MANIFEST.MF
diff --git a/common/src/desktop/java/net/caffeinemc/mods/sodium/desktop/LaunchWarn.java b/common/src/desktop/java/net/caffeinemc/mods/sodium/desktop/LaunchWarn.java
index 577c762e52..71aea09e74 100644
--- a/common/src/desktop/java/net/caffeinemc/mods/sodium/desktop/LaunchWarn.java
+++ b/common/src/desktop/java/net/caffeinemc/mods/sodium/desktop/LaunchWarn.java
@@ -7,7 +7,7 @@
import java.io.IOException;
public class LaunchWarn {
- private static final String HELP_URL = "https://github.com/CaffeineMC/sodium/wiki/Installation";
+ private static final String HELP_URL = "https://link.caffeinemc.net/guides/sodium/installation";
private static final String RICH_MESSAGE =
"" +
@@ -91,7 +91,7 @@ private static void showRichGraphicalDialog(BrowseUrlHandler browseUrlHandler) {
}
private static void showFallbackGraphicalDialog() {
- // Fallback for Linux, etc users with no "default" browser
+ // Fallback for Linux, etc. users with no "default" browser
showDialogBox(FALLBACK_MESSAGE, WINDOW_TITLE, JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE, null, null);
}
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/SodiumClientMod.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/SodiumClientMod.java
index 9dba7b3641..efe539a697 100644
--- a/common/src/main/java/net/caffeinemc/mods/sodium/client/SodiumClientMod.java
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/SodiumClientMod.java
@@ -1,18 +1,18 @@
package net.caffeinemc.mods.sodium.client;
-import net.caffeinemc.mods.sodium.client.data.fingerprint.FingerprintMeasure;
-import net.caffeinemc.mods.sodium.client.data.fingerprint.HashedFingerprint;
-import net.caffeinemc.mods.sodium.client.gui.SodiumGameOptions;
import net.caffeinemc.mods.sodium.client.console.Console;
import net.caffeinemc.mods.sodium.client.console.message.MessageLevel;
-import net.minecraft.network.chat.Component;
+import net.caffeinemc.mods.sodium.client.data.fingerprint.FingerprintMeasure;
+import net.caffeinemc.mods.sodium.client.data.fingerprint.HashedFingerprint;
+import net.caffeinemc.mods.sodium.client.gui.SodiumOptions;
+import net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class SodiumClientMod {
- private static SodiumGameOptions CONFIG;
+ private static SodiumOptions OPTIONS;
private static final Logger LOGGER = LoggerFactory.getLogger("Sodium");
private static String MOD_VERSION;
@@ -20,7 +20,7 @@ public class SodiumClientMod {
public static void onInitialization(String version) {
MOD_VERSION = version;
- CONFIG = loadConfig();
+ OPTIONS = loadConfig();
try {
updateFingerprint();
@@ -29,12 +29,12 @@ public static void onInitialization(String version) {
}
}
- public static SodiumGameOptions options() {
- if (CONFIG == null) {
+ public static SodiumOptions options() {
+ if (OPTIONS == null) {
throw new IllegalStateException("Config not yet available");
}
- return CONFIG;
+ return OPTIONS;
}
public static Logger logger() {
@@ -45,16 +45,16 @@ public static Logger logger() {
return LOGGER;
}
- private static SodiumGameOptions loadConfig() {
+ private static SodiumOptions loadConfig() {
try {
- return SodiumGameOptions.loadFromDisk();
+ return SodiumOptions.loadFromDisk();
} catch (Exception e) {
LOGGER.error("Failed to load configuration file", e);
LOGGER.error("Using default configuration file in read-only mode");
Console.instance().logMessage(MessageLevel.SEVERE, "sodium.console.config_not_loaded", true, 12.5);
- var config = SodiumGameOptions.defaults();
+ var config = SodiumOptions.defaults();
config.setReadOnly();
return config;
@@ -62,10 +62,10 @@ private static SodiumGameOptions loadConfig() {
}
public static void restoreDefaultOptions() {
- CONFIG = SodiumGameOptions.defaults();
+ OPTIONS = SodiumOptions.defaults();
try {
- SodiumGameOptions.writeToDisk(CONFIG);
+ SodiumOptions.writeToDisk(OPTIONS);
} catch (IOException e) {
throw new RuntimeException("Failed to write config file", e);
}
@@ -97,14 +97,18 @@ private static void updateFingerprint() {
if (saved == null || !current.looselyMatches(saved)) {
HashedFingerprint.writeToDisk(current.hashed());
- CONFIG.notifications.hasSeenDonationPrompt = false;
- CONFIG.notifications.hasClearedDonationButton = false;
+ OPTIONS.notifications.hasSeenDonationPrompt = false;
+ OPTIONS.notifications.hasClearedDonationButton = false;
try {
- SodiumGameOptions.writeToDisk(CONFIG);
+ SodiumOptions.writeToDisk(OPTIONS);
} catch (IOException e) {
LOGGER.error("Failed to update config file", e);
}
}
}
+
+ public static boolean allowDebuggingOptions() {
+ return PlatformRuntimeInformation.getInstance().isDevelopmentEnvironment();
+ }
}
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/checks/ResourcePackScanner.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/checks/ResourcePackScanner.java
index 6f9ad8117c..b4ca154078 100644
--- a/common/src/main/java/net/caffeinemc/mods/sodium/client/checks/ResourcePackScanner.java
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/checks/ResourcePackScanner.java
@@ -2,8 +2,6 @@
import net.caffeinemc.mods.sodium.client.console.Console;
import net.caffeinemc.mods.sodium.client.console.message.MessageLevel;
-import net.minecraft.network.chat.Component;
-import net.minecraft.network.chat.MutableComponent;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.FilePackResources;
import net.minecraft.server.packs.PackResources;
@@ -38,7 +36,10 @@ public class ResourcePackScanner {
"rendertype_translucent.json",
"rendertype_tripwire.vsh",
"rendertype_tripwire.fsh",
- "rendertype_tripwire.json"
+ "rendertype_tripwire.json",
+ "rendertype_clouds.vsh",
+ "rendertype_clouds.fsh",
+ "rendertype_clouds.json"
);
private static final Set SHADER_INCLUDE_BLACKLIST = Set.of(
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/AnonymousOptionBinding.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/AnonymousOptionBinding.java
new file mode 100644
index 0000000000..e4f057cf6e
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/AnonymousOptionBinding.java
@@ -0,0 +1,26 @@
+package net.caffeinemc.mods.sodium.client.config;
+
+import net.caffeinemc.mods.sodium.api.config.option.OptionBinding;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class AnonymousOptionBinding implements OptionBinding {
+ private final Consumer save;
+ private final Supplier load;
+
+ public AnonymousOptionBinding(Consumer save, Supplier load) {
+ this.save = save;
+ this.load = load;
+ }
+
+ @Override
+ public void save(V value) {
+ this.save.accept(value);
+ }
+
+ @Override
+ public V load() {
+ return this.load.get();
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/ConfigManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/ConfigManager.java
new file mode 100644
index 0000000000..f3ba1e9a0f
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/ConfigManager.java
@@ -0,0 +1,133 @@
+package net.caffeinemc.mods.sodium.client.config;
+
+
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint;
+import net.caffeinemc.mods.sodium.api.config.structure.ConfigBuilder;
+import net.caffeinemc.mods.sodium.client.SodiumClientMod;
+import net.caffeinemc.mods.sodium.client.config.builder.ConfigBuilderImpl;
+import net.caffeinemc.mods.sodium.client.config.structure.Config;
+import net.caffeinemc.mods.sodium.client.config.structure.ModOptions;
+import net.minecraft.CrashReport;
+import net.minecraft.client.Minecraft;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class ConfigManager {
+ public static final String CONFIG_ENTRY_POINT_KEY = "sodium:config_api_user";
+
+ private record ConfigUser(
+ Supplier configEntrypoint,
+ String modId) {
+ }
+ public record ModMetadata(String modName, String modVersion) {
+ }
+
+ private static final Collection configUsers = new ArrayList<>();
+
+ public static Config CONFIG;
+ private static Function modInfoFunction;
+
+ public static void setModInfoFunction(Function modInfoFunction) {
+ ConfigManager.modInfoFunction = modInfoFunction;
+ }
+
+ public static void registerConfigEntryPoint(String className, String modId) {
+ Class> entryPointClass;
+ try {
+ entryPointClass = Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ SodiumClientMod.logger().warn("Mod '{}' provided a custom config integration but the class is missing: {}", modId, className);
+ return;
+ }
+ if (!ConfigEntryPoint.class.isAssignableFrom(entryPointClass)) {
+ SodiumClientMod.logger().warn("Mod '{}' provided a custom config integration but the class is of the wrong type: {}", modId, entryPointClass);
+ return;
+ }
+
+ registerConfigEntryPoint(() -> {
+ try {
+ Constructor> constructor = entryPointClass.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ return (ConfigEntryPoint) constructor.newInstance();
+ } catch (ReflectiveOperationException e) {
+ SodiumClientMod.logger().warn("Mod '{}' provided a custom config integration but the class could not be constructed: {}", modId, entryPointClass);
+ }
+ return null;
+ }, modId);
+ }
+
+ public static void registerConfigEntryPoint(Supplier entryPoint, String modId) {
+ configUsers.add(new ConfigUser(entryPoint, modId));
+ }
+
+ public static void registerConfigsEarly() {
+ registerConfigs(ConfigEntryPoint::registerConfigEarly);
+ }
+
+ public static void registerConfigsLate() {
+ registerConfigs(ConfigEntryPoint::registerConfigLate);
+ }
+
+ private static void registerConfigs(BiConsumer registerMethod) {
+ var configIds = new ObjectOpenHashSet<>();
+ ModOptions sodiumModOptions = null;
+ var modConfigs = new ObjectArrayList();
+
+ for (ConfigUser configUser : configUsers) {
+ var entryPoint = configUser.configEntrypoint.get();
+ if (entryPoint == null) {
+ continue;
+ }
+
+ var builder = new ConfigBuilderImpl(modInfoFunction, configUser.modId);
+ Collection builtConfigs;
+ try {
+ registerMethod.accept(entryPoint, builder);
+ builtConfigs = builder.build();
+
+ for (var modConfig : builtConfigs) {
+ var configId = modConfig.configId();
+ if (configIds.contains(configId)) {
+ throw new IllegalArgumentException("Mod '" + configUser.modId + "' provided a duplicate mod id: " + configId);
+ }
+
+ configIds.add(configId);
+
+ if (configId.equals("sodium")) {
+ sodiumModOptions = modConfig;
+ } else {
+ modConfigs.add(modConfig);
+ }
+ }
+ } catch (Exception e) {
+ crashWithMessage("Mod '" + configUser.modId + "' failed while registering config options.", e);
+ return;
+ }
+ }
+
+ modConfigs.sort(Comparator.comparing(ModOptions::name));
+
+ if (sodiumModOptions == null) {
+ throw new RuntimeException("Sodium mod config not found");
+ }
+ modConfigs.add(0, sodiumModOptions);
+
+ try {
+ CONFIG = new Config(modConfigs);
+ } catch (Exception e) {
+ crashWithMessage("Failed to build config options", e);
+ }
+ }
+
+ private static void crashWithMessage(String message, Exception e) {
+ Minecraft.crash(null, Minecraft.getInstance().gameDirectory, new CrashReport(message, e));
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/BooleanOptionBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/BooleanOptionBuilderImpl.java
new file mode 100644
index 0000000000..62f656902a
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/BooleanOptionBuilderImpl.java
@@ -0,0 +1,135 @@
+package net.caffeinemc.mods.sodium.client.config.builder;
+
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.caffeinemc.mods.sodium.api.config.StorageEventHandler;
+import net.caffeinemc.mods.sodium.api.config.option.OptionBinding;
+import net.caffeinemc.mods.sodium.api.config.option.OptionFlag;
+import net.caffeinemc.mods.sodium.api.config.option.OptionImpact;
+import net.caffeinemc.mods.sodium.api.config.structure.BooleanOptionBuilder;
+import net.caffeinemc.mods.sodium.client.config.structure.BooleanOption;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+class BooleanOptionBuilderImpl extends StatefulOptionBuilderImpl implements BooleanOptionBuilder {
+ BooleanOptionBuilderImpl(ResourceLocation id) {
+ super(id);
+ }
+
+ @Override
+ BooleanOption build() {
+ this.prepareBuild();
+
+ return new BooleanOption(
+ this.id,
+ this.getDependencies(),
+ this.getName(),
+ this.getEnabled(),
+ this.getStorage(),
+ this.getTooltipProvider(),
+ this.getImpact(),
+ this.getFlags(),
+ this.getDefaultValue(),
+ this.getControlHiddenWhenDisabled(),
+ this.getBinding(),
+ this.getApplyHook());
+ }
+
+ @Override
+ Class getOptionClass() {
+ return BooleanOption.class;
+ }
+
+ @Override
+ public BooleanOptionBuilder setName(Component name) {
+ super.setName(name);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setStorageHandler(StorageEventHandler storage) {
+ super.setStorageHandler(storage);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setTooltip(Component tooltip) {
+ super.setTooltip(tooltip);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setTooltip(Function tooltip) {
+ super.setTooltip(tooltip);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setImpact(OptionImpact impact) {
+ super.setImpact(impact);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setFlags(OptionFlag... flags) {
+ super.setFlags(flags);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setFlags(ResourceLocation... flags) {
+ super.setFlags(flags);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setDefaultValue(Boolean value) {
+ super.setDefaultValue(value);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies) {
+ super.setDefaultProvider(provider, dependencies);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setEnabled(boolean available) {
+ super.setEnabled(available);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies) {
+ super.setEnabledProvider(provider, dependencies);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setControlHiddenWhenDisabled(boolean hidden) {
+ super.setControlHiddenWhenDisabled(hidden);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setBinding(Consumer save, Supplier load) {
+ super.setBinding(save, load);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setBinding(OptionBinding binding) {
+ super.setBinding(binding);
+ return this;
+ }
+
+ @Override
+ public BooleanOptionBuilder setApplyHook(Consumer hook) {
+ super.setApplyHook(hook);
+ return this;
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ColorThemeBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ColorThemeBuilderImpl.java
new file mode 100644
index 0000000000..e0bedd91fe
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ColorThemeBuilderImpl.java
@@ -0,0 +1,44 @@
+package net.caffeinemc.mods.sodium.client.config.builder;
+
+import net.caffeinemc.mods.sodium.api.config.structure.ColorThemeBuilder;
+import net.caffeinemc.mods.sodium.client.gui.ColorTheme;
+import net.caffeinemc.mods.sodium.client.gui.Colors;
+
+public class ColorThemeBuilderImpl implements ColorThemeBuilder {
+ private static final float MIN_THEME_SATURATION = 0.2f;
+ private static final float MIN_THEME_BRIGHTNESS = 0.55f;
+
+ private int baseTheme;
+ private int themeHighlight;
+ private int themeDisabled;
+
+ ColorTheme build() {
+ if (this.baseTheme == 0) {
+ throw new IllegalStateException("Base theme must be set");
+ }
+
+ this.baseTheme = Colors.constrainColorHSV(this.baseTheme, MIN_THEME_SATURATION, MIN_THEME_BRIGHTNESS);
+
+ if (this.themeHighlight == 0 || this.themeDisabled == 0) {
+ return new ColorTheme(this.baseTheme);
+ } else {
+ this.themeHighlight = Colors.constrainColorHSV(this.themeHighlight, MIN_THEME_SATURATION, MIN_THEME_BRIGHTNESS);
+ this.themeDisabled = Colors.constrainColorHSV(this.themeDisabled, MIN_THEME_SATURATION, 0);
+ return new ColorTheme(this.baseTheme, this.themeHighlight, this.themeDisabled);
+ }
+ }
+
+ @Override
+ public ColorThemeBuilder setBaseThemeRGB(int theme) {
+ this.baseTheme = theme | 0xFF000000;
+ return this;
+ }
+
+ @Override
+ public ColorThemeBuilder setFullThemeRGB(int theme, int themeHighlight, int themeDisabled) {
+ this.baseTheme = theme | 0xFF000000;
+ this.themeHighlight = themeHighlight | 0xFF000000;
+ this.themeDisabled = themeDisabled | 0xFF000000;
+ return this;
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ConfigBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ConfigBuilderImpl.java
new file mode 100644
index 0000000000..b9999ed75b
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ConfigBuilderImpl.java
@@ -0,0 +1,89 @@
+package net.caffeinemc.mods.sodium.client.config.builder;
+
+import net.caffeinemc.mods.sodium.api.config.structure.*;
+import net.caffeinemc.mods.sodium.client.config.ConfigManager;
+import net.caffeinemc.mods.sodium.client.config.structure.ModOptions;
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+
+public class ConfigBuilderImpl implements ConfigBuilder {
+ private final List pendingModConfigBuilders = new ArrayList<>(1);
+
+ private final Function modInfoFunction;
+ private final String defaultConfigId;
+
+ public ConfigBuilderImpl(Function modInfoFunction, String defaultConfigId) {
+ this.modInfoFunction = modInfoFunction;
+ this.defaultConfigId = defaultConfigId;
+ }
+
+ public Collection build() {
+ var configs = new ArrayList(this.pendingModConfigBuilders.size());
+ for (var builder : this.pendingModConfigBuilders) {
+ configs.add(builder.build());
+ }
+ return configs;
+ }
+
+ @Override
+ public ModOptionsBuilder registerModOptions(String configId, String name, String version) {
+ var builder = new ModOptionsBuilderImpl(configId, name, version);
+ this.pendingModConfigBuilders.add(builder);
+ return builder;
+ }
+
+ @Override
+ public ModOptionsBuilder registerModOptions(String configId) {
+ var metadata = this.modInfoFunction.apply(configId);
+ return this.registerModOptions(configId, metadata.modName(), metadata.modVersion());
+ }
+
+ @Override
+ public ModOptionsBuilder registerOwnModOptions() {
+ return this.registerModOptions(this.defaultConfigId);
+ }
+
+ @Override
+ public ColorThemeBuilder createColorTheme() {
+ return new ColorThemeBuilderImpl();
+ }
+
+ @Override
+ public OptionPageBuilder createOptionPage() {
+ return new OptionPageBuilderImpl();
+ }
+
+ @Override
+ public ExternalPageBuilder createExternalPage() {
+ return new ExternalPageBuilderImpl();
+ }
+
+ @Override
+ public OptionGroupBuilder createOptionGroup() {
+ return new OptionGroupBuilderImpl();
+ }
+
+ @Override
+ public BooleanOptionBuilder createBooleanOption(ResourceLocation id) {
+ return new BooleanOptionBuilderImpl(id);
+ }
+
+ @Override
+ public IntegerOptionBuilder createIntegerOption(ResourceLocation id) {
+ return new IntegerOptionBuilderImpl(id);
+ }
+
+ @Override
+ public > EnumOptionBuilder createEnumOption(ResourceLocation id, Class enumClass) {
+ return new EnumOptionBuilderImpl<>(id, enumClass);
+ }
+
+ @Override
+ public ExternalButtonOptionBuilder createExternalButtonOption(ResourceLocation id) {
+ return new ExternalButtonOptionBuilderImpl(id);
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/EnumOptionBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/EnumOptionBuilderImpl.java
new file mode 100644
index 0000000000..0b20c553f0
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/EnumOptionBuilderImpl.java
@@ -0,0 +1,204 @@
+package net.caffeinemc.mods.sodium.client.config.builder;
+
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.caffeinemc.mods.sodium.api.config.StorageEventHandler;
+import net.caffeinemc.mods.sodium.api.config.option.OptionBinding;
+import net.caffeinemc.mods.sodium.api.config.option.OptionFlag;
+import net.caffeinemc.mods.sodium.api.config.option.OptionImpact;
+import net.caffeinemc.mods.sodium.api.config.structure.EnumOptionBuilder;
+import net.caffeinemc.mods.sodium.client.config.structure.EnumOption;
+import net.caffeinemc.mods.sodium.client.config.value.ConstantValue;
+import net.caffeinemc.mods.sodium.client.config.value.DependentValue;
+import net.caffeinemc.mods.sodium.client.config.value.DynamicValue;
+import net.caffeinemc.mods.sodium.client.gui.options.TextProvider;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import org.apache.commons.lang3.Validate;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+class EnumOptionBuilderImpl> extends StatefulOptionBuilderImpl, E> implements EnumOptionBuilder {
+ private final Class enumClass;
+
+ private DependentValue> allowedValues;
+ private Function elementNameProvider;
+
+ EnumOptionBuilderImpl(ResourceLocation id, Class enumClass) {
+ super(id);
+ this.enumClass = enumClass;
+ }
+
+ @Override
+ void validateData() {
+ super.validateData();
+
+ Validate.notNull(this.getElementNameProvider(), "Element name provider must be set or enum class must implement TextProvider");
+ }
+
+ @Override
+ EnumOption build() {
+ if (this.getAllowedValues() == null) {
+ this.allowedValues = new ConstantValue<>(Set.of(this.enumClass.getEnumConstants()));
+ }
+
+ if (this.getElementNameProvider() == null && TextProvider.class.isAssignableFrom(this.enumClass)) {
+ this.elementNameProvider = e -> ((TextProvider) e).getLocalizedName();
+ }
+
+ this.prepareBuild();
+
+ return new EnumOption<>(this.id,
+ this.getDependencies(),
+ this.getName(),
+ this.getEnabled(),
+ this.getStorage(),
+ this.getTooltipProvider(), this.getImpact(),
+ this.getFlags(),
+ this.getDefaultValue(),
+ this.getControlHiddenWhenDisabled(),
+ this.getBinding(),
+ this.getApplyHook(),
+ this.getEnumClass(),
+ this.getAllowedValues(),
+ this.getElementNameProvider());
+ }
+
+ @Override
+ Class> getOptionClass() {
+ @SuppressWarnings("unchecked")
+ Class> clazz = (Class>) (Class>) EnumOption.class;
+ return clazz;
+ }
+
+ @Override
+ Collection getDependencies() {
+ var deps = super.getDependencies();
+ deps.addAll(this.getAllowedValues().getDependencies());
+ return deps;
+ }
+
+ Class getEnumClass() {
+ return this.getFirstNotNull(this.enumClass, EnumOption::getEnumClass);
+ }
+
+ DependentValue> getAllowedValues() {
+ return this.getFirstNotNull(this.allowedValues, EnumOption::getAllowedValues);
+ }
+
+ Function getElementNameProvider() {
+ return this.getFirstNotNull(this.elementNameProvider, EnumOption::getElementNameProvider);
+ }
+
+ @Override
+ public EnumOptionBuilder setName(Component name) {
+ super.setName(name);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setElementNameProvider(Function provider) {
+ this.elementNameProvider = provider;
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setEnabled(boolean available) {
+ super.setEnabled(available);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies) {
+ super.setEnabledProvider(provider, dependencies);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setStorageHandler(StorageEventHandler storage) {
+ super.setStorageHandler(storage);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setTooltip(Component tooltip) {
+ super.setTooltip(tooltip);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setTooltip(Function tooltip) {
+ super.setTooltip(tooltip);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setImpact(OptionImpact impact) {
+ super.setImpact(impact);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setFlags(OptionFlag... flags) {
+ super.setFlags(flags);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setFlags(ResourceLocation... flags) {
+ super.setFlags(flags);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setDefaultValue(E value) {
+ super.setDefaultValue(value);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies) {
+ super.setDefaultProvider(provider, dependencies);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setControlHiddenWhenDisabled(boolean hidden) {
+ super.setControlHiddenWhenDisabled(hidden);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setBinding(Consumer save, Supplier load) {
+ super.setBinding(save, load);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setBinding(OptionBinding binding) {
+ super.setBinding(binding);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setApplyHook(Consumer hook) {
+ super.setApplyHook(hook);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setAllowedValues(Set allowedValues) {
+ this.allowedValues = new ConstantValue<>(allowedValues);
+ return this;
+ }
+
+ @Override
+ public EnumOptionBuilder setAllowedValuesProvider(Function> provider, ResourceLocation... dependencies) {
+ this.allowedValues = new DynamicValue<>(provider, dependencies);
+ return this;
+ }
+
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ExternalButtonOptionBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ExternalButtonOptionBuilderImpl.java
new file mode 100644
index 0000000000..ef74b272ec
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ExternalButtonOptionBuilderImpl.java
@@ -0,0 +1,73 @@
+package net.caffeinemc.mods.sodium.client.config.builder;
+
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.caffeinemc.mods.sodium.api.config.structure.ExternalButtonOptionBuilder;
+import net.caffeinemc.mods.sodium.client.config.structure.ExternalButtonOption;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import org.apache.commons.lang3.Validate;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+class ExternalButtonOptionBuilderImpl extends StaticOptionBuilderImpl implements ExternalButtonOptionBuilder {
+ private Consumer currentScreenConsumer;
+
+ ExternalButtonOptionBuilderImpl(ResourceLocation id) {
+ super(id);
+ }
+
+ @Override
+ void validateData() {
+ super.validateData();
+
+ Validate.notNull(this.getCurrentScreenConsumer(), "Screen provider must be set");
+ }
+
+ @Override
+ ExternalButtonOption build() {
+ this.prepareBuild();
+
+ return new ExternalButtonOption(this.id, this.getDependencies(), this.getName(), this.getEnabled(), this.getTooltip(), this.getCurrentScreenConsumer());
+ }
+
+ @Override
+ Class getOptionClass() {
+ return ExternalButtonOption.class;
+ }
+
+ Consumer getCurrentScreenConsumer() {
+ return this.getFirstNotNull(this.currentScreenConsumer, ExternalButtonOption::getCurrentScreenConsumer);
+ }
+
+ @Override
+ public ExternalButtonOptionBuilder setName(Component name) {
+ super.setName(name);
+ return this;
+ }
+
+ @Override
+ public ExternalButtonOptionBuilder setEnabled(boolean available) {
+ super.setEnabled(available);
+ return this;
+ }
+
+ @Override
+ public ExternalButtonOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies) {
+ super.setEnabledProvider(provider, dependencies);
+ return this;
+ }
+
+ @Override
+ public ExternalButtonOptionBuilder setTooltip(Component tooltip) {
+ super.setTooltip(tooltip);
+ return this;
+ }
+
+ @Override
+ public ExternalButtonOptionBuilder setScreenConsumer(Consumer currentScreenConsumer) {
+ this.currentScreenConsumer = currentScreenConsumer;
+ return this;
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ExternalPageBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ExternalPageBuilderImpl.java
new file mode 100644
index 0000000000..5c05fd5497
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ExternalPageBuilderImpl.java
@@ -0,0 +1,38 @@
+package net.caffeinemc.mods.sodium.client.config.builder;
+
+import net.caffeinemc.mods.sodium.api.config.structure.ExternalPageBuilder;
+import net.caffeinemc.mods.sodium.client.config.structure.ExternalPage;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.Component;
+import org.apache.commons.lang3.Validate;
+
+import java.util.function.Consumer;
+
+public class ExternalPageBuilderImpl extends PageBuilderImpl implements ExternalPageBuilder {
+ private Consumer currentScreenConsumer;
+
+ @Override
+ void prepareBuild() {
+ super.prepareBuild();
+
+ Validate.notNull(this.currentScreenConsumer, "Screen consumer must not be null");
+ }
+
+ @Override
+ ExternalPage build() {
+ this.prepareBuild();
+ return new ExternalPage(this.name, this.currentScreenConsumer);
+ }
+
+ @Override
+ public ExternalPageBuilder setScreenConsumer(Consumer currentScreenConsumer) {
+ this.currentScreenConsumer = currentScreenConsumer;
+ return this;
+ }
+
+ @Override
+ public ExternalPageBuilderImpl setName(Component name) {
+ super.setName(name);
+ return this;
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/IntegerOptionBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/IntegerOptionBuilderImpl.java
new file mode 100644
index 0000000000..f2c2873836
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/IntegerOptionBuilderImpl.java
@@ -0,0 +1,201 @@
+package net.caffeinemc.mods.sodium.client.config.builder;
+
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.caffeinemc.mods.sodium.api.config.StorageEventHandler;
+import net.caffeinemc.mods.sodium.api.config.option.*;
+import net.caffeinemc.mods.sodium.api.config.structure.IntegerOptionBuilder;
+import net.caffeinemc.mods.sodium.client.config.structure.IntegerOption;
+import net.caffeinemc.mods.sodium.client.config.value.ConstantValue;
+import net.caffeinemc.mods.sodium.client.config.value.DependentValue;
+import net.caffeinemc.mods.sodium.client.config.value.DynamicValue;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import org.apache.commons.lang3.Validate;
+
+import java.util.Collection;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+class IntegerOptionBuilderImpl extends StatefulOptionBuilderImpl implements IntegerOptionBuilder {
+ private DependentValue extends SteppedValidator> validatorProvider;
+ private ControlValueFormatter valueFormatter;
+
+ IntegerOptionBuilderImpl(ResourceLocation id) {
+ super(id);
+ }
+
+ @Override
+ void validateData() {
+ super.validateData();
+
+ Validate.notNull(this.getValidatorProvider(), "Validator provider must be set");
+ Validate.notNull(this.getValueFormatter(), "Value formatter must be set");
+ }
+
+ @Override
+ IntegerOption build() {
+ this.prepareBuild();
+
+ return new IntegerOption(
+ this.id,
+ this.getDependencies(),
+ this.getName(),
+ this.getEnabled(),
+ this.getStorage(),
+ this.getTooltipProvider(),
+ this.getImpact(),
+ this.getFlags(),
+ this.getDefaultValue(),
+ this.getControlHiddenWhenDisabled(),
+ this.getBinding(),
+ this.getApplyHook(),
+ this.getValidatorProvider(),
+ this.getValueFormatter());
+ }
+
+ @Override
+ Collection getDependencies() {
+ var deps = super.getDependencies();
+ deps.addAll(this.getValidatorProvider().getDependencies());
+ return deps;
+ }
+
+ @Override
+ Class getOptionClass() {
+ return IntegerOption.class;
+ }
+
+ DependentValue extends SteppedValidator> getValidatorProvider() {
+ return this.getFirstNotNull(this.validatorProvider, IntegerOption::getValidatorProvider);
+ }
+
+ ControlValueFormatter getValueFormatter() {
+ return this.getFirstNotNull(this.valueFormatter, IntegerOption::getValueFormatter);
+ }
+
+ @Override
+ public IntegerOptionBuilder setName(Component name) {
+ super.setName(name);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setEnabled(boolean available) {
+ super.setEnabled(available);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies) {
+ super.setEnabledProvider(provider, dependencies);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setStorageHandler(StorageEventHandler storage) {
+ super.setStorageHandler(storage);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setTooltip(Component tooltip) {
+ super.setTooltip(tooltip);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setTooltip(Function tooltip) {
+ super.setTooltip(tooltip);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setImpact(OptionImpact impact) {
+ super.setImpact(impact);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setFlags(OptionFlag... flags) {
+ super.setFlags(flags);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setFlags(ResourceLocation... flags) {
+ super.setFlags(flags);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setDefaultValue(Integer value) {
+ super.setDefaultValue(value);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setDefaultProvider(Function provider, ResourceLocation... dependencies) {
+ super.setDefaultProvider(provider, dependencies);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setControlHiddenWhenDisabled(boolean hidden) {
+ super.setControlHiddenWhenDisabled(hidden);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setBinding(Consumer save, Supplier load) {
+ super.setBinding(save, load);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setBinding(OptionBinding binding) {
+ super.setBinding(binding);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setApplyHook(Consumer hook) {
+ super.setApplyHook(hook);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setRange(int min, int max, int step) {
+ return this.setRange(new Range(min, max, step));
+ }
+
+ @Override
+ public IntegerOptionBuilder setRange(Range range) {
+ this.validatorProvider = new ConstantValue<>(range);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setRangeProvider(Function provider, ResourceLocation... dependencies) {
+ this.validatorProvider = new DynamicValue<>(provider, dependencies);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setValidator(SteppedValidator validator) {
+ this.validatorProvider = new ConstantValue<>(validator);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setValidatorProvider(Function provider, ResourceLocation... dependencies) {
+ this.validatorProvider = new DynamicValue<>(provider, dependencies);
+ return this;
+ }
+
+ @Override
+ public IntegerOptionBuilder setValueFormatter(ControlValueFormatter formatter) {
+ this.valueFormatter = formatter;
+ return this;
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ModOptionsBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ModOptionsBuilderImpl.java
new file mode 100644
index 0000000000..c5a35cff2c
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/ModOptionsBuilderImpl.java
@@ -0,0 +1,135 @@
+package net.caffeinemc.mods.sodium.client.config.builder;
+
+import com.google.common.collect.ImmutableList;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.caffeinemc.mods.sodium.api.config.option.FlagHook;
+import net.caffeinemc.mods.sodium.api.config.structure.ColorThemeBuilder;
+import net.caffeinemc.mods.sodium.api.config.structure.ModOptionsBuilder;
+import net.caffeinemc.mods.sodium.api.config.structure.OptionBuilder;
+import net.caffeinemc.mods.sodium.api.config.structure.PageBuilder;
+import net.caffeinemc.mods.sodium.client.config.structure.*;
+import net.caffeinemc.mods.sodium.client.gui.ColorTheme;
+import net.minecraft.resources.ResourceLocation;
+import org.apache.commons.lang3.Validate;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+class ModOptionsBuilderImpl implements ModOptionsBuilder {
+ private final String configId;
+ private String name;
+ private String version;
+ private ColorTheme theme;
+ private ResourceLocation icon;
+ private boolean iconMonochrome = true;
+ private final List pages = new ArrayList<>();
+ private List optionOverrides;
+ private List optionOverlays;
+ private Collection flagHooks;
+
+ ModOptionsBuilderImpl(String configId, String name, String version) {
+ this.configId = configId;
+ this.name = name;
+ this.version = version;
+ }
+
+ ModOptions build() {
+ Validate.notEmpty(this.name, "Name must not be empty");
+ Validate.notEmpty(this.version, "Version must not be empty");
+
+ var overrides = this.optionOverrides == null ? List.of() : this.optionOverrides;
+ var overlays = this.optionOverlays == null ? List.of() : this.optionOverlays;
+
+ if (this.pages.isEmpty() && overrides.isEmpty() && overlays.isEmpty()) {
+ throw new IllegalStateException("At least one page, option override, or option overlay must be added");
+ }
+
+ // if no theme is specified, pick one pseudo-randomly based on the configId
+ if (this.theme == null) {
+ this.theme = ColorTheme.PRESETS[Math.abs(this.configId.hashCode()) % ColorTheme.PRESETS.length];
+ }
+
+ return new ModOptions(this.configId, this.name, this.version, this.theme, this.icon, this.iconMonochrome, ImmutableList.copyOf(this.pages), overrides, overlays, this.flagHooks);
+ }
+
+ @Override
+ public ModOptionsBuilder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ @Override
+ public ModOptionsBuilder setVersion(String version) {
+ this.version = version;
+ return this;
+ }
+
+ @Override
+ public ModOptionsBuilder formatVersion(Function versionFormatter) {
+ this.version = versionFormatter.apply(this.version);
+ return this;
+ }
+
+ @Override
+ public ModOptionsBuilder setColorTheme(ColorThemeBuilder theme) {
+ this.theme = ((ColorThemeBuilderImpl) theme).build();
+ return this;
+ }
+
+ @Override
+ public ModOptionsBuilder setIcon(ResourceLocation texture) {
+ this.icon = texture;
+ return this;
+ }
+
+ @Override
+ public ModOptionsBuilder setNonTintedIcon(ResourceLocation texture) {
+ this.icon = texture;
+ this.iconMonochrome = false;
+ return this;
+ }
+
+ @Override
+ public ModOptionsBuilder addPage(PageBuilder builder) {
+ this.pages.add(((PageBuilderImpl) builder).build());
+ return this;
+ }
+
+ @Override
+ public ModOptionsBuilder registerOptionReplacement(ResourceLocation target, OptionBuilder replacement) {
+ var override = new OptionOverride(target, this.configId, ((OptionBuilderImpl>) replacement).build());
+ if (this.optionOverrides == null) {
+ this.optionOverrides = new ArrayList<>();
+ }
+ this.optionOverrides.add(override);
+ return this;
+ }
+
+ @Override
+ public ModOptionsBuilder registerOptionOverlay(ResourceLocation target, OptionBuilder overlay) {
+ var optionOverlay = new OptionOverlay(target, this.configId, ((OptionBuilderImpl>) overlay));
+ if (this.optionOverlays == null) {
+ this.optionOverlays = new ArrayList<>();
+ }
+ this.optionOverlays.add(optionOverlay);
+ return this;
+ }
+
+ @Override
+ public ModOptionsBuilder registerFlagHook(BiConsumer, ConfigState> hook, ResourceLocation... triggers) {
+ return this.registerFlagHook(new FlagHookImpl(hook, List.of(triggers)));
+ }
+
+ @Override
+ public ModOptionsBuilder registerFlagHook(FlagHook hook) {
+ if (this.flagHooks == null) {
+ this.flagHooks = new ObjectArrayList<>();
+ }
+ this.flagHooks.add(hook);
+ return this;
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/OptionBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/OptionBuilderImpl.java
new file mode 100644
index 0000000000..9a63823704
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/OptionBuilderImpl.java
@@ -0,0 +1,101 @@
+package net.caffeinemc.mods.sodium.client.config.builder;
+
+import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
+import net.caffeinemc.mods.sodium.api.config.ConfigState;
+import net.caffeinemc.mods.sodium.api.config.structure.OptionBuilder;
+import net.caffeinemc.mods.sodium.client.config.structure.Option;
+import net.caffeinemc.mods.sodium.client.config.value.ConstantValue;
+import net.caffeinemc.mods.sodium.client.config.value.DependentValue;
+import net.caffeinemc.mods.sodium.client.config.value.DynamicValue;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import org.apache.commons.lang3.Validate;
+
+import java.util.Collection;
+import java.util.function.Function;
+
+public abstract class OptionBuilderImpl implements OptionBuilder {
+ final ResourceLocation id;
+
+ private O baseOption;
+
+ private Component name;
+ private DependentValue enabled;
+
+ OptionBuilderImpl(ResourceLocation id) {
+ this.id = id;
+ }
+
+ abstract O build();
+
+ abstract Class getOptionClass();
+
+ public O buildWithBaseOption(Option baseOption) {
+ Validate.isTrue(this.getOptionClass().isInstance(baseOption), "Base option must be of type %s", this.getOptionClass().getSimpleName());
+
+ @SuppressWarnings("unchecked")
+ O castedBaseOption = (O) baseOption;
+ this.baseOption = castedBaseOption;
+
+ return this.build();
+ }
+
+ void validateData() {
+ Validate.notNull(this.getName(), "Name must be set");
+ Validate.notBlank(this.getName().getString(), "Name must not be blank");
+ }
+
+ void prepareBuild() {
+ this.validateData();
+
+ if (this.getEnabled() == null) {
+ this.enabled = new ConstantValue<>(true);
+ }
+ }
+
+ Collection getDependencies() {
+ var dependencies = new ObjectLinkedOpenHashSet();
+ dependencies.addAll(this.getEnabled().getDependencies());
+ return dependencies;
+ }
+
+ public V getFirstNotNull(V overlayValue, Function extractor) {
+ if (overlayValue != null) {
+ return overlayValue;
+ } else if (this.baseOption != null) {
+ return extractor.apply(this.baseOption);
+ } else {
+ return null;
+ }
+ }
+
+ Component getName() {
+ return this.getFirstNotNull(this.name, Option::getName);
+ }
+
+ DependentValue getEnabled() {
+ return this.getFirstNotNull(this.enabled, Option::getEnabled);
+ }
+
+ @Override
+ public OptionBuilder setName(Component name) {
+ Validate.notNull(name, "Argument must not be null");
+
+ this.name = name;
+ return this;
+ }
+
+ @Override
+ public OptionBuilder setEnabled(boolean available) {
+ this.enabled = new ConstantValue<>(available);
+ return this;
+ }
+
+ @Override
+ public OptionBuilder setEnabledProvider(Function provider, ResourceLocation... dependencies) {
+ Validate.notNull(provider, "Argument must not be null");
+
+ this.enabled = new DynamicValue<>(provider, dependencies);
+ return this;
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/OptionGroupBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/OptionGroupBuilderImpl.java
new file mode 100644
index 0000000000..c07685ee69
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/builder/OptionGroupBuilderImpl.java
@@ -0,0 +1,34 @@
+package net.caffeinemc.mods.sodium.client.config.builder;
+
+import net.caffeinemc.mods.sodium.api.config.structure.OptionBuilder;
+import net.caffeinemc.mods.sodium.api.config.structure.OptionGroupBuilder;
+import net.caffeinemc.mods.sodium.client.config.structure.Option;
+import net.caffeinemc.mods.sodium.client.config.structure.OptionGroup;
+import net.minecraft.network.chat.Component;
+import org.apache.commons.lang3.Validate;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class OptionGroupBuilderImpl implements OptionGroupBuilder {
+ private Component name;
+ private final List