From f3169469ed5b34f4932122aa12659da8965c78a8 Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Sun, 16 Nov 2025 22:55:43 -0800 Subject: [PATCH 01/14] Overhaul Mod Menu This makes the code readable, updates dependencies, uses JSpecify for nullability annotations, and fixes scrolling. --- .gitattributes | 5 + .github/workflows/build.yml | 20 +- build.gradle | 169 --- build.gradle.kts | 192 +++ gradle.properties | 56 +- gradle/wrapper/gradle-wrapper.jar | Bin 52818 -> 45457 bytes gradle/wrapper/gradle-wrapper.properties | 8 +- gradlew | 305 ++-- gradlew.bat | 79 +- jitpack.yml | 7 +- settings.gradle | 25 - settings.gradle.kts | 20 + .../io/github/prospector/modmenu/ModMenu.java | 189 +-- .../prospector/modmenu/api/ModMenuApi.java | 60 +- .../modmenu/config/ModMenuConfig.java | 83 +- .../modmenu/config/ModMenuConfigManager.java | 104 +- .../gui/AlwaysSelectedEntryListWidget.java | 43 +- .../modmenu/gui/DescriptionListWidget.java | 113 +- .../modmenu/gui/EntryListWidget.java | 1181 ++++++++------- .../prospector/modmenu/gui/ModListEntry.java | 268 ++-- .../prospector/modmenu/gui/ModListScreen.java | 1113 ++++++++------ .../prospector/modmenu/gui/ModListWidget.java | 587 ++++---- .../modmenu/gui/ModMenuButtonWidget.java | 7 +- .../gui/ModMenuTexturedButtonWidget.java | 122 +- .../modmenu/gui/TextFieldWidget.java | 1309 +++++++++-------- .../modmenu/gui/entries/ChildEntry.java | 51 +- .../modmenu/gui/entries/IndependentEntry.java | 8 +- .../modmenu/gui/entries/ParentEntry.java | 216 +-- .../modmenu/impl/ModMenuApiImpl.java | 6 - .../modmenu/mixin/GuiButtonAccessor.java | 15 +- .../modmenu/mixin/LanguageAccessor.java | 5 +- .../modmenu/mixin/MinecraftAccessor.java | 9 +- .../modmenu/mixin/MixinGuiIngameMenu.java | 29 +- .../modmenu/mixin/MixinGuiMainMenu.java | 41 +- .../prospector/modmenu/mixin/MixinI18n.java | 17 +- .../modmenu/mixin/MixinTexturePacks.java | 21 +- .../mixin/ModMenuMixinConfigPlugin.java | 9 +- .../mixin/TextFieldEditorAccessor.java | 1 - .../modmenu/util/BadgeRenderer.java | 108 +- .../prospector/modmenu/util/ButtonUtil.java | 15 +- .../modmenu/util/HardcodedUtil.java | 147 +- .../modmenu/util/ModListSearch.java | 89 +- .../prospector/modmenu/util/RenderUtils.java | 89 +- .../modmenu/util/TestModContainer.java | 314 ++-- src/main/resources/fabric.mod.json | 11 +- ...ixins.modmenu.json => modmenu.mixins.json} | 2 +- 46 files changed, 3831 insertions(+), 3437 deletions(-) create mode 100644 .gitattributes delete mode 100644 build.gradle create mode 100644 build.gradle.kts delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts rename src/main/resources/{mixins.modmenu.json => modmenu.mixins.json} (91%) diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..eaeceeea7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto eol=lf +gradlew text eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf +gradle-wrapper.jar -text -diff -merge -filter \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 14b36ed85..c0d30fd1b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ # against bad commits. name: build -on: [pull_request, push] +on: [ pull_request, push ] jobs: build: @@ -12,28 +12,30 @@ jobs: matrix: # Use these Java versions java: [ - 17, + 21, # Current Java LTS & minimum supported by Minecraft ] # and run on both Linux and Windows - os: [ubuntu-22.04, windows-2022] + os: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - name: checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v5 - name: validate gradle wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/actions/wrapper-validation@v5 - name: setup jdk ${{ matrix.java }} - uses: actions/setup-java@v1 + uses: actions/setup-java@v5 with: java-version: ${{ matrix.java }} + distribution: 'temurin' + cache: gradle - name: make gradle wrapper executable if: ${{ runner.os != 'Windows' }} run: chmod +x ./gradlew - name: build run: ./gradlew build - name: capture build artifacts - if: ${{ runner.os == 'Linux' && matrix.java == '17' }} # Only upload artifacts built from latest java on one OS - uses: actions/upload-artifact@v2 + if: ${{ runner.os == 'Linux' && matrix.java == '21' }} # Only upload artifacts built from latest java on one OS + uses: actions/upload-artifact@v5 with: name: Artifacts - path: build/libs/ + path: build/libs/ \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 5dd5e9a2e..000000000 --- a/build.gradle +++ /dev/null @@ -1,169 +0,0 @@ -plugins { - id 'fabric-loom' version '1.7.bta' - id 'java' - id 'maven-publish' -} - -import org.gradle.internal.os.OperatingSystem - -project.ext.lwjglVersion = "3.3.4" - -switch (OperatingSystem.current()) { - case OperatingSystem.LINUX: - project.ext.lwjglNatives = "natives-linux" - break - case OperatingSystem.WINDOWS: - project.ext.lwjglNatives = "natives-windows" - break - case OperatingSystem.MAC_OS: - project.ext.lwjglNatives = "natives-macos" -} - -group = project.mod_group -archivesBaseName = project.mod_name -version = project.mod_version - -loom { - noIntermediateMappings() - customMinecraftMetadata.set("https://github.com/Turnip-Labs/bta-manifest-repo/releases/download/v${project.bta_version}/${project.bta_version}.json") -} - -repositories { - mavenCentral() - maven { url = "https://jitpack.io" } - maven { - name = 'Babric' - url = 'https://maven.glass-launcher.net/babric' - } - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } - maven { - name = 'signalumMaven' - url = 'https://maven.thesignalumproject.net/infrastructure' - } - maven { - name = 'signalumMaven' - url = 'https://maven.thesignalumproject.net/releases' - } - ivy { - url = "https://github.com/Better-than-Adventure" - patternLayout { - artifact "[organisation]/releases/download/v[revision]/[module].jar" - m2compatible = true - } - metadataSources { artifact() } - } - ivy { - url = "https://downloads.betterthanadventure.net/bta-client/prerelease/" - patternLayout { - artifact "/v[revision]/client.jar" - m2compatible = true - } - metadataSources { artifact() } - } - ivy { - url = "https://downloads.betterthanadventure.net/bta-server/prerelease/" - patternLayout { - artifact "/v[revision]/server.jar" - m2compatible = true - } - metadataSources { artifact() } - } - ivy { - url = "https://piston-data.mojang.com" - patternLayout { - artifact "v1/[organisation]/[revision]/[module].jar" - m2compatible = true - } - metadataSources { artifact() } - } -} - -dependencies { - minecraft "::${project.bta_version}" - mappings loom.layered() {} - - modRuntimeOnly "objects:client:43db9b498cb67058d2e12d394e6507722e71bb45" // https://piston-data.mojang.com/v1/objects/43db9b498cb67058d2e12d394e6507722e71bb45/client.jar - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" - - implementation "org.slf4j:slf4j-api:1.8.0-beta4" - implementation "org.apache.logging.log4j:log4j-slf4j18-impl:2.16.0" - - implementation 'com.google.guava:guava:33.0.0-jre' - implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1' - var log4jVersion = "2.20.0" - implementation("org.apache.logging.log4j:log4j-core:${log4jVersion}") - implementation("org.apache.logging.log4j:log4j-api:${log4jVersion}") - implementation("org.apache.logging.log4j:log4j-1.2-api:${log4jVersion}") - - include(implementation("org.apache.commons:commons-lang3:3.12.0")) - - modImplementation("com.github.zarzelcow:legacy-lwjgl3:1.0.4") - implementation platform("org.lwjgl:lwjgl-bom:$lwjglVersion") - - runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives" - runtimeOnly "org.lwjgl:lwjgl-assimp::$lwjglNatives" - runtimeOnly "org.lwjgl:lwjgl-glfw::$lwjglNatives" - runtimeOnly "org.lwjgl:lwjgl-openal::$lwjglNatives" - runtimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives" - runtimeOnly "org.lwjgl:lwjgl-stb::$lwjglNatives" - implementation "org.lwjgl:lwjgl:$lwjglVersion" - implementation "org.lwjgl:lwjgl-assimp:$lwjglVersion" - implementation "org.lwjgl:lwjgl-glfw:$lwjglVersion" - implementation "org.lwjgl:lwjgl-openal:$lwjglVersion" - implementation "org.lwjgl:lwjgl-opengl:$lwjglVersion" - implementation "org.lwjgl:lwjgl-stb:$lwjglVersion" -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - withSourcesJar() -} - -tasks.withType(JavaCompile).configureEach { - options.release.set 8 -} - -jar { - from("LICENSE") { - rename { "${it}_${archivesBaseName}" } - } -} - -configurations.configureEach { - // Removes LWJGL2 dependencies - exclude group: "org.lwjgl.lwjgl" -} - -processResources { - inputs.property "version", version - - filesMatching("fabric.mod.json") { - expand "version": version - } -} - -publishing { - repositories { - maven { - name = "signalumMaven" - url = "https://maven.thesignalumproject.net/releases" - credentials(PasswordCredentials) - authentication { - basic(BasicAuthentication) - } - } - } - - publications { - maven(MavenPublication) { - groupId = project.mod_group - artifactId = project.mod_name - version = project.mod_version - from components.java - } - } -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..f9d6b5b26 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,192 @@ +@file:Suppress("UnstableApiUsage", "PropertyName") + +import org.apache.tools.ant.taskdefs.condition.Os + +plugins { + id("fabric-loom") + java + `maven-publish` +} + +val lwjglVersion = providers.gradleProperty("lwjgl_version") +val lwjglNatives = when { + Os.isFamily(Os.FAMILY_UNIX) && !Os.isFamily(Os.FAMILY_MAC) -> "natives-linux" + Os.isFamily(Os.FAMILY_WINDOWS) -> "natives-windows" + Os.isFamily(Os.FAMILY_MAC) -> "natives-macos${if (Os.isArch("aarch64")) "-arm64" else ""}" + else -> error("Unsupported OS") +} + +val modVersion = providers.gradleProperty("mod_version") +val modGroup = providers.gradleProperty("mod_group") +val modName = providers.gradleProperty("mod_name") + +val btaChannel = providers.gradleProperty("bta_channel") +val btaVersion = providers.gradleProperty("bta_version") + +val loaderVersion = providers.gradleProperty("loader_version") +val legacyLwjglVersion = providers.gradleProperty("legacy_lwjgl_version") + +val slf4jApiVersion = providers.gradleProperty("slf4j_api_version") +val log4jVersion = providers.gradleProperty("log4j_version") +val guavaVersion = providers.gradleProperty("guava_version") +val gsonVersion = providers.gradleProperty("gson_version") +val commonsLang3Version = providers.gradleProperty("commons_lang3_version") + +val javaVersion = providers.gradleProperty("java_version") + +group = modGroup.get() +base.archivesName = modName.get() +version = modVersion.get() + +loom { + noIntermediateMappings() + customMinecraftMetadata.set("https://downloads.betterthanadventure.net/bta-client/${btaChannel.get()}/v${btaVersion.get()}/manifest.json") +} + +repositories { + mavenCentral() + maven("https://jitpack.io") + maven("https://maven.glass-launcher.net/babric") { name = "Babric" } + maven("https://maven.fabricmc.net/") { name = "Fabric" } + maven("https://maven.thesignalumproject.net/infrastructure") { name = "SignalumMavenInfrastructure" } + maven("https://maven.thesignalumproject.net/releases") { name = "SignalumMavenReleases" } + ivy("https://github.com/Better-than-Adventure") { + patternLayout { artifact("[organisation]/releases/download/v[revision]/[module].jar") } + metadataSources { artifact() } + } + ivy("https://downloads.betterthanadventure.net/bta-client/${btaChannel.get()}/") { + patternLayout { artifact("/v[revision]/client.jar") } + metadataSources { artifact() } + } + ivy("https://downloads.betterthanadventure.net/bta-server/${btaChannel.get()}/") { + patternLayout { artifact("/v[revision]/server.jar") } + metadataSources { artifact() } + } + ivy("https://piston-data.mojang.com") { + patternLayout { artifact("v1/[organisation]/[revision]/[module].jar") } + metadataSources { artifact() } + } +} + +dependencies { + minecraft("::${btaVersion.get()}") + mappings(loom.layered {}) + + // https://piston-data.mojang.com/v1/objects/43db9b498cb67058d2e12d394e6507722e71bb45/client.jar + modRuntimeOnly("objects:client:43db9b498cb67058d2e12d394e6507722e71bb45") + modImplementation("net.fabricmc:fabric-loader:${loaderVersion.get()}") + modImplementation("com.github.Better-than-Adventure:legacy-lwjgl3:${legacyLwjglVersion.get()}") + + implementation(platform("org.lwjgl:lwjgl-bom:${lwjglVersion.get()}")) + implementation("org.slf4j:slf4j-api:${slf4jApiVersion.get()}") + + implementation("com.google.guava:guava:${guavaVersion.get()}") + implementation("com.google.code.gson:gson:${gsonVersion.get()}") + + implementation("org.apache.logging.log4j:log4j-slf4j2-impl:${log4jVersion.get()}") + implementation("org.apache.logging.log4j:log4j-core:${log4jVersion.get()}") + implementation("org.apache.logging.log4j:log4j-api:${log4jVersion.get()}") + implementation("org.apache.logging.log4j:log4j-1.2-api:${log4jVersion.get()}") + + implementation("org.apache.commons:commons-lang3:${commonsLang3Version.get()}") + include("org.apache.commons:commons-lang3:${commonsLang3Version.get()}") + + implementation("org.lwjgl:lwjgl:${lwjglVersion.get()}") + implementation("org.lwjgl:lwjgl-assimp:${lwjglVersion.get()}") + implementation("org.lwjgl:lwjgl-glfw:${lwjglVersion.get()}") + implementation("org.lwjgl:lwjgl-openal:${lwjglVersion.get()}") + implementation("org.lwjgl:lwjgl-opengl:${lwjglVersion.get()}") + implementation("org.lwjgl:lwjgl-stb:${lwjglVersion.get()}") + + runtimeOnly("org.lwjgl:lwjgl::$lwjglNatives") + runtimeOnly("org.lwjgl:lwjgl-assimp::$lwjglNatives") + runtimeOnly("org.lwjgl:lwjgl-glfw::$lwjglNatives") + runtimeOnly("org.lwjgl:lwjgl-openal::$lwjglNatives") + runtimeOnly("org.lwjgl:lwjgl-opengl::$lwjglNatives") + runtimeOnly("org.lwjgl:lwjgl-stb::$lwjglNatives") +} + +tasks { + withType().configureEach { + options.encoding = "UTF-8" + sourceCompatibility = javaVersion.get() + targetCompatibility = javaVersion.get() + if (javaVersion.get().toInt() > 8) options.release = javaVersion.get().toInt() + } + withType().configureEach { defaultCharacterEncoding = "UTF-8" } + withType().configureEach { options.encoding = "UTF-8" } + withType().configureEach { defaultCharacterEncoding = "UTF-8" } + named("jar") { + val rootLicense = layout.projectDirectory.file("LICENSE") + val parentLicense = layout.projectDirectory.file("../LICENSE") + val licenseFile = when { + rootLicense.asFile.exists() -> { + logger.lifecycle("Using LICENSE from project root: ${rootLicense.asFile}") + rootLicense + } + parentLicense.asFile.exists() -> { + logger.lifecycle("Using LICENSE from parent directory: ${parentLicense.asFile}") + parentLicense + } + else -> { + logger.warn("No LICENSE file found in project or parent directory.") + null + } + } + licenseFile?.let { + from(it) { + rename { original -> "${original}_${archiveBaseName.get()}" } + } + } + } + processResources { + val stringModVersion = modVersion.get() + val stringLoaderVersion = loaderVersion.get() + val stringJavaVersion = javaVersion.get() + inputs.property("modVersion", stringModVersion) + inputs.property("loaderVersion", stringLoaderVersion) + inputs.property("javaVersion", stringJavaVersion) + filesMatching("fabric.mod.json") { + expand( + mapOf( + "version" to stringModVersion, + "fabricloader" to stringLoaderVersion, + "java" to stringJavaVersion + ) + ) + } + filesMatching("**/*.mixins.json") { expand(mapOf("java" to stringJavaVersion)) } + } + java { + toolchain { + languageVersion = JavaLanguageVersion.of(javaVersion.get()) + vendor = JvmVendorSpec.ADOPTIUM + } + sourceCompatibility = JavaVersion.toVersion(javaVersion.get().toInt()) + targetCompatibility = JavaVersion.toVersion(javaVersion.get().toInt()) + withSourcesJar() + } +} +// Removes LWJGL2 dependencies +configurations.configureEach { exclude(group = "org.lwjgl.lwjgl") } + +publishing { + repositories { + maven("https://maven.thesignalumproject.net/releases") { + name = "signalumMaven" + credentials(PasswordCredentials::class) + authentication { + create("basic") + } + } + } + publications { + create("maven") { + groupId = modGroup.get() + artifactId = modName.get() + version = modVersion.get() + from(components["java"]) + } + } +} + diff --git a/gradle.properties b/gradle.properties index 28dab3ef7..1a3cac984 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,47 @@ -org.gradle.jvmargs=-Xmx2G -org.gradle.daemon=false - -mod_version=3.0.0 -mod_group=turniplabs -mod_name=modmenu-bta - -bta_version=7.3-pre1 -loader_version=0.15.6-bta.7 +########################################################################## +# Standard Properties +org.gradle.jvmargs = -Xmx2G +org.gradle.warning.mode = all +# This currently has to be set to false because the "Minecraft Client" and "Minecraft Server" configurations generated +# by IntelliJ aren't currently compatible with the configuration cache. If you use the runClient and runServer Gradle +# tasks instead, then this can (and should) be set to true. +org.gradle.configuration-cache = false +########################################################################## +# Standard BTA Dependencies +# Check this on https://downloads.betterthanadventure.net/bta-client/ +bta_version = 7.3_04 +# Options are release, prerelease, nightly, and misc. +bta_channel = release +# Check this on https://maven.thesignalumproject.net/#/infrastructure/net/fabricmc/fabric-loader/ +loader_version = 0.17.3-bta.8 +# Check this on https://maven.thesignalumproject.net/#/infrastructure/fabric-loom/fabric-loom.gradle.plugin/ +loom_version = 1.13.0-bta +java_version = 8 +########################################################################## +# Mod Properties +mod_version = 4.0.0 +mod_group = turniplabs +mod_name = modmenu-bta +########################################################################## +# Mod Dependencies +# Check this on https://github.com/Better-than-Adventure/legacy-lwjgl3/releases/latest/ +legacy_lwjgl_version = 1.0.6 +########################################################################## +# 3rd Party Dependencies +# Check this on https://central.sonatype.com/artifact/org.slf4j/slf4j-api/ +slf4j_api_version = 2.0.17 +# Check this on https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-api/ +log4j_version = 2.20.0 +# Check this on https://central.sonatype.com/artifact/com.google.guava/guava/ +guava_version = 33.5.0-jre +# Check this on https://central.sonatype.com/artifact/com.google.code.gson/gson/ +gson_version = 2.13.2 +# Check this on https://central.sonatype.com/artifact/org.apache.commons/commons-lang3/ +commons_lang3_version = 3.20.0 +# This should match the version used by the current BTA release. +lwjgl_version = 3.3.3 +########################################################################## +# Plugin Dependency +# Check this on https://plugins.gradle.org/plugin/org.gradle.toolchains.foojay-resolver-convention/ +foojay_resolver_version = 1.0.0 +########################################################################## diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index deedc7fa5e6310eac3148a7dd0b1f069b07364cb..8bdaf60c75ab801e22807dde59e12a8735a34077 100644 GIT binary patch literal 45457 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 c82=K%QXLKFx%@O{wJjEi6Y56o#$)Bpeg literal 52818 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≺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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c1c32a50d..2397460ad 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,8 @@ -#Sun Nov 11 14:39:10 PST 2018 +# Check this on https://gradle.org/releases/ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-all.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/gradlew b/gradlew index 9aa616c27..adff685a0 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,128 @@ -#!/usr/bin/env bash +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,89 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then - cd "$(dirname "$0")" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f9553162f..c4bdd3ab8 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,25 +27,29 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -35,48 +57,35 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/jitpack.yml b/jitpack.yml index 063b3cf02..7c3264984 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,2 +1,5 @@ -jdk: -   - openjdk17 +jdk: + - openjdk21 +before_install: + - sdk install java 21.0.8-tem + - sdk use java 21.0.8-tem \ No newline at end of file diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index fa273184c..000000000 --- a/settings.gradle +++ /dev/null @@ -1,25 +0,0 @@ -pluginManagement { - repositories { - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } - gradlePluginPortal() - maven { - name = 'Jitpack' - url = 'https://jitpack.io' - } - maven { - name = 'Babric' - url = 'https://maven.glass-launcher.net/babric' - } - maven { - name = 'Quilt' - url = 'https://maven.quiltmc.org/repository/release' - } - maven { - name = 'signalumMaven' - url = 'https://maven.thesignalumproject.net/infrastructure' - } - } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..9603aa224 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,20 @@ +val modName = providers.gradleProperty("mod_name") +rootProject.name = modName.get() +pluginManagement { + repositories { + gradlePluginPortal() + maven("https://maven.fabricmc.net/") { name = "Fabric" } + maven("https://jitpack.io") { name = "Jitpack" } + maven("https://maven.glass-launcher.net/babric") { name = "Babric" } + maven("https://maven.thesignalumproject.net/infrastructure") { name = "SignalumMavenInfrastructure" } + } + val foojayResolverVersion = providers.gradleProperty("foojay_resolver_version") + val loomVersion = providers.gradleProperty("loom_version") + plugins { + id("org.gradle.toolchains.foojay-resolver-convention").version(foojayResolverVersion.get()) + id("fabric-loom").version(loomVersion.get()) + } +} +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") +} diff --git a/src/main/java/io/github/prospector/modmenu/ModMenu.java b/src/main/java/io/github/prospector/modmenu/ModMenu.java index dceb91c2e..272ce7bfe 100644 --- a/src/main/java/io/github/prospector/modmenu/ModMenu.java +++ b/src/main/java/io/github/prospector/modmenu/ModMenu.java @@ -18,6 +18,7 @@ import net.minecraft.client.gui.Screen; import net.minecraft.client.gui.options.ScreenOptions; import net.minecraft.client.gui.options.data.OptionsPages; +import org.jspecify.annotations.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,114 +26,116 @@ import java.util.*; import java.util.function.Function; +@SuppressWarnings("java:S2386") public class ModMenu implements ModInitializer { - public static final String MOD_ID = "modmenu"; - public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - public static final Gson GSON = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setPrettyPrinting().create(); + public static final String MOD_ID = "modmenu"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + public static final Gson GSON = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setPrettyPrinting().create(); - private static final Map LEGACY_CONFIG_SCREEN_TASKS = new HashMap<>(); - public static final List LIBRARY_MODS = new ArrayList<>(); - public static final Set CLIENTSIDE_MODS = new HashSet<>(); + private static final Map LEGACY_CONFIG_SCREEN_TASKS = new HashMap<>(); + public static final List LIBRARY_MODS = new ArrayList<>(); + public static final Set CLIENTSIDE_MODS = new HashSet<>(); public static final Set DEPRECATED_MODS = new HashSet<>(); - public static final Set PATCHWORK_FORGE_MODS = new HashSet<>(); + public static final Set PATCHWORK_FORGE_MODS = new HashSet<>(); public static final Map>> CUSTOM_BADGE_MODS = new HashMap<>(); - public static final LinkedListMultimap PARENT_MAP = LinkedListMultimap.create(); - private static ImmutableMap> configScreenFactories = ImmutableMap.of(); - - public static boolean hasConfigScreenFactory(String modid) { - return configScreenFactories.containsKey(modid); - } - - public static Screen getConfigScreen(String modid, Screen menuScreen) { - Function factory = configScreenFactories.get(modid); - return factory != null ? factory.apply(menuScreen) : null; - } - - public static void openConfigScreen(String modid) { - Runnable opener = LEGACY_CONFIG_SCREEN_TASKS.get(modid); - if (opener != null) opener.run(); - } - - public static void addLegacyConfigScreenTask(String modid, Runnable task) { - LEGACY_CONFIG_SCREEN_TASKS.putIfAbsent(modid, task); - } - - public static boolean hasLegacyConfigScreenTask(String modid) { - return LEGACY_CONFIG_SCREEN_TASKS.containsKey(modid); - } - - public static void addLibraryMod(String modid) { - if (LIBRARY_MODS.contains(modid)) return; - - LIBRARY_MODS.add(modid); - } - - @SuppressWarnings("RedundantCollectionOperation") - @Override - public void onInitialize() { - ModMenuConfigManager.initializeConfig(); - ImmutableMap.Builder> factories = ImmutableMap.builder(); - FabricLoader.getInstance().getEntrypointContainers("modmenu", ModMenuApi.class).forEach(entrypoint -> { - ModMenuApi api = entrypoint.getEntrypoint(); - ModContainer mod = entrypoint.getProvider(); + public static final LinkedListMultimap PARENT_MAP = LinkedListMultimap.create(); + private static ImmutableMap<@NonNull String, @NonNull Function> configScreenFactories = ImmutableMap.of(); + + public static boolean hasConfigScreenFactory(String modid) { + return configScreenFactories.containsKey(modid); + } + + public static Screen getConfigScreen(String modid, Screen menuScreen) { + Function factory = configScreenFactories.get(modid); + return factory != null ? factory.apply(menuScreen) : null; + } + + public static void openConfigScreen(String modid) { + Runnable opener = LEGACY_CONFIG_SCREEN_TASKS.get(modid); + if (opener != null) opener.run(); + } + + @SuppressWarnings("unused") + public static void addLegacyConfigScreenTask(String modid, Runnable task) { + LEGACY_CONFIG_SCREEN_TASKS.putIfAbsent(modid, task); + } + + public static boolean hasLegacyConfigScreenTask(String modid) { + return LEGACY_CONFIG_SCREEN_TASKS.containsKey(modid); + } + + public static void addLibraryMod(String modid) { + if (LIBRARY_MODS.contains(modid)) return; + + LIBRARY_MODS.add(modid); + } + + @SuppressWarnings("RedundantCollectionOperation") + @Override + public void onInitialize() { + ModMenuConfigManager.initializeConfig(); + ImmutableMap.Builder<@NonNull String, @NonNull Function> factories = ImmutableMap.builder(); + FabricLoader.getInstance().getEntrypointContainers(MOD_ID, ModMenuApi.class).forEach(entrypoint -> { + ModMenuApi api = entrypoint.getEntrypoint(); + ModContainer mod = entrypoint.getProvider(); try { api.getClass().getDeclaredMethod("getConfigScreenFactory"); // Make sure the method is implemented factories.put(mod.getMetadata().getId(), api.getConfigScreenFactory()); - } catch (NoSuchMethodException ignored) {} + } catch (NoSuchMethodException ignored) { /* noop */} api.attachCustomBadges((name, outlineColor, fillColor) -> { Map> map = new HashMap<>(); map.put(name, new AbstractMap.SimpleEntry<>(outlineColor, fillColor)); CUSTOM_BADGE_MODS.put(mod.getMetadata().getId(), map); }); }); - factories.put("minecraft", (screenBase -> new ScreenOptions(screenBase, OptionsPages.GENERAL))); - configScreenFactories = factories.build(); - Collection mods = FabricLoader.getInstance().getAllMods(); - HardcodedUtil.initializeHardcodings(); - for (ModContainer mod : mods) { - ModMetadata metadata = mod.getMetadata(); - String id = metadata.getId(); - // API badges - if (metadata.containsCustomValue("modmenu:api") && metadata.getCustomValue("modmenu:api").getAsBoolean()) { - addLibraryMod(id); - } - - // Client side badges - if (metadata.getEnvironment().equals(ModEnvironment.CLIENT)) { - CLIENTSIDE_MODS.add(id); - } - if (metadata.containsCustomValue("modmenu:clientsideOnly") && metadata.getCustomValue("modmenu:clientsideOnly").getAsBoolean()) { - LOGGER.warn("Found mod with id \"{}\" using deprecated value \"modmenu:clientsideOnly\"!", metadata.getId()); - if (!(CLIENTSIDE_MODS.contains(id))) CLIENTSIDE_MODS.add(id); - } - - // Deprecated badges + factories.put("minecraft", (screenBase -> new ScreenOptions(screenBase, OptionsPages.GENERAL))); + configScreenFactories = factories.build(); + Collection mods = FabricLoader.getInstance().getAllMods(); + HardcodedUtil.initializeHardcodings(); + for (ModContainer mod : mods) { + ModMetadata metadata = mod.getMetadata(); + String id = metadata.getId(); + // API badges + if (metadata.containsCustomValue("modmenu:api") && metadata.getCustomValue("modmenu:api").getAsBoolean()) { + addLibraryMod(id); + } + + // Client side badges + if (metadata.getEnvironment().equals(ModEnvironment.CLIENT)) { + CLIENTSIDE_MODS.add(id); + } + if (metadata.containsCustomValue("modmenu:clientsideOnly") && metadata.getCustomValue("modmenu:clientsideOnly").getAsBoolean()) { + LOGGER.warn("Found mod with id \"{}\" using deprecated value \"modmenu:clientsideOnly\"!", metadata.getId()); + if (!(CLIENTSIDE_MODS.contains(id))) CLIENTSIDE_MODS.add(id); + } + + // Deprecated badges if (metadata.containsCustomValue("modmenu:deprecated") && metadata.getCustomValue("modmenu:deprecated").getAsBoolean()) { DEPRECATED_MODS.add(id); } - // Patchwork (unused) - if (metadata.containsCustomValue("patchwork:source") && metadata.getCustomValue("patchwork:source").getAsObject() != null) { - CustomValue.CvObject object = metadata.getCustomValue("patchwork:source").getAsObject(); - if ("forge".equals(object.get("loader").getAsString())) { - PATCHWORK_FORGE_MODS.add(id); - } - } - - // Parent mods - if (metadata.containsCustomValue("modmenu:parent")) { - String parentId = metadata.getCustomValue("modmenu:parent").getAsString(); - if (parentId != null) { - Optional parent = FabricLoader.getInstance().getModContainer(parentId); - parent.ifPresent(modContainer -> PARENT_MAP.put(modContainer, mod)); - } - } else { - HardcodedUtil.hardcodeModuleMetadata(mod, metadata, id); - } - } - } - - public static String getFormattedModCount() { - return NumberFormat.getInstance().format(FabricLoader.getInstance().getAllMods().size()); - } + // Patchwork (unused) + if (metadata.containsCustomValue("patchwork:source") && metadata.getCustomValue("patchwork:source").getAsObject() != null) { + CustomValue.CvObject object = metadata.getCustomValue("patchwork:source").getAsObject(); + if ("forge".equals(object.get("loader").getAsString())) { + PATCHWORK_FORGE_MODS.add(id); + } + } + + // Parent mods + if (metadata.containsCustomValue("modmenu:parent")) { + String parentId = metadata.getCustomValue("modmenu:parent").getAsString(); + if (parentId != null) { + Optional parent = FabricLoader.getInstance().getModContainer(parentId); + parent.ifPresent(modContainer -> PARENT_MAP.put(modContainer, mod)); + } + } else { + HardcodedUtil.hardcodeModuleMetadata(mod, metadata, id); + } + } + } + + public static String getFormattedModCount() { + return NumberFormat.getInstance().format(FabricLoader.getInstance().getAllMods().size()); + } } diff --git a/src/main/java/io/github/prospector/modmenu/api/ModMenuApi.java b/src/main/java/io/github/prospector/modmenu/api/ModMenuApi.java index c9cc10f99..ff99ba4c8 100644 --- a/src/main/java/io/github/prospector/modmenu/api/ModMenuApi.java +++ b/src/main/java/io/github/prospector/modmenu/api/ModMenuApi.java @@ -1,61 +1,21 @@ package io.github.prospector.modmenu.api; - -import io.github.prospector.modmenu.ModMenu; import io.github.prospector.modmenu.util.TriConsumer; import net.minecraft.client.gui.Screen; -import org.jetbrains.annotations.ApiStatus; -import java.util.Optional; import java.util.function.Function; -import java.util.function.Supplier; public interface ModMenuApi { - /** - * Replaced with {@link ModMenuApi#getConfigScreen(Screen)}, with - * the ModMenuApi implemented onto a class that is added as an - * entry point to your fabric mod metadata. - * - * @deprecated Will be removed in 1.15 snapshots. - */ - @Deprecated - static void addConfigOverride(String modid, Runnable action) { - ModMenu.addLegacyConfigScreenTask(modid, action); - } - - /** - * Used to determine the owner of this API implementation. - * - * @deprecated This method is deprecated as Fabric Loader - * itself provides provider info for entrypoints. - */ - @Deprecated - String getModId(); - - /** - * Replaced with {@link ModMenuApi#getConfigScreenFactory()}, which - * now allows ModMenu to open the screen for you, rather than depending - * on you to open it, and gets rid of the messy Optional->Supplier wrapping. - * - * @deprecated For internal use only. - */ - @Deprecated - @ApiStatus.Internal - default Optional> getConfigScreen(Screen screen) { - return Optional.empty(); - } - - /** - * Used to construct a new config screen instance when your mod's - * configuration button is selected on the mod menu screen. The - * screen instance parameter is the active mod menu screen. - * - * @return A factory function for constructing config screen instances. - */ - default Function getConfigScreenFactory() { - return screen -> getConfigScreen(screen).map(Supplier::get).orElse(null); - } - + /** + * Used to construct a new config screen instance when your mod's + * configuration button is selected on the mod menu screen. The + * screen instance parameter is the active mod menu screen. + * + * @return A factory function for constructing config screen instances. + */ + default Function getConfigScreenFactory() { + return screen -> null; + } /** * Add custom badges for mods to use. diff --git a/src/main/java/io/github/prospector/modmenu/config/ModMenuConfig.java b/src/main/java/io/github/prospector/modmenu/config/ModMenuConfig.java index 128a3df5d..336dd4fb3 100644 --- a/src/main/java/io/github/prospector/modmenu/config/ModMenuConfig.java +++ b/src/main/java/io/github/prospector/modmenu/config/ModMenuConfig.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.config; - import io.github.prospector.modmenu.util.HardcodedUtil; import net.fabricmc.loader.api.ModContainer; import net.minecraft.core.lang.I18n; @@ -8,45 +7,45 @@ import java.util.Comparator; public class ModMenuConfig { - private boolean showLibraries = false; - private Sorting sorting = Sorting.ASCENDING; - - public void toggleShowLibraries() { - this.showLibraries = !this.showLibraries; - ModMenuConfigManager.save(); - } - - public void toggleSortMode() { - this.sorting = Sorting.values()[(sorting.ordinal() + 1) % Sorting.values().length]; - ModMenuConfigManager.save(); - } - - public boolean showLibraries() { - return showLibraries; - } - - public Sorting getSorting() { - return sorting; - } - - public enum Sorting { - ASCENDING(Comparator.comparing(modContainer -> HardcodedUtil.formatFabricModuleName(modContainer.getMetadata().getName())), "modmenu.sorting.ascending"), - DECENDING(ASCENDING.getComparator().reversed(), "modmenu.sorting.decending"); - - final Comparator comparator; - final String key; - - Sorting(Comparator comparator, String key) { - this.comparator = comparator; - this.key = key; - } - - public Comparator getComparator() { - return comparator; - } - - public String getName() { - return I18n.getInstance().translateKey(key); - } - } + private boolean showLibraries = false; + private Sorting sorting = Sorting.ASCENDING; + + public void toggleShowLibraries() { + this.showLibraries = !this.showLibraries; + ModMenuConfigManager.save(); + } + + public void toggleSortMode() { + this.sorting = Sorting.values()[(sorting.ordinal() + 1) % Sorting.values().length]; + ModMenuConfigManager.save(); + } + + public boolean showLibraries() { + return showLibraries; + } + + public Sorting getSorting() { + return sorting; + } + + public enum Sorting { + ASCENDING(Comparator.comparing(modContainer -> HardcodedUtil.formatFabricModuleName(modContainer.getMetadata().getName())), "modmenu.sorting.ascending"), + DECENDING(ASCENDING.getComparator().reversed(), "modmenu.sorting.decending"); + + private final Comparator comparator; + private final String key; + + Sorting(Comparator comparator, String key) { + this.comparator = comparator; + this.key = key; + } + + public Comparator getComparator() { + return comparator; + } + + public String getName() { + return I18n.getInstance().translateKey(key); + } + } } diff --git a/src/main/java/io/github/prospector/modmenu/config/ModMenuConfigManager.java b/src/main/java/io/github/prospector/modmenu/config/ModMenuConfigManager.java index ea589446b..9aef3375f 100644 --- a/src/main/java/io/github/prospector/modmenu/config/ModMenuConfigManager.java +++ b/src/main/java/io/github/prospector/modmenu/config/ModMenuConfigManager.java @@ -4,62 +4,54 @@ import io.github.prospector.modmenu.ModMenu; import net.fabricmc.loader.api.FabricLoader; -import java.io.*; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; public class ModMenuConfigManager { - private static File file; - private static ModMenuConfig config; - - private static void prepareBiomeConfigFile() { - if (file != null) { - return; - } - file = new File(FabricLoader.getInstance().getConfigDir().toFile(), ModMenu.MOD_ID + ".json"); - } - - public static ModMenuConfig initializeConfig() { - if (config != null) { - return config; - } - - config = new ModMenuConfig(); - load(); - - return config; - } - - private static void load() { - prepareBiomeConfigFile(); - - try { - if (!file.exists()) { - save(); - } - if (file.exists()) { - BufferedReader br = new BufferedReader(new FileReader(file)); - - config = ModMenu.GSON.fromJson(br, ModMenuConfig.class); - } - } catch (FileNotFoundException e) { - System.err.println("Couldn't load Mod Menu configuration file; reverting to defaults"); - e.printStackTrace(); - } - } - - public static void save() { - prepareBiomeConfigFile(); - - String jsonString = ModMenu.GSON.toJson(config); - - try (FileWriter fileWriter = new FileWriter(file)) { - fileWriter.write(jsonString); - } catch (IOException e) { - System.err.println("Couldn't save Mod Menu configuration file"); - e.printStackTrace(); - } - } - - public static ModMenuConfig getConfig() { - return config; - } + private static Path file; + private static ModMenuConfig config; + + private static void prepareBiomeConfigFile() { + if (file != null) return; + file = FabricLoader.getInstance().getConfigDir().resolve(ModMenu.MOD_ID + ".json"); + } + + @SuppressWarnings("UnusedReturnValue") + public static ModMenuConfig initializeConfig() { + if (config != null) return config; + config = new ModMenuConfig(); + load(); + return config; + } + + private static void load() { + prepareBiomeConfigFile(); + try { + if (Files.notExists(file)) save(); + if (Files.exists(file)) { + BufferedReader br = Files.newBufferedReader(file); + config = ModMenu.GSON.fromJson(br, ModMenuConfig.class); + } + } catch (IOException e) { + ModMenu.LOGGER.error("Couldn't load Mod Menu configuration file; reverting to defaults", e); + } + } + + public static void save() { + prepareBiomeConfigFile(); + String jsonString = ModMenu.GSON.toJson(config); + try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { + writer.write(jsonString); + } catch (IOException e) { + ModMenu.LOGGER.error("Couldn't save Mod Menu configuration file", e); + } + } + + public static ModMenuConfig getConfig() { + return config; + } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/AlwaysSelectedEntryListWidget.java b/src/main/java/io/github/prospector/modmenu/gui/AlwaysSelectedEntryListWidget.java index be7793968..50a212457 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/AlwaysSelectedEntryListWidget.java +++ b/src/main/java/io/github/prospector/modmenu/gui/AlwaysSelectedEntryListWidget.java @@ -1,45 +1,16 @@ -// -// Source code recreated from a .class file by IntelliJ IDEA -// (powered by Fernflower decompiler) -// - package io.github.prospector.modmenu.gui; - import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.Minecraft; @Environment(EnvType.CLIENT) public abstract class AlwaysSelectedEntryListWidget> extends EntryListWidget { - private boolean inFocus; - - public AlwaysSelectedEntryListWidget(Minecraft minecraftClient, int i, int j, int k, int l, int m) { - super(minecraftClient, i, j, k, l, m); - } - - public boolean setFocus(boolean bl) { - if (!this.inFocus && this.getItemCount() == 0) { - return false; - } else { - this.inFocus = !this.inFocus; - if (this.inFocus && this.getSelected() == null && this.getItemCount() > 0) { - this.moveSelection(1); - } else if (this.inFocus && this.getSelected() != null) { - this.moveSelection(0); - } - - return this.inFocus; - } - } - - @Environment(EnvType.CLIENT) - public abstract static class Entry> extends EntryListWidget.Entry { - public Entry() { - } - - public boolean changeFocus(boolean bl) { - return false; - } - } + protected AlwaysSelectedEntryListWidget(Minecraft minecraft, int width, int height, int top, int bottom, int itemHeight) { + super(minecraft, width, height, top, bottom, itemHeight); + } + @Environment(EnvType.CLIENT) + public abstract static class Entry> extends EntryListWidget.Entry { + protected Entry() {} + } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/DescriptionListWidget.java b/src/main/java/io/github/prospector/modmenu/gui/DescriptionListWidget.java index 6bf48d669..d9dd899fa 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/DescriptionListWidget.java +++ b/src/main/java/io/github/prospector/modmenu/gui/DescriptionListWidget.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.gui; - import io.github.prospector.modmenu.util.HardcodedUtil; import io.github.prospector.modmenu.util.RenderUtils; import net.fabricmc.loader.api.metadata.Person; @@ -11,52 +10,52 @@ import java.util.Collection; public class DescriptionListWidget extends EntryListWidget { - private final ModListScreen parent; - private final Font textRenderer; - private ModListEntry lastSelected = null; + private final ModListScreen parent; + private final Font textRenderer; + private ModListEntry lastSelected = null; - public DescriptionListWidget(Minecraft client, int width, int height, int top, int bottom, int entryHeight, ModListScreen parent) { - super(client, width, height, top, bottom, entryHeight); - this.parent = parent; - this.textRenderer = client.font; - } + public DescriptionListWidget(Minecraft client, int width, int height, int top, int bottom, int entryHeight, ModListScreen parent) { + super(client, width, height, top, bottom, entryHeight); + this.parent = parent; + this.textRenderer = client.font; + } - @Override - public DescriptionEntry getSelected() { - return null; - } + @Override + public DescriptionEntry getSelected() { + return null; + } - @Override - public int getRowWidth() { - return this.width - 10; - } + @Override + public int getRowWidth() { + return this.width - 10; + } - @Override - protected int getScrollbarPosition() { - return this.width - 6 + left; - } + @Override + protected int getScrollbarPosition() { + return this.width - 6 + left; + } - @Override - public void render(int mouseX, int mouseY, float delta) { - I18n i18n = I18n.getInstance(); - ModListEntry selectedEntry = parent.getSelectedEntry(); - if (selectedEntry != lastSelected) { - lastSelected = selectedEntry; - clearEntries(); - setScrollAmount(-Double.MAX_VALUE); - String description = lastSelected.getMetadata().getDescription(); - String id = lastSelected.getMetadata().getId(); + @Override + public void render(int mouseX, int mouseY, float delta) { + I18n i18n = I18n.getInstance(); + ModListEntry selectedEntry = parent.getSelectedEntry(); + if (selectedEntry != lastSelected) { + lastSelected = selectedEntry; + clearEntries(); + setScrollAmount(-Double.MAX_VALUE); + String description = lastSelected.getMetadata().getDescription(); + String id = lastSelected.getMetadata().getId(); Collection authors = lastSelected.getMetadata().getAuthors(); Collection contributors = lastSelected.getMetadata().getContributors(); Collection licenses = lastSelected.getMetadata().getLicense(); - if (description.isEmpty() && HardcodedUtil.getHardcodedDescriptions().containsKey(id)) { - description = HardcodedUtil.getHardcodedDescription(id); - } - if (lastSelected != null && description != null && !description.isEmpty()) { - for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, description.replaceAll("\n", "\n\n"), getRowWidth())) { - children().add(new DescriptionEntry(line)); - } - } + if (description.isEmpty() && HardcodedUtil.getHardcodedDescriptions().containsKey(id)) { + description = HardcodedUtil.getHardcodedDescription(id); + } + if (lastSelected != null && description != null && !description.isEmpty()) { + for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, description.replace("\n", "\n\n"), getRowWidth())) { + children().add(new DescriptionEntry(line)); + } + } if (!authors.isEmpty()) { if (!children().isEmpty()) children().add(new DescriptionEntry("")); children().add(new DescriptionEntry(i18n.translateKey("modmenu.authors"))); @@ -78,27 +77,27 @@ public void render(int mouseX, int mouseY, float delta) { children().add(new DescriptionEntry(" " + license)); } } - } - super.render(mouseX, mouseY, delta); - } + } + super.render(mouseX, mouseY, delta); + } - @Override - protected void renderHoleBackground(int y1, int y2, int startAlpha, int endAlpha) { - // Awful hack but it makes the background "seamless" - parent.overlayBackground(left, y1, right, y2, 64, 64, 64, startAlpha, endAlpha); - } + @Override + protected void renderHoleBackground(int y1, int y2, int startAlpha, int endAlpha) { + // Awful hack but it makes the background "seamless" + parent.overlayBackground(left, y1, right, y2, 64, 64, 64, startAlpha, endAlpha); + } - protected class DescriptionEntry extends EntryListWidget.Entry { - protected String text; + public class DescriptionEntry extends EntryListWidget.Entry { + protected String text; - public DescriptionEntry(String text) { - this.text = text; - } + public DescriptionEntry(String text) { + this.text = text; + } - @Override - public void render(int index, int y, int x, int itemWidth, int itemHeight, int mouseX, int mouseY, boolean isSelected, float delta) { - textRenderer.drawStringWithShadow(text, x, y, 0xAAAAAA); - } - } + @Override + public void render(int index, int y, int x, int itemWidth, int itemHeight, int mouseX, int mouseY, boolean isSelected, float delta) { + textRenderer.drawStringWithShadow(text, x, y, 0xAAAAAA); + } + } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/EntryListWidget.java b/src/main/java/io/github/prospector/modmenu/gui/EntryListWidget.java index 888504bbb..db86bde6b 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/EntryListWidget.java +++ b/src/main/java/io/github/prospector/modmenu/gui/EntryListWidget.java @@ -1,13 +1,13 @@ package io.github.prospector.modmenu.gui; - import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Screen; import net.minecraft.client.render.tessellator.Tessellator; import net.minecraft.core.util.helper.MathHelper; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.Nullable; +import org.lwjgl.glfw.GLFW; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL14; import org.spongepowered.include.com.google.common.collect.Lists; @@ -17,537 +17,658 @@ import java.util.List; import java.util.Objects; -@SuppressWarnings({"unchecked", "unused"}) @Environment(EnvType.CLIENT) public abstract class EntryListWidget> extends Screen { - protected static final int DRAG_OUTSIDE = -2; - protected final Minecraft minecraft; - protected final int itemHeight; - private final List children = new EntryListWidget.Entries(); - protected int width; - protected int height; - protected int top; - protected int bottom; - protected int right; - protected int left; - protected boolean centerListVertically = true; - protected int yDrag = -2; - private double scrollAmount; - protected boolean renderSelection = true; - protected boolean renderHeader; - protected int headerHeight; - private boolean scrolling; - private E selected; - - private E focused; - private boolean dragging; - - public EntryListWidget(Minecraft minecraftClient, int i, int j, int k, int l, int m) { - this.minecraft = minecraftClient; - this.width = i; - this.height = j; - this.top = k; - this.bottom = l; - this.itemHeight = m; - this.left = 0; - this.right = i; - } - - public void setRenderSelection(boolean bl) { - this.renderSelection = bl; - } - - protected void setRenderHeader(boolean bl, int i) { - this.renderHeader = bl; - this.headerHeight = i; - if (!bl) { - this.headerHeight = 0; - } - - } - - public int getRowWidth() { - return 220; - } - - @Nullable - public E getSelected() { - return this.selected; - } - - public void setSelected(@Nullable E entry) { - this.selected = entry; - } - - @Nullable - public E getFocused() { - return focused; - } - - public void setFocused(E focused) { - this.focused = focused; - } - - public void setDragging(boolean dragging) { - this.dragging = dragging; - } - - public final List children() { - return this.children; - } - - protected final void clearEntries() { - this.children.clear(); - } - - protected void replaceEntries(Collection collection) { - this.children.clear(); - this.children.addAll(collection); - } - - protected E getEntry(int i) { - return this.children().get(i); - } - - protected int addEntry(E entry) { - this.children.add(entry); - return this.children.size() - 1; - } - - protected int getItemCount() { - return this.children().size(); - } - - protected boolean isSelectedItem(int i) { - return Objects.equals(this.getSelected(), this.children().get(i)); - } - - @Nullable - protected final E getEntryAtPosition(double d, double e) { - int i = this.getRowWidth() / 2; - int j = this.left + this.width / 2; - int k = j - i; - int l = j + i; - int m = MathHelper.floor(e - (double)this.top) - this.headerHeight + (int)this.getScrollAmount() - 4; // convertToBlockCoord - int n = m / this.itemHeight; - return d < (double)this.getScrollbarPosition() && d >= (double)k && d <= (double)l && n >= 0 && m >= 0 && n < this.getItemCount() ? this.children().get(n) : null; - } - - public void updateSize(int i, int j, int k, int l) { - this.width = i; - this.height = j; - this.top = k; - this.bottom = l; - this.left = 0; - this.right = i; - } - - public void setLeftPos(int i) { - this.left = i; - this.right = i + this.width; - } - - protected int getMaxPosition() { - return this.getItemCount() * this.itemHeight + this.headerHeight; - } - - protected void clickedHeader(int i, int j) { - } - - protected void renderHeader(int i, int j, Tessellator tessellator) { - } - - - public void renderBackground() { - } - - protected void renderDecorations(int i, int j) { - } - - public void render(int i, int j, float f) { - oldY = j; - this.renderBackground(); - int k = this.getScrollbarPosition(); - int l = k + 6; - Tessellator tessellator = Tessellator.instance; - this.minecraft.textureManager.bindTexture(this.minecraft.textureManager.loadTexture("/gui/background.png")); - GL11.glColor4f(1f, 1f, 1f, 1f); - float g = 32.0F; - tessellator.startDrawingQuads(); - tessellator.setColorOpaque(32, 32, 32); - tessellator.addVertexWithUV(this.left, this.bottom, 0.0D, (float)this.left / 32.0F, (float)(this.bottom + (int)this.getScrollAmount()) / 32.0F); - tessellator.addVertexWithUV(this.right, this.bottom, 0.0D, (float)this.right / 32.0F, (float)(this.bottom + (int)this.getScrollAmount()) / 32.0F); - tessellator.addVertexWithUV(this.right, this.top, 0.0D, (float)this.right / 32.0F, (float)(this.top + (int)this.getScrollAmount()) / 32.0F); - tessellator.addVertexWithUV(this.left, this.top, 0.0D, (float)this.left / 32.0F, (float)(this.top + (int)this.getScrollAmount()) / 32.0F); - tessellator.draw(); - int m = this.getRowLeft(); - int n = this.top + 4 - (int)this.getScrollAmount(); - if (this.renderHeader) { - this.renderHeader(m, n, tessellator); - } - - this.renderList(m, n, i, j, f); - GL11.glDisable(GL11.GL_DEPTH_TEST); - this.renderHoleBackground(0, this.top, 255, 255); - this.renderHoleBackground(this.bottom, this.height, 255, 255); - GL11.glEnable(GL11.GL_BLEND); - GL14.glBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ZERO, GL11.GL_ONE); - GL11.glDisable(GL11.GL_ALPHA_TEST); - GL11.glShadeModel(GL11.GL_SMOOTH); - GL11.glDisable(GL11.GL_TEXTURE_2D); - boolean o = true; - tessellator.startDrawingQuads(); - tessellator.setColorRGBA(0, 0, 0, 0); - tessellator.addVertexWithUV(this.left, this.top + 4, 0.0D, 0.0F, 1.0F); - tessellator.addVertexWithUV(this.right, this.top + 4, 0.0D, 1.0F, 1.0F); - tessellator.setColorOpaque(0, 0, 0); - tessellator.addVertexWithUV(this.right, this.top, 0.0D, 1.0F, 0.0F); - tessellator.addVertexWithUV(this.left, this.top, 0.0D, 0.0F, 0.0F); - tessellator.draw(); - tessellator.startDrawingQuads(); - tessellator.setColorOpaque(0, 0, 0); - tessellator.addVertexWithUV(this.left, this.bottom, 0.0D, 0.0F, 1.0F); - tessellator.addVertexWithUV(this.right, this.bottom, 0.0D, 1.0F, 1.0F); - tessellator.setColorRGBA(0, 0, 0, 0); - tessellator.addVertexWithUV(this.right, this.bottom - 4, 0.0D, 1.0F, 0.0F); - tessellator.addVertexWithUV(this.left, this.bottom - 4, 0.0D, 0.0F, 0.0F); - tessellator.draw(); - int p = this.getMaxScroll(); - if (p > 0) { - int q = (int)((float)((this.bottom - this.top) * (this.bottom - this.top)) / (float)this.getMaxPosition()); - if (q < 32) - q = 32; - else if (q > this.bottom - this.top - 8) - q = this.bottom - this.top - 8; - int r = (int)this.getScrollAmount() * (this.bottom - this.top - q) / p + this.top; - if (r < this.top) { - r = this.top; - } - - tessellator.startDrawingQuads(); - tessellator.setColorOpaque(0, 0, 0); - tessellator.addVertexWithUV(k, this.bottom, 0.0D, 0.0F, 1.0F); - tessellator.addVertexWithUV(l, this.bottom, 0.0D, 1.0F, 1.0F); - tessellator.addVertexWithUV(l, this.top, 0.0D, 1.0F, 0.0F); - tessellator.addVertexWithUV(k, this.top, 0.0D, 0.0F, 0.0F); - tessellator.draw(); - tessellator.startDrawingQuads(); - tessellator.setColorOpaque(128, 128, 128); - tessellator.addVertexWithUV(k, r + q, 0.0D, 0.0F, 1.0F); - tessellator.addVertexWithUV(l, r + q, 0.0D, 1.0F, 1.0F); - tessellator.addVertexWithUV(l, r, 0.0D, 1.0F, 0.0F); - tessellator.addVertexWithUV(k, r, 0.0D, 0.0F, 0.0F); - tessellator.draw(); - tessellator.startDrawingQuads(); - tessellator.setColorOpaque(192, 192, 192); - tessellator.addVertexWithUV(k, r + q - 1, 0.0D, 0.0F, 1.0F); - tessellator.addVertexWithUV(l - 1, r + q - 1, 0.0D, 1.0F, 1.0F); - tessellator.addVertexWithUV(l - 1, r, 0.0D, 1.0F, 0.0F); - tessellator.addVertexWithUV(k, r, 0.0D, 0.0F, 0.0F); - tessellator.draw(); - } - - this.renderDecorations(i, j); - GL11.glEnable(GL11.GL_TEXTURE_2D); - GL11.glShadeModel(GL11.GL_FLAT); - GL11.glEnable(GL11.GL_ALPHA_TEST); - GL11.glDisable(GL11.GL_BLEND); - GL11.glEnable(GL11.GL_DEPTH_TEST); - } - - protected void centerScrollOn(E entry) { - this.setScrollAmount(this.children().indexOf(entry) * this.itemHeight + this.itemHeight / 2 - (this.bottom - this.top) / 2); - } - - protected void ensureVisible(E entry) { - int i = this.getRowTop(this.children().indexOf(entry)); - int j = i - this.top - 4 - this.itemHeight; - if (j < 0) { - this.scroll(j); - } - - int k = this.bottom - i - this.itemHeight - this.itemHeight; - if (k < 0) { - this.scroll(-k); - } - - } - - private void scroll(int i) { - this.setScrollAmount(this.getScrollAmount() + (double)i); - this.yDrag = -2; - } - - public double getScrollAmount() { - return this.scrollAmount; - } - - public void setScrollAmount(double d) { - if (d < 0) - d = 0; - if (d > getMaxScroll()) - d = getMaxScroll(); - this.scrollAmount = d; - } - - private int getMaxScroll() { - return Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4)); - } - - public int getScrollBottom() { - return (int)this.getScrollAmount() - this.height - this.headerHeight; - } - - protected void updateScrollingState(double d, double e, int i) { - this.scrolling = i == 0 && d >= (double)this.getScrollbarPosition() && d < (double)(this.getScrollbarPosition() + 6); - } - - protected int getScrollbarPosition() { - return this.width / 2 + 124; - } + protected static final int TOP_PADDING = 4; + protected static final int SCROLLBAR_WIDTH = 6; + protected final Minecraft minecraft; + protected final int itemHeight; + private final List children = new EntryListWidget.EntryList(); + protected int top; + protected int bottom; + protected int right; + protected int left; + private double scrollAmount; + protected boolean renderSelection = true; + protected boolean shouldRenderHeader; + protected int headerHeight; + private E selected; + private E focused; + + protected EntryListWidget(Minecraft minecraft, int width, int height, int top, int bottom, int itemHeight) { + this.minecraft = minecraft; + this.width = width; + this.height = height; + this.top = top; + this.bottom = bottom; + this.itemHeight = itemHeight; + this.left = 0; + this.right = width; + } + + @SuppressWarnings("unused") + public void setRenderSelection(boolean bl) { + this.renderSelection = bl; + } + + @SuppressWarnings("unused") + protected void setRenderHeader(boolean bl, int i) { + this.shouldRenderHeader = bl; + this.headerHeight = i; + if (!bl) { + this.headerHeight = 0; + } + + } + + public int getRowWidth() { + return 220; + } + + @Nullable + public E getSelected() { + return this.selected; + } + + public void setSelected(@Nullable E entry) { + this.selected = entry; + } + + @Nullable + public E getFocused() { + return focused; + } + + @SuppressWarnings("unused") + public void setFocused(E focused) { + this.focused = focused; + } + + public final List children() { + return this.children; + } + + protected final void clearEntries() { + this.children.clear(); + } + + @SuppressWarnings("unused") + protected void replaceEntries(Collection collection) { + this.children.clear(); + this.children.addAll(collection); + } + + protected E getEntry(int index) { + return this.children().get(index); + } + + protected int addEntry(E entry) { + this.children.add(entry); + return this.children.size() - 1; + } + + protected int getItemCount() { + return this.children().size(); + } + + protected boolean isSelectedItem(int index) { + return Objects.equals(this.getSelected(), this.children().get(index)); + } + + @Nullable + protected E getEntryAtPosition(double mouseX, double mouseY) { + int halfRowWidth = this.getRowWidth() / 2; + int centerX = this.left + this.width / 2; + int rowLeft = centerX - halfRowWidth; + int rowRight = centerX + halfRowWidth; + + // Convert screen Y to list-relative Y, accounting for header and scroll + int yInList = MathHelper.floor(mouseY - this.top) - this.headerHeight + (int) this.getScrollAmount() - TOP_PADDING; + int rowIndex = yInList / this.itemHeight; + + // Outside horizontal list area or over scrollbar + if (mouseX >= this.getScrollbarPosition() || mouseX < rowLeft || mouseX > rowRight) { + return null; + } + + // Above first row or before header area + if (yInList < 0 || rowIndex < 0 || rowIndex >= this.getItemCount()) { + return null; + } + + return this.children().get(rowIndex); + } + + + @SuppressWarnings("unused") + public void updateSize(int width, int height, int top, int bottom) { + this.width = width; + this.height = height; + this.top = top; + this.bottom = bottom; + this.left = 0; + this.right = width; + } + + public void setLeftEdge(int leftEdge) { + this.left = leftEdge; + this.right = leftEdge + this.width; + } + + protected int getMaxPosition() { + return this.getItemCount() * this.itemHeight + this.headerHeight; + } + + @SuppressWarnings("unused") + protected void onHeaderClicked(int relativeX, int relativeY) {} + + @SuppressWarnings("unused") + protected void renderHeader(int left, int top, Tessellator tessellator) {} + + @Override + public void renderBackground() {} + + @SuppressWarnings("unused") + protected void renderDecorations(int mouseX, int mouseY) {} @Override - public void mouseClicked(int d, int e, int i) { - this.updateScrollingState(d, e, i); - if (this.isMouseOver(d, e)) { - E entry = this.getEntryAtPosition(d, e); - if (entry != null) { - if (entry.list.getFocused() != null) { - if (!entry.list.getFocused().equals(entry)) { - this.focused = entry; - this.selected = entry; - this.dragging = true; - super.mouseClicked(d, e, i); - } - } else { - this.focused = entry; - this.selected = entry; - this.dragging = true; - super.mouseClicked(d, e, i); + public void render(int mouseX, int mouseY, float delta) { + // No more oldY usage – we now use deltaY in mouseDragged instead + + this.renderBackground(); + + int scrollbarX = this.getScrollbarPosition(); + int scrollbarEndX = scrollbarX + SCROLLBAR_WIDTH; + + Tessellator tessellator = Tessellator.instance; + + // Background texture + this.minecraft.textureManager.bindTexture( + this.minecraft.textureManager.loadTexture("/gui/background.png") + ); + GL11.glColor4f(1f, 1f, 1f, 1f); + + tessellator.startDrawingQuads(); + tessellator.setColorOpaque(32, 32, 32); + tessellator.addVertexWithUV(this.left, this.bottom, 0.0D, this.left / 32.0F, (this.bottom + (int) this.getScrollAmount()) / 32.0F); + tessellator.addVertexWithUV(this.right, this.bottom, 0.0D, this.right / 32.0F, (this.bottom + (int) this.getScrollAmount()) / 32.0F); + tessellator.addVertexWithUV(this.right, this.top, 0.0D,this.right / 32.0F, (this.top + (int) this.getScrollAmount()) / 32.0F); + tessellator.addVertexWithUV(this.left, this.top, 0.0D, this.left / 32.0F, (this.top + (int) this.getScrollAmount()) / 32.0F); + tessellator.draw(); + + int rowLeft = this.getRowLeft(); + int baseRowTop = this.top + TOP_PADDING - (int) this.getScrollAmount(); + + // Optional header + if (this.shouldRenderHeader) { + this.renderHeader(rowLeft, baseRowTop, tessellator); + } + + // List entries + this.renderList(rowLeft, baseRowTop, mouseX, mouseY, delta); + + // Black top/bottom overlays + GL11.glDisable(GL11.GL_DEPTH_TEST); + this.renderHoleBackground(0, this.top, 255, 255); + this.renderHoleBackground(this.bottom, this.height, 255, 255); + + // Fade edges + GL11.glEnable(GL11.GL_BLEND); + GL14.glBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ZERO, GL11.GL_ONE); + GL11.glDisable(GL11.GL_ALPHA_TEST); + GL11.glShadeModel(GL11.GL_SMOOTH); + GL11.glDisable(GL11.GL_TEXTURE_2D); + + // Top gradient + tessellator.startDrawingQuads(); + tessellator.setColorRGBA(0, 0, 0, 0); + tessellator.addVertexWithUV(this.left, (double) this.top + TOP_PADDING, 0.0D, 0.0F, 1.0F); + tessellator.addVertexWithUV(this.right, (double) this.top + TOP_PADDING, 0.0D, 1.0F, 1.0F); + tessellator.setColorOpaque(0, 0, 0); + tessellator.addVertexWithUV(this.right, this.top, 0.0D, 1.0F, 0.0F); + tessellator.addVertexWithUV(this.left, this.top, 0.0D, 0.0F, 0.0F); + tessellator.draw(); + + // Bottom gradient + tessellator.startDrawingQuads(); + tessellator.setColorOpaque(0, 0, 0); + tessellator.addVertexWithUV(this.left, this.bottom, 0.0D, 0.0F, 1.0F); + tessellator.addVertexWithUV(this.right, this.bottom, 0.0D, 1.0F, 1.0F); + tessellator.setColorRGBA(0, 0, 0, 0); + tessellator.addVertexWithUV(this.right, (double) this.bottom - TOP_PADDING, 0.0D, 1.0F, 0.0F); + tessellator.addVertexWithUV(this.left, (double) this.bottom - TOP_PADDING, 0.0D, 0.0F, 0.0F); + tessellator.draw(); + + // Scrollbar + int maxScroll = this.getMaxScroll(); + if (maxScroll > 0) { + int visibleHeight = this.bottom - this.top; + int contentHeight = this.getMaxPosition(); + + int thumbHeight = (int) ((float) visibleHeight * visibleHeight / contentHeight); + if (thumbHeight < 32) { + thumbHeight = 32; + } else if (thumbHeight > visibleHeight - 8) { + thumbHeight = visibleHeight - 8; + } + + int thumbY = (int) this.getScrollAmount() * (visibleHeight - thumbHeight) / maxScroll + this.top; + if (thumbY < this.top) { + thumbY = this.top; + } + + // Scrollbar track + tessellator.startDrawingQuads(); + tessellator.setColorOpaque(0, 0, 0); + tessellator.addVertexWithUV(scrollbarX, this.bottom, 0.0D, 0.0F, 1.0F); + tessellator.addVertexWithUV(scrollbarEndX, this.bottom, 0.0D, 1.0F, 1.0F); + tessellator.addVertexWithUV(scrollbarEndX, this.top, 0.0D, 1.0F, 0.0F); + tessellator.addVertexWithUV(scrollbarX, this.top, 0.0D, 0.0F, 0.0F); + tessellator.draw(); + + // Scrollbar thumb (inner) + tessellator.startDrawingQuads(); + tessellator.setColorOpaque(128, 128, 128); + tessellator.addVertexWithUV(scrollbarX, (double) thumbY + thumbHeight, 0.0D, 0.0F, 1.0F); + tessellator.addVertexWithUV(scrollbarEndX, (double) thumbY + thumbHeight, 0.0D, 1.0F, 1.0F); + tessellator.addVertexWithUV(scrollbarEndX, thumbY, 0.0D, 1.0F, 0.0F); + tessellator.addVertexWithUV(scrollbarX, thumbY, 0.0D, 0.0F, 0.0F); + tessellator.draw(); + + // Scrollbar thumb border + tessellator.startDrawingQuads(); + tessellator.setColorOpaque(192, 192, 192); + tessellator.addVertexWithUV(scrollbarX, thumbY + thumbHeight - 1.0, 0.0D, 0.0F, 1.0F); + tessellator.addVertexWithUV(scrollbarEndX - 1.0, thumbY + thumbHeight - 1.0, 0.0D, 1.0F, 1.0F); + tessellator.addVertexWithUV(scrollbarEndX - 1.0, thumbY, 0.0D, 1.0F, 0.0F); + tessellator.addVertexWithUV(scrollbarX, thumbY,0.0D, 0.0F, 0.0F); + tessellator.draw(); + } + + // Extra decorations hook + this.renderDecorations(mouseX, mouseY); + + // Restore GL state + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_DEPTH_TEST); + } + + @SuppressWarnings("unused") + protected void centerScrollOn(E entry) { + int index = this.children().indexOf(entry); + if (index < 0) return; // entry not found + + int listHeight = this.bottom - this.top; + int entryTop = getRowTop(index); + int entryCenter = entryTop + (this.itemHeight / 2); + + double newScroll = entryCenter - (listHeight / 2.0); + this.setScrollAmount(newScroll); + } + + protected void ensureVisible(E entry) { + int rowIndex = this.children().indexOf(entry); + if (rowIndex < 0) { + return; + } + + int rowTop = this.getRowTop(rowIndex); + + // How far the row is above the visible area (negative = off the top) + int overshootAbove = rowTop - this.top - TOP_PADDING - this.itemHeight; + if (overshootAbove < 0) { + this.applyScrollDelta(overshootAbove); + } + + // How far the row is below the visible area (negative = off the bottom) + int overshootBelow = this.bottom - rowTop - this.itemHeight - this.itemHeight; + if (overshootBelow < 0) { + this.applyScrollDelta(-overshootBelow); + } + } + + + private void applyScrollDelta(double deltaY) { + if (deltaY == 0) { + return; + } + this.setScrollAmount(this.getScrollAmount() + deltaY); + } + + public double getScrollAmount() { + return this.scrollAmount; + } + + public void setScrollAmount(double value) { + this.scrollAmount = MathHelper.clamp(value, 0, this.getMaxScroll()); + } + + private int getMaxScroll() { + return Math.max(0, this.getMaxPosition() - (this.bottom - this.top - TOP_PADDING)); + } + + protected int getScrollbarPosition() { + int halfList = this.getRowWidth() / 2; + int centerX = this.left + this.width / 2; + return centerX + halfList; + } + + @Override + public void mouseClicked(int mouseX, int mouseY, int button) { + // Ignore clicks outside the list bounds + if (!this.isMouseOver(mouseX, mouseY)) { + return; + } + + E clickedEntry = this.getEntryAtPosition(mouseX, mouseY); + if (clickedEntry != null) { + // Only change focus/selection if it's a different entry + if (this.focused == null || this.focused != clickedEntry) { + this.focused = clickedEntry; + this.selected = clickedEntry; + super.mouseClicked(mouseX, mouseY, button); + } + } else if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT) { + // Left-click on header area + int headerClickX = (int) (mouseX - (this.left + this.width / 2.0 - this.getRowWidth() / 2.0)); + int headerClickY = (int) (mouseY - (double) this.top) + (int) this.getScrollAmount() - TOP_PADDING; + this.onHeaderClicked(headerClickX, headerClickY); + } + } + + + @SuppressWarnings("unused") + public boolean mouseReleased(double mouseX, double mouseY, int button) { + E theFocused = this.getFocused(); + if (theFocused != null) { + return theFocused.mouseReleased(mouseX, mouseY, button); + } + return false; + } + + @SuppressWarnings({"UnusedReturnValue", "unused"}) + public boolean mouseDragged(double mouseX, double mouseY, int mouseButton, double deltaX, double deltaY) { + if (mouseButton == GLFW.GLFW_MOUSE_BUTTON_LEFT && isMouseOver(mouseX, mouseY)) { + int maxScroll = this.getMaxScroll(); + if (maxScroll <= 0) { + return false; + } + + int visibleHeight = this.bottom - this.top; + int contentHeight = this.getMaxPosition(); + + // Same logic as in render() for thumb height + int thumbHeight = (int) ((float) visibleHeight * visibleHeight / contentHeight); + if (thumbHeight < 32) { + thumbHeight = 32; + } else if (thumbHeight > visibleHeight - 8) { + thumbHeight = visibleHeight - 8; + } + + int scrollTrack = visibleHeight - thumbHeight; + if (scrollTrack <= 0) { + return false; + } + + // Map drag distance in screen space to scroll distance in content space + double scrollChange = deltaY * maxScroll / scrollTrack; + + // Sign is chosen so it matches the now-correct wheel direction + this.setScrollAmount(this.getScrollAmount() + scrollChange); + return true; + } + return false; + } + + @SuppressWarnings({"UnusedReturnValue", "unused"}) + public boolean mouseScrolled(double mouseX, double mouseY, double scrollDelta) { + // scrollDelta is Mouse.getDWheel(), e.g. ±120 or ±1 depending on the platform. + // We mimic ScreenOptions: scrollAmount += dWheel / -0.05F; + double amount = scrollDelta / -0.05D; + + // Apply to our scrollAmount + this.setScrollAmount(this.getScrollAmount() + amount); + return true; + } + + @SuppressWarnings("UnusedReturnValue") + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + // First let the focused entry handle it + E theFocused = this.getFocused(); + if (theFocused != null && theFocused.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } + // Handle up/down arrow movement + if (keyCode == GLFW.GLFW_KEY_DOWN) { + this.moveSelection(1); + return true; + } + if (keyCode == GLFW.GLFW_KEY_UP) { + this.moveSelection(-1); + return true; + } + return false; + } + + protected void moveSelection(int direction) { + if (!this.children().isEmpty()) { + int currentIndex = this.children().indexOf(this.getSelected()); + int newIndex = MathHelper.clamp(currentIndex + direction, 0, getItemCount() - 1); + + E entry = this.children().get(newIndex); + this.setSelected(entry); + this.ensureVisible(entry); + } + } + + public boolean isMouseOver(double mouseX, double mouseY) { + return mouseY >= this.top && mouseY <= this.bottom + && mouseX >= this.left && mouseX <= this.right; + } + + @SuppressWarnings("java:S1172") + protected void renderList(int rowLeft, int baseRowTop, int mouseX, int mouseY, float delta) { + int itemCount = this.getItemCount(); + Tessellator tessellator = Tessellator.instance; + + for (int rowIndex = 0; rowIndex < itemCount; ++rowIndex) { + int rowTop = this.getRowTop(rowIndex); + int rowBottom = this.getRowBottom(rowIndex); + + // Only render rows that are visible in the current viewport + if (rowBottom >= this.top && rowTop <= this.bottom) { + int entryTop = baseRowTop + rowIndex * this.itemHeight + this.headerHeight; + int rowHeightInner = this.itemHeight - TOP_PADDING; + E entry = this.getEntry(rowIndex); + int rowWidth = this.getRowWidth(); + + if (this.renderSelection && this.isSelectedItem(rowIndex)) { + int selectionLeft = this.left + this.width / 2 - rowWidth / 2; + int selectionRight = this.left + this.width / 2 + rowWidth / 2; + + GL11.glDisable(GL11.GL_TEXTURE_2D); + float selectionBrightness = this.isFocused() ? 1.0F : 0.5F; + GL11.glColor4f(selectionBrightness, selectionBrightness, selectionBrightness, 1f); + + // Outer selection box + tessellator.startDrawingQuads(); + tessellator.addVertex(selectionLeft, entryTop + rowHeightInner + 2.0, 0.0D); + tessellator.addVertex(selectionRight, entryTop + rowHeightInner + 2.0, 0.0D); + tessellator.addVertex(selectionRight, entryTop - 2.0, 0.0D); + tessellator.addVertex(selectionLeft, entryTop - 2.0, 0.0D); + tessellator.draw(); + + // Inner border + GL11.glColor4f(0f, 0f, 0f, 1f); + tessellator.startDrawingQuads(); + tessellator.addVertex(selectionLeft + 1.0, entryTop + rowHeightInner + 1.0, 0.0D); + tessellator.addVertex(selectionRight - 1.0, entryTop + rowHeightInner + 1.0, 0.0D); + tessellator.addVertex(selectionRight - 1.0, entryTop - 1.0, 0.0D); + tessellator.addVertex(selectionLeft + 1.0, entryTop - 1.0, 0.0D); + tessellator.draw(); + + GL11.glEnable(GL11.GL_TEXTURE_2D); } - } else if (i == 0) { - this.clickedHeader((int)(d - (double)(this.left + this.width / 2 - this.getRowWidth() / 2)), (int)(e - (double)this.top) + (int)this.getScrollAmount() - 4); - } - } - } - - public boolean mouseReleased(double d, double e, int i) { - if (this.getFocused() != null) { - this.getFocused().mouseReleased(d, e, i); - } - return false; - } - - double oldY = -1; - public boolean mouseDragged(double mouseX, double mouseY, int mouseButton, double mouseDX, double mouseDY) { - if (mouseButton == 0 && isMouseOver(mouseX, mouseY)) { - setScrollAmount(getScrollAmount() - mouseY + oldY); - return true; - } else { - return false; - } - } - - public boolean mouseScrolled(double d, double e, double f) { - this.setScrollAmount(this.getScrollAmount() - f * (double)this.itemHeight / 2.0D); - return true; - } - - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (this.getFocused() != null && this.getFocused().keyPressed(keyCode, scanCode, modifiers)) { - return true; - } else if (keyCode == 264) { - this.moveSelection(1); - return true; - } else if (keyCode == 265) { - this.moveSelection(-1); - return true; - } else { - return false; - } - } - - protected void moveSelection(int i) { - if (!this.children().isEmpty()) { - int j = this.children().indexOf(this.getSelected()); - int k = j + i; - if (k < 0) - k = 0; - else if (k > getItemCount() - 1) - k = getItemCount() - 1; - E entry = this.children().get(k); - this.setSelected(entry); - this.ensureVisible(entry); - } - - } - - public boolean isMouseOver(double d, double e) { - return e >= (double)this.top && e <= (double)this.bottom && d >= (double)this.left && d <= (double)this.right; - } - - protected void renderList(int i, int j, int k, int l, float f) { - int m = this.getItemCount(); - Tessellator tessellator = Tessellator.instance; - - for(int n = 0; n < m; ++n) { - int o = this.getRowTop(n); - int p = this.getRowBottom(n); - if (p >= this.top && o <= this.bottom) { - int q = j + n * this.itemHeight + this.headerHeight; - int r = this.itemHeight - 4; - E entry = this.getEntry(n); - int s = this.getRowWidth(); - int v; - if (this.renderSelection && this.isSelectedItem(n)) { - v = this.left + this.width / 2 - s / 2; - int u = this.left + this.width / 2 + s / 2; - GL11.glDisable(GL11.GL_TEXTURE_2D); - float g = this.isFocused() ? 1.0F : 0.5F; - GL11.glColor4f(g, g, g, 1f); - tessellator.startDrawingQuads(); - tessellator.addVertex(v, q + r + 2, 0.0D); - tessellator.addVertex(u, q + r + 2, 0.0D); - tessellator.addVertex(u, q - 2, 0.0D); - tessellator.addVertex(v, q - 2, 0.0D); - tessellator.draw(); - GL11.glColor4f(0f, 0f, 0f, 1f); - tessellator.startDrawingQuads(); - tessellator.addVertex(v + 1, q + r + 1, 0.0D); - tessellator.addVertex(u - 1, q + r + 1, 0.0D); - tessellator.addVertex(u - 1, q - 1, 0.0D); - tessellator.addVertex(v + 1, q - 1, 0.0D); - tessellator.draw(); - GL11.glEnable(GL11.GL_TEXTURE_2D); - } - - v = this.getRowLeft(); - entry.render(n, o, v, s, r, k, l, this.isMouseOver(k, l) && Objects.equals(this.getEntryAtPosition(k, l), entry), f); - } - } - - } - - protected int getRowLeft() { - return this.left + this.width / 2 - this.getRowWidth() / 2 + 2; - } - - protected int getRowTop(int i) { - return this.top + 4 - (int)this.getScrollAmount() + i * this.itemHeight + this.headerHeight; - } - - private int getRowBottom(int i) { - return this.getRowTop(i) + this.itemHeight; - } - - protected boolean isFocused() { - return false; - } - - protected void renderHoleBackground(int i, int j, int k, int l) { - Tessellator tessellator = Tessellator.instance; - this.minecraft.textureManager.bindTexture(this.minecraft.textureManager.loadTexture("/gui/background.png")); - GL11.glColor4f(1f, 1f, 1f, 1f); - float f = 32.0F; - tessellator.startDrawingQuads(); - tessellator.setColorRGBA(64, 64, 64, l); - tessellator.addVertexWithUV(this.left, j, 0.0D, 0.0F, (float)j / 32.0F); - tessellator.addVertexWithUV(this.left + this.width, j, 0.0D, (float)this.width / 32.0F, (float)j / 32.0F); - tessellator.setColorRGBA(64, 64, 64, k); - tessellator.addVertexWithUV(this.left + this.width, i, 0.0D, (float)this.width / 32.0F, (float)i / 32.0F); - tessellator.addVertexWithUV(this.left, i, 0.0D, 0.0F, (float)i / 32.0F); - tessellator.draw(); - } - - protected E remove(int i) { - E entry = this.children.get(i); - return this.removeEntry(this.children.get(i)) ? entry : null; - } - - protected boolean removeEntry(E entry) { - boolean bl = this.children.remove(entry); - if (bl && entry == this.getSelected()) { - this.setSelected(null); - } - - return bl; - } - - @Environment(EnvType.CLIENT) - class Entries extends AbstractList { - private final List entries; - - private Entries() { - this.entries = Lists.newArrayList(); - } - - public E get(int i) { - return this.entries.get(i); - } - - public int size() { - return this.entries.size(); - } - - public E set(int i, E entry) { - E entry2 = this.entries.set(i, entry); - entry.list = EntryListWidget.this; - return entry2; - } - - public void add(int i, E entry) { - this.entries.add(i, entry); - entry.list = EntryListWidget.this; - } - - public E remove(int i) { - return this.entries.remove(i); - } - } - - @Environment(EnvType.CLIENT) - public abstract static class Entry> extends Screen { - @Deprecated - EntryListWidget list; - - public Entry() { - } - - public abstract void render(int i, int j, int k, int l, int m, int n, int o, boolean bl, float f); - - public boolean isMouseOver(double d, double e) { - return Objects.equals(this.list.getEntryAtPosition(d, e), this); - } - - public void mouseMoved(double d, double e) { - } - - public void mouseClicked(int d, int e, int f) { - } - - public boolean mouseReleased(double d, double e, int i) { - return false; - } - - public boolean mouseScrolled(double d, double e, double f) { - return false; - } - - public boolean keyPressed(int i, int j, int k) { - return false; - } - - public boolean keyReleased(int i, int j, int k) { - return false; - } - - public boolean charTyped(char c, int i) { - return false; - } - - public boolean changeFocus(boolean bl) { - return false; - } - - } + + int entryLeft = this.getRowLeft(); + boolean hovered = this.isMouseOver(mouseX, mouseY) + && Objects.equals(this.getEntryAtPosition(mouseX, mouseY), entry); + + entry.render( + rowIndex, + rowTop, + entryLeft, + rowWidth, + rowHeightInner, + mouseX, + mouseY, + hovered, + delta + ); + } + } + } + + protected int getRowLeft() { + return this.left + this.width / 2 - this.getRowWidth() / 2 + 2; + } + + protected int getRowTop(int rowIndex) { + return this.top + TOP_PADDING - (int)this.getScrollAmount() + rowIndex * this.itemHeight + this.headerHeight; + } + + int getRowBottom(int rowIndex) { + return this.getRowTop(rowIndex) + this.itemHeight; + } + + protected boolean isFocused() { + return false; + } + + @SuppressWarnings("SameParameterValue") + protected void renderHoleBackground(int topY, int bottomY, int topAlpha, int bottomAlpha) { + Tessellator tessellator = Tessellator.instance; + this.minecraft.textureManager.bindTexture( + this.minecraft.textureManager.loadTexture("/gui/background.png") + ); + + GL11.glColor4f(1f, 1f, 1f, 1f); + + tessellator.startDrawingQuads(); + + // bottom fade + tessellator.setColorRGBA(64, 64, 64, bottomAlpha); + tessellator.addVertexWithUV(this.left, bottomY, 0.0D, 0.0F, bottomY / 32.0F); + tessellator.addVertexWithUV((double) this.left + this.width, bottomY, 0.0D, this.width / 32.0F, bottomY / 32.0F); + + // top fade + tessellator.setColorRGBA(64, 64, 64, topAlpha); + tessellator.addVertexWithUV((double) this.left + this.width, topY, 0.0D, this.width / 32.0F, topY / 32.0F); + tessellator.addVertexWithUV(this.left, topY, 0.0D, 0.0F, topY / 32.0F); + + tessellator.draw(); + } + + + protected E remove(int index) { + E entry = this.children.get(index); + return this.removeEntry(entry) ? entry : null; + } + + protected boolean removeEntry(E entry) { + boolean removed = this.children.remove(entry); + if (removed && entry == this.getSelected()) { + this.setSelected(null); + } + return removed; + } + + @Environment(EnvType.CLIENT) + class EntryList extends AbstractList { + private final List entries; + + private EntryList() { + this.entries = Lists.newArrayList(); + } + @Override + public E get(int index) { + return this.entries.get(index); + } + @Override + public int size() { + return this.entries.size(); + } + + @Override + public E set(int index, E entry) { + return this.entries.set(index, entry); + } + + @Override + public void add(int index, E entry) { + this.entries.add(index, entry); + } + + @Override + public E remove(int index) { + return this.entries.remove(index); + } + } + + @SuppressWarnings("unused") + @Environment(EnvType.CLIENT) + public abstract static class Entry> extends Screen { + protected Entry() {} + + public abstract void render( + int index, + int rowTop, + int rowLeft, + int rowWidth, + int rowHeight, + int mouseX, + int mouseY, + boolean hovered, + float delta + ); + + @SuppressWarnings("unused") + public void mouseMoved(double mouseX, double mouseY) {} + + @Override + public void mouseClicked(int mouseX, int mouseY, int button) {} + + @SuppressWarnings({"UnusedReturnValue", "unused"}) + public boolean mouseReleased(double mouseX, double mouseY, int button) { + return false; + } + + @SuppressWarnings({"java:S1172", "unused"}) + public boolean mouseScrolled(double mouseX, double mouseY, double scrollAmount) { + return false; + } + + @SuppressWarnings("java:S1172") + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + return false; + } + + @SuppressWarnings({"java:S1172", "unused"}) + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + return false; + } + + @SuppressWarnings({"java:S1172", "unused"}) + public boolean charTyped(char character, int keyCode) { + return false; + } + + @SuppressWarnings({"java:S1172", "unused"}) + public boolean changeFocus(boolean moveForward) { + return false; + } + } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModListEntry.java b/src/main/java/io/github/prospector/modmenu/gui/ModListEntry.java index 1e48ad3f3..2d5a37206 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListEntry.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModListEntry.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.gui; - import io.github.prospector.modmenu.ModMenu; import io.github.prospector.modmenu.util.BadgeRenderer; import io.github.prospector.modmenu.util.HardcodedUtil; @@ -20,128 +19,171 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Objects; public class ModListEntry extends AlwaysSelectedEntryListWidget.Entry { - public static final String UNKNOWN_ICON = "/gui/unknown_pack.png"; - private static final Logger LOGGER = LoggerFactory.getLogger(ModMenu.MOD_ID); - - protected final Minecraft client; - protected final ModContainer container; - protected final ModMetadata metadata; - protected final ModListWidget list; - protected Integer iconLocation; - - public ModListEntry(Minecraft mc, ModContainer container, ModListWidget list) { - this.container = container; - this.list = list; - this.metadata = container.getMetadata(); - this.client = mc; - } - - @Override - public void render(int index, int y, int x, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean isSelected, float delta) { - x += getXOffset(); - rowWidth -= getXOffset(); - GL11.glColor4f(1f, 1f, 1f, 1f); - this.bindIconTexture(); - internalRender(y, x); - String name = metadata.getName(); - if (name.equals("Minecraft")){ // BAD CODE - name = "Better than Adventure"; - } - name = HardcodedUtil.formatFabricModuleName(name); - String trimmedName = name; - int maxNameWidth = rowWidth - 32 - 3; - Font font = this.client.font; - trimmedName = ModListScreen.getString(font, name, trimmedName, maxNameWidth); - font.drawString(trimmedName, x + 32 + 3, y + 1, 0xFFFFFF); - new BadgeRenderer(client, x + 32 + 3 + font.getStringWidth(name) + 2, y, x + rowWidth, container, list.getParent()).draw(mouseX, mouseY); - String description = metadata.getDescription(); - if (description.isEmpty() && HardcodedUtil.getHardcodedDescriptions().containsKey(metadata.getId())) { - description = HardcodedUtil.getHardcodedDescription(metadata.getId()); - } - RenderUtils.INSTANCE.drawWrappedString(font, description, (x + 32 + 3 + 4), (y + 9 + 2), rowWidth - 32 - 7, 2, 0x808080); - } - - static void internalRender(int y, int x) { + public static final String UNKNOWN_ICON = "/gui/unknown_pack.png"; + private static final Logger LOGGER = LoggerFactory.getLogger(ModMenu.MOD_ID); + + protected final Minecraft client; + protected final ModContainer container; + protected final ModMetadata metadata; + protected final ModListWidget list; + protected Integer iconLocation; + + public ModListEntry(Minecraft client, ModContainer container, ModListWidget list) { + this.container = container; + this.list = list; + this.metadata = container.getMetadata(); + this.client = client; + } + + @Override + public void render(int index, int rowTop, int rowLeft, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean isSelected, float delta) { + int offsetX = getXOffset(); + rowLeft += offsetX; + rowWidth -= offsetX; + + GL11.glColor4f(1f, 1f, 1f, 1f); + bindIconTexture(); + drawIconQuad(rowLeft, rowTop); + + String displayName = getDisplayName(); + displayName = HardcodedUtil.formatFabricModuleName(displayName); + + Font font = this.client.font; + int maxNameWidth = rowWidth - 32 - 3; + String trimmedName = ModListScreen.getString(font, displayName, maxNameWidth); + + int nameX = rowLeft + 32 + 3; + int nameY = rowTop + 1; + font.drawString(trimmedName, nameX, nameY, 0xFFFFFF); + + int badgeStartX = nameX + font.getStringWidth(trimmedName) + 2; + new BadgeRenderer(client, badgeStartX, rowTop, rowLeft + rowWidth, container, list.getParent()) + .draw(mouseX, mouseY); + + String description = metadata.getDescription(); + if (description.isEmpty() && HardcodedUtil.getHardcodedDescriptions().containsKey(metadata.getId())) { + description = HardcodedUtil.getHardcodedDescription(metadata.getId()); + } + + int descX = nameX + 4; + int descY = rowTop + 9 + 2; + int descWidth = rowWidth - 32 - 7; + RenderUtils.INSTANCE.drawWrappedString(font, description, descX, descY, descWidth, 2, 0x808080); + } + + private String getDisplayName() { + String name = metadata.getName(); + // BTA-specific hardcoded rename + if ("Minecraft".equals(name)) { + return "Better than Adventure"; + } + return name; + } + + private static void drawIconQuad(int x, int y) { GL11.glEnable(GL11.GL_BLEND); Tessellator tess = Tessellator.instance; tess.startDrawingQuads(); tess.addVertexWithUV(x, y, 0, 0, 0); - tess.addVertexWithUV(x, y + 32, 0, 0, 1); - tess.addVertexWithUV(x + 32, y + 32, 0, 1, 1); - tess.addVertexWithUV(x + 32, y, 0, 1, 0); + tess.addVertexWithUV(x, y + 32.0, 0, 0, 1); + tess.addVertexWithUV(x + 32.0, y + 32.0, 0, 1, 1); + tess.addVertexWithUV(x + 32.0, y, 0, 1, 0); tess.draw(); GL11.glDisable(GL11.GL_BLEND); } private BufferedImage createIcon() { - try { - Path path = container.getPath(metadata.getIconPath(0).orElse("assets/" + metadata.getId() + "/icon.png")); - BufferedImage cached = this.list.getCachedModIcon(path); - if (cached != null) { - return cached; - } - if (!Files.exists(path)) { - ModContainer modMenu = FabricLoader.getInstance().getModContainer(ModMenu.MOD_ID).orElseThrow(IllegalAccessError::new); - if (HardcodedUtil.getFabricMods().contains(metadata.getId())) { - path = modMenu.getPath("assets/" + ModMenu.MOD_ID + "/fabric_icon.png"); - } else if (metadata.getId().equals("minecraft")) { - path = modMenu.getPath("assets/" + ModMenu.MOD_ID + "/mc_icon.png"); - } else if (metadata.getId().equals("java")) { - path = modMenu.getPath("assets/" + ModMenu.MOD_ID + "/java_icon.png"); - } else { - path = modMenu.getPath("assets/" + ModMenu.MOD_ID + "/grey_fabric_icon.png"); + try { + Path iconPath = resolveIconPath(); + if (iconPath == null) { + return null; + } + + BufferedImage cached = this.list.getCachedModIcon(iconPath); + if (cached != null) { + return cached; + } + + try (InputStream inputStream = Files.newInputStream(iconPath)) { + BufferedImage image = ImageIO.read(inputStream); + if (image == null) { + return null; + } + if (image.getHeight() != image.getWidth()) { + throw new IllegalStateException("Must be square icon"); } - } - cached = this.list.getCachedModIcon(path); - if (cached != null) { - return cached; - } - try (InputStream inputStream = Files.newInputStream(path)) { - BufferedImage image = ImageIO.read(Objects.requireNonNull(inputStream)); - if (image.getHeight() != image.getWidth()) - throw new IllegalStateException("Must be square icon"); - this.list.cacheModIcon(path, image); - return image; - } - - } catch (Throwable t) { - LOGGER.error("Invalid icon for mod {}", this.container.getMetadata().getName(), t); - return null; - } - } - - @Override - public void mouseClicked(int v, int v1, int i) { - list.select(this); - } - - public ModMetadata getMetadata() { - return metadata; - } - - public void bindIconTexture() { - if (this.iconLocation == null) { - BufferedImage icon = this.createIcon(); - if (icon != null) { - this.iconLocation = this.client.textureManager.loadBufferedTexture(icon).id(); - } else { - this.iconLocation = this.client.textureManager.loadTexture(UNKNOWN_ICON).id(); - } - } - this.client.textureManager.bindTexture(this.iconLocation); - } - - public void deleteTexture() { - if (iconLocation != null) { - this.client.textureManager.idToTextureMap.remove(iconLocation); - GL11.glDeleteTextures(iconLocation); - } - } - - public int getXOffset() { - return 0; - } + this.list.cacheModIcon(iconPath, image); + return image; + } + } catch (Exception e) { + LOGGER.error("Invalid icon for mod {}", this.metadata.getName(), e); + return null; + } + } + + @SuppressWarnings("java:S1075") + private Path resolveIconPath() { + // Try mod-provided icon first + Path path = container.findPath( + metadata.getIconPath(0).orElse("assets/" + metadata.getId() + "/icon.png") + ).orElse(null); + + if (path != null && Files.exists(path)) { + return path; + } + + // Fallback icons from Mod Menu + ModContainer modMenu = FabricLoader.getInstance() + .getModContainer(ModMenu.MOD_ID) + .orElseThrow(IllegalAccessError::new); + + String basePath = "assets/" + ModMenu.MOD_ID + "/"; + String fallback; + + if (HardcodedUtil.getFabricMods().contains(metadata.getId())) { + fallback = "fabric_icon.png"; + } else if ("minecraft".equals(metadata.getId())) { + fallback = "mc_icon.png"; + } else if ("java".equals(metadata.getId())) { + fallback = "java_icon.png"; + } else { + fallback = "grey_fabric_icon.png"; + } + + return modMenu.findPath(basePath + fallback).orElse(null); + } + + @Override + public void mouseClicked(int mouseX, int mouseY, int button) { + list.select(this); + } + + public ModMetadata getMetadata() { + return metadata; + } + + public void bindIconTexture() { + if (this.iconLocation == null) { + BufferedImage icon = this.createIcon(); + if (icon != null) { + this.iconLocation = this.client.textureManager.loadBufferedTexture(icon).id(); + } else { + this.iconLocation = this.client.textureManager.loadTexture(UNKNOWN_ICON).id(); + } + } + this.client.textureManager.bindTexture(this.iconLocation); + } + + public void deleteTexture() { + if (iconLocation != null) { + this.client.textureManager.idToTextureMap.remove(iconLocation); + GL11.glDeleteTextures(iconLocation); + } + } + + public int getXOffset() { + return 0; + } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java index bfb511860..824717d08 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java @@ -34,476 +34,661 @@ import java.util.*; public class ModListScreen extends Screen { - private static final String FILTERS_BUTTON_LOCATION = "/assets/" + ModMenu.MOD_ID + "/textures/gui/filters_button.png"; - private static final String CONFIGURE_BUTTON_LOCATION = "/assets/" + ModMenu.MOD_ID + "/textures/gui/configure_button.png"; - private static final Logger LOGGER = LoggerFactory.getLogger("modlistscreen"); - private final String textTitle; - private TextFieldWidget searchBox; - private DescriptionListWidget descriptionListWidget; - private final Screen parent; - private ModListWidget modList; - private String tooltip; - private ModListEntry selected; - private BadgeRenderer badgeRenderer; - private double scrollPercent = 0; - private boolean showModCount = false; - private boolean init = false; - private boolean filterOptionsShown = false; - private int paneY; - private int paneWidth; - private int rightPaneX; - private int searchBoxX; - public Set showModChildren = new HashSet<>(); - private String lastSearchString = null; - - private static final int CONFIGURE_BUTTON_ID = 0; - private static final int WEBSITE_BUTTON_ID = 1; - private static final int ISSUES_BUTTON_ID = 2; - private static final int TOGGLE_FILTER_OPTIONS_BUTTON_ID = 3; - private static final int TOGGLE_SORT_MODE_BUTTON_ID = 4; - private static final int TOGGLE_SHOW_LIBRARIES_BUTTON_ID = 5; - private static final int MODS_FOLDER_BUTTON_ID = 6; - private static final int DONE_BUTTON_ID = 7; - - public ModListScreen(Screen previousGui) { - this.parent = previousGui; - this.textTitle = I18n.getInstance().translateKey("modmenu.title"); - } - - public void updateEvents() { - super.updateEvents(); - int dWheel = Mouse.getEventDWheel() / 50; - if (dWheel != 0) { - int mouseX = Mouse.getEventX() * this.width / this.mc.resolution.getScaledHeightScreenCoords(); // field_6326_c - int mouseY = this.height - Mouse.getEventY() * this.height / this.mc.resolution.getScaledHeightScreenCoords() - 1; // field_6325_d - mouseScrolled(mouseX, mouseY, dWheel); - } - } - - public void mouseScrolled(double double_1, double double_2, double double_3) { - if (modList.isMouseOver(double_1, double_2)) - this.modList.mouseScrolled(double_1, double_2, double_3); - if (descriptionListWidget.isMouseOver(double_1, double_2)) - this.descriptionListWidget.mouseScrolled(double_1, double_2, double_3); - } - - @Override - public void tick() { - this.searchBox.updateCursorCounter(); - } - - @Override - public void init() { - I18n i18n = I18n.getInstance(); - Keyboard.enableRepeatEvents(true); - Font font = this.font; - paneY = 48; - paneWidth = this.width / 2 - 8; - rightPaneX = width - paneWidth; - - int searchBoxWidth = paneWidth - 32 - 22; - searchBoxX = paneWidth / 2 - searchBoxWidth / 2 - 22 / 2; - String oldText = this.searchBox == null ? "" : this.searchBox.getText(); - this.searchBox = new TextFieldWidget(this.font, searchBoxX, 22, searchBoxWidth, 20, i18n.translateKey("modmenu.search")); // field_6451_g - this.searchBox.setText(oldText); - this.modList = new ModListWidget(this.mc, paneWidth, this.height, paneY + 19, this.height - 36, 36, this.searchBox.getText(), this.modList, this); - this.modList.setLeftPos(0); - this.descriptionListWidget = new DescriptionListWidget(this.mc, paneWidth, this.height, paneY + 60, this.height - 36, 9 + 1, this); - this.descriptionListWidget.setLeftPos(rightPaneX); - ButtonElement configureButton = new ModMenuTexturedButtonWidget(CONFIGURE_BUTTON_ID, width - 24, paneY, 20, 20, 0, 0, CONFIGURE_BUTTON_LOCATION, 32, 64) { - - @Override - public void render(Minecraft mc, int mouseX, int mouseY) { - if (selected != null) { - String modid = selected.getMetadata().getId(); - enabled = ModMenu.hasConfigScreenFactory(modid) || ModMenu.hasLegacyConfigScreenTask(modid); - } else { - enabled = false; - } - visible = enabled; // visible = enabled - GL11.glColor4f(1f, 1f, 1f, 1f); - super.render(mc, mouseX, mouseY); - } - }; - int urlButtonWidths = paneWidth / 2 - 2; - int cappedButtonWidth = Math.min(urlButtonWidths, 200); - ButtonElement websiteButton = new ButtonElement(WEBSITE_BUTTON_ID, rightPaneX + (urlButtonWidths / 2) - (cappedButtonWidth / 2), paneY + 36, Math.min(urlButtonWidths, 200), 20, i18n.translateKey("modmenu.website")) { - @Override - public void drawButton(Minecraft mc, int var1, int var2) { - visible = selected != null; // visible = selected != null - enabled = visible && selected.getMetadata().getContact().get("homepage").isPresent(); - super.drawButton(mc, var1, var2); - } - }; - ButtonElement issuesButton = new ButtonElement(ISSUES_BUTTON_ID, rightPaneX + urlButtonWidths + 4 + (urlButtonWidths / 2) - (cappedButtonWidth / 2), paneY + 36, Math.min(urlButtonWidths, 200), 20, i18n.translateKey("modmenu.issues")) { - @Override - public void drawButton(Minecraft mc, int var1, int var2) { - visible = selected != null; // visible = selected != null - enabled = visible && selected.getMetadata().getContact().get("issues").isPresent(); - super.drawButton(mc, var1, var2); - } - }; - this.buttons.add(new ModMenuTexturedButtonWidget(TOGGLE_FILTER_OPTIONS_BUTTON_ID, paneWidth / 2 + searchBoxWidth / 2 - 20 / 2 + 2, 22, 20, 20, 0, 0, FILTERS_BUTTON_LOCATION, 32, 64) { - @Override - public void render(Minecraft mc, int int_1, int int_2) { - super.render(mc, int_1, int_2); - if (isHovered(int_1, int_2)) { - setTooltip(i18n.translateKey("modmenu.toggleFilterOptions")); - } - } - }); - String showLibrariesText = i18n.translateKeyAndFormat("modmenu.showLibraries", i18n.translateKey("modmenu.showLibraries." + ModMenuConfigManager.getConfig().showLibraries())); - String sortingText = i18n.translateKeyAndFormat("modmenu.sorting", ModMenuConfigManager.getConfig().getSorting().getName()); - int showLibrariesWidth = font.getStringWidth(showLibrariesText) + 20; - int sortingWidth = font.getStringWidth(sortingText) + 20; - int filtersX; - int filtersWidth = showLibrariesWidth + sortingWidth + 2; - if ((filtersWidth + font.getStringWidth(i18n.translateKeyAndFormat("modmenu.showingMods", NumberFormat.getInstance().format(modList.getDisplayedCount()) + "/" + NumberFormat.getInstance().format(FabricLoader.getInstance().getAllMods().size()))) + 20) >= searchBoxX + searchBoxWidth + 22) { - filtersX = paneWidth / 2 - filtersWidth / 2; - showModCount = false; - } else { - filtersX = searchBoxX + searchBoxWidth + 22 - filtersWidth + 1; - showModCount = true; - } - this.buttons.add(new ButtonElement(TOGGLE_SORT_MODE_BUTTON_ID, filtersX, 45, sortingWidth, 20, sortingText) { - @Override - public void drawButton(Minecraft mc, int mouseX, int mouseY) { - visible = enabled = filterOptionsShown; - this.displayString = i18n.translateKeyAndFormat("modmenu.sorting", ModMenuConfigManager.getConfig().getSorting().getName()); - super.drawButton(mc, mouseX, mouseY); - } - }); - this.buttons.add(new ButtonElement(TOGGLE_SHOW_LIBRARIES_BUTTON_ID, filtersX + sortingWidth + 2, 45, showLibrariesWidth, 20, showLibrariesText) { - @Override - public void drawButton(Minecraft mc, int mouseX, int mouseY) { - visible = enabled = filterOptionsShown; - this.displayString = i18n.translateKeyAndFormat("modmenu.showLibraries", i18n.translateKey("modmenu.showLibraries." + ModMenuConfigManager.getConfig().showLibraries())); - super.drawButton(mc, mouseX, mouseY); - } - }); - this.buttons.add(configureButton); - this.buttons.add(websiteButton); - this.buttons.add(issuesButton); - this.buttons.add(ButtonUtil.createButton(MODS_FOLDER_BUTTON_ID, this.width / 2 - 154, this.height - 28, 150, 20, "Open Mods Folder")); - this.buttons.add(ButtonUtil.createButton(DONE_BUTTON_ID, this.width / 2 + 4, this.height - 28, 150, 20, "Done")); - this.searchBox.setFocused(true); - - init = true; - } - - @Override - protected void buttonClicked(ButtonElement button) { - switch (button.id) { - case CONFIGURE_BUTTON_ID: { - final String modid = Objects.requireNonNull(selected).getMetadata().getId(); - final Screen screen = ModMenu.getConfigScreen(modid, this); - if (screen != null) { - mc.displayScreen(screen); - } else { - ModMenu.openConfigScreen(modid); - } - break; - } - case WEBSITE_BUTTON_ID: { - final ModMetadata metadata = Objects.requireNonNull(selected).getMetadata(); - metadata.getContact().get("homepage").ifPresent(Sys::openURL); - break; - } - case ISSUES_BUTTON_ID: { - final ModMetadata metadata = Objects.requireNonNull(selected).getMetadata(); - metadata.getContact().get("issues").ifPresent(Sys::openURL); - break; - } - case TOGGLE_FILTER_OPTIONS_BUTTON_ID: { - filterOptionsShown = !filterOptionsShown; - break; - } - case TOGGLE_SORT_MODE_BUTTON_ID: { - ModMenuConfigManager.getConfig().toggleSortMode(); - modList.reloadFilters(); - break; - } - case TOGGLE_SHOW_LIBRARIES_BUTTON_ID: { - ModMenuConfigManager.getConfig().toggleShowLibraries(); - modList.reloadFilters(); - break; - } - case MODS_FOLDER_BUTTON_ID: { - File modsFolder = new File(FabricLoader.getInstance().getGameDir().toFile(), "mods"); - try { - Sys.openURL(modsFolder.toURI().toURL().toString()); - } catch (MalformedURLException e) { - LOGGER.error("Malformed mods folder URL", e); - } - break; - } - case DONE_BUTTON_ID: { - mc.displayScreen(parent); - break; - } - } - } - - public ModListWidget getModList() { - return modList; - } - - @Override - public void keyPressed(char char_1, int int_1, int mouseX, int mouseY) { - this.searchBox.textboxKeyTyped(char_1, int_1); - if (int_1 == 1) { + private static final String FILTERS_BUTTON_LOCATION = + "/assets/" + ModMenu.MOD_ID + "/textures/gui/filters_button.png"; + private static final String CONFIGURE_BUTTON_LOCATION = + "/assets/" + ModMenu.MOD_ID + "/textures/gui/configure_button.png"; + private static final Logger LOGGER = LoggerFactory.getLogger("modlistscreen"); + + private final String textTitle; + private final Screen parent; + + private TextFieldWidget searchBox; + private DescriptionListWidget descriptionListWidget; + private ModListWidget modList; + private String tooltip; + private ModListEntry selected; + private BadgeRenderer badgeRenderer; + + private double scrollPercent = 0; + private boolean showModCount = false; + private boolean init = false; + private boolean filterOptionsShown = false; + private int paneY; + private int paneWidth; + private int rightPaneX; + private int searchBoxX; + private final Set showModChildren = new HashSet<>(); + private String lastSearchString = null; + + private static final int CONFIGURE_BUTTON_ID = 0; + private static final int WEBSITE_BUTTON_ID = 1; + private static final int ISSUES_BUTTON_ID = 2; + private static final int TOGGLE_FILTER_OPTIONS_BUTTON_ID = 3; + private static final int TOGGLE_SORT_MODE_BUTTON_ID = 4; + private static final int TOGGLE_SHOW_LIBRARIES_BUTTON_ID = 5; + private static final int MODS_FOLDER_BUTTON_ID = 6; + private static final int DONE_BUTTON_ID = 7; + + public ModListScreen(Screen previousGui) { + this.parent = previousGui; + this.textTitle = I18n.getInstance().translateKey("modmenu.title"); + } + + @Override + public void updateEvents() { + super.updateEvents(); + + int dWheel = Mouse.getDWheel(); + if (dWheel == 0) { + return; + } + + int mouseX = Mouse.getX() * this.width / this.mc.resolution.getWidthScreenCoords(); + int mouseY = this.height - Mouse.getY() * this.height / this.mc.resolution.getHeightScreenCoords() - 1; + + if (modList.isMouseOver(mouseX, mouseY)) { + // pass raw wheel delta + modList.mouseScrolled(mouseX, mouseY, dWheel); + } else if (descriptionListWidget.isMouseOver(mouseX, mouseY)) { + descriptionListWidget.mouseScrolled(mouseX, mouseY, dWheel); + } + } + + + + + @SuppressWarnings("unused") + public void mouseScrolled(double mouseX, double mouseY, double scrollDelta) { + if (modList.isMouseOver(mouseX, mouseY)) { + this.modList.mouseScrolled(mouseX, mouseY, scrollDelta); + } + if (descriptionListWidget.isMouseOver(mouseX, mouseY)) { + this.descriptionListWidget.mouseScrolled(mouseX, mouseY, scrollDelta); + } + } + + @Override + public void tick() { + this.searchBox.updateCursorCounter(); + } + + @Override + public void init() { + I18n i18n = I18n.getInstance(); + Keyboard.enableRepeatEvents(true); + Font font = this.font; + + paneY = 48; + paneWidth = this.width / 2 - 8; + rightPaneX = width - paneWidth; + + int searchBoxWidth = paneWidth - 32 - 22; + searchBoxX = paneWidth / 2 - searchBoxWidth / 2 - 22 / 2; + + String oldText = this.searchBox == null ? "" : this.searchBox.getText(); + this.searchBox = new TextFieldWidget(this.font, searchBoxX, 22, searchBoxWidth, 20, + i18n.translateKey("modmenu.search")); + this.searchBox.setText(oldText); + + this.modList = new ModListWidget(this.mc, paneWidth, this.height, + paneY + 19, this.height - 36, 36, + this.searchBox.getText(), this.modList, this); + this.modList.setLeftEdge(0); + + this.descriptionListWidget = new DescriptionListWidget(this.mc, paneWidth, this.height, + paneY + 60, this.height - 36, 9 + 1, this); + this.descriptionListWidget.setLeftEdge(rightPaneX); + + ButtonElement configureButton = new ModMenuTexturedButtonWidget( + CONFIGURE_BUTTON_ID, width - 24, paneY, 20, 20, + 0, 0, CONFIGURE_BUTTON_LOCATION, 32, 64 + ) { + @Override + public void render(Minecraft mc, int mouseX, int mouseY) { + if (selected != null) { + String modid = selected.getMetadata().getId(); + enabled = ModMenu.hasConfigScreenFactory(modid) + || ModMenu.hasLegacyConfigScreenTask(modid); + } else { + enabled = false; + } + visible = enabled; + GL11.glColor4f(1f, 1f, 1f, 1f); + super.render(mc, mouseX, mouseY); + } + }; + + int urlButtonWidths = paneWidth / 2 - 2; + int cappedButtonWidth = Math.min(urlButtonWidths, 200); + + ButtonElement websiteButton = new ButtonElement( + WEBSITE_BUTTON_ID, + rightPaneX + (urlButtonWidths / 2) - (cappedButtonWidth / 2), + paneY + 36, + cappedButtonWidth, 20, + i18n.translateKey("modmenu.website") + ) { + @Override + public void drawButton(Minecraft mc, int mouseX, int mouseY) { + visible = selected != null; + enabled = visible + && selected.getMetadata().getContact().get("homepage").isPresent(); + super.drawButton(mc, mouseX, mouseY); + } + }; + + ButtonElement issuesButton = new ButtonElement( + ISSUES_BUTTON_ID, + rightPaneX + urlButtonWidths + 4 + (urlButtonWidths / 2) - (cappedButtonWidth / 2), + paneY + 36, + cappedButtonWidth, 20, + i18n.translateKey("modmenu.issues") + ) { + @Override + public void drawButton(Minecraft mc, int mouseX, int mouseY) { + visible = selected != null; + enabled = visible + && selected.getMetadata().getContact().get("issues").isPresent(); + super.drawButton(mc, mouseX, mouseY); + } + }; + + this.buttons.add(new ModMenuTexturedButtonWidget( + TOGGLE_FILTER_OPTIONS_BUTTON_ID, + paneWidth / 2 + searchBoxWidth / 2 - 20 / 2 + 2, + 22, 20, 20, + 0, 0, FILTERS_BUTTON_LOCATION, 32, 64 + ) { + @Override + public void render(Minecraft mc, int mouseX, int mouseY) { + super.render(mc, mouseX, mouseY); + if (isHovered(mouseX, mouseY)) { + setTooltip(i18n.translateKey("modmenu.toggleFilterOptions")); + } + } + }); + + String showLibrariesText = i18n.translateKeyAndFormat( + "modmenu.showLibraries", + i18n.translateKey("modmenu.showLibraries." + ModMenuConfigManager.getConfig().showLibraries()) + ); + String sortingText = i18n.translateKeyAndFormat( + "modmenu.sorting", + ModMenuConfigManager.getConfig().getSorting().getName() + ); + int showLibrariesWidth = font.getStringWidth(showLibrariesText) + 20; + int sortingWidth = font.getStringWidth(sortingText) + 20; + + int filtersWidth = showLibrariesWidth + sortingWidth + 2; + int filtersX; + + String modCountString = i18n.translateKeyAndFormat( + "modmenu.showingMods", + NumberFormat.getInstance().format(modList.getDisplayedCount()) + + "/" + + NumberFormat.getInstance().format(FabricLoader.getInstance().getAllMods().size()) + ); + + if ((filtersWidth + font.getStringWidth(modCountString) + 20) + >= searchBoxX + searchBoxWidth + 22) { + filtersX = paneWidth / 2 - filtersWidth / 2; + showModCount = false; + } else { + filtersX = searchBoxX + searchBoxWidth + 22 - filtersWidth + 1; + showModCount = true; + } + + this.buttons.add(new ButtonElement( + TOGGLE_SORT_MODE_BUTTON_ID, + filtersX, 45, + sortingWidth, 20, + sortingText + ) { + @Override + public void drawButton(Minecraft mc, int mouseX, int mouseY) { + visible = enabled = filterOptionsShown; + this.displayString = i18n.translateKeyAndFormat( + "modmenu.sorting", + ModMenuConfigManager.getConfig().getSorting().getName() + ); + super.drawButton(mc, mouseX, mouseY); + } + }); + + this.buttons.add(new ButtonElement( + TOGGLE_SHOW_LIBRARIES_BUTTON_ID, + filtersX + sortingWidth + 2, 45, + showLibrariesWidth, 20, + showLibrariesText + ) { + @Override + public void drawButton(Minecraft mc, int mouseX, int mouseY) { + visible = enabled = filterOptionsShown; + this.displayString = i18n.translateKeyAndFormat( + "modmenu.showLibraries", + i18n.translateKey("modmenu.showLibraries." + + ModMenuConfigManager.getConfig().showLibraries()) + ); + super.drawButton(mc, mouseX, mouseY); + } + }); + + this.buttons.add(configureButton); + this.buttons.add(websiteButton); + this.buttons.add(issuesButton); + this.buttons.add(ButtonUtil.createButton( + MODS_FOLDER_BUTTON_ID, + this.width / 2 - 154, + this.height - 28, + 150, 20, + "Open Mods Folder" + )); + this.buttons.add(ButtonUtil.createButton( + DONE_BUTTON_ID, + this.width / 2 + 4, + this.height - 28, + 150, 20, + "Done" + )); + + this.searchBox.setFocused(true); + init = true; + } + + @SuppressWarnings("java:S131") + @Override + protected void buttonClicked(ButtonElement button) { + switch (button.id) { + case CONFIGURE_BUTTON_ID: { + String modid = Objects.requireNonNull(selected).getMetadata().getId(); + Screen screen = ModMenu.getConfigScreen(modid, this); + if (screen != null) { + mc.displayScreen(screen); + } else { + ModMenu.openConfigScreen(modid); + } + break; + } + case WEBSITE_BUTTON_ID: { + ModMetadata metadata = Objects.requireNonNull(selected).getMetadata(); + metadata.getContact().get("homepage").ifPresent(Sys::openURL); + break; + } + case ISSUES_BUTTON_ID: { + ModMetadata metadata = Objects.requireNonNull(selected).getMetadata(); + metadata.getContact().get("issues").ifPresent(Sys::openURL); + break; + } + case TOGGLE_FILTER_OPTIONS_BUTTON_ID: { + filterOptionsShown = !filterOptionsShown; + break; + } + case TOGGLE_SORT_MODE_BUTTON_ID: { + ModMenuConfigManager.getConfig().toggleSortMode(); + modList.reloadFilters(); + break; + } + case TOGGLE_SHOW_LIBRARIES_BUTTON_ID: { + ModMenuConfigManager.getConfig().toggleShowLibraries(); + modList.reloadFilters(); + break; + } + case MODS_FOLDER_BUTTON_ID: { + File modsFolder = new File(FabricLoader.getInstance().getGameDir().toFile(), "mods"); + try { + Sys.openURL(modsFolder.toURI().toURL().toString()); + } catch (MalformedURLException e) { + LOGGER.error("Malformed mods folder URL", e); + } + break; + } + case DONE_BUTTON_ID: { + mc.displayScreen(parent); + break; + } + } + } + + @SuppressWarnings("unused") + public ModListWidget getModList() { + return modList; + } + + @Override + public void keyPressed(char typedChar, int keyCode, int mouseX, int mouseY) { + this.searchBox.textboxKeyTyped(typedChar, keyCode); + if (keyCode == 1) { // ESC (Minecraft's internal keycode) this.mc.displayScreen(this.parent); } - modList.keyPressed(int_1, 0, 0); - descriptionListWidget.keyPressed(int_1, 0, 0); - } - - @Override - public void mouseClicked(int mouseX, int mouseY, int mouseButton) { - super.mouseClicked(mouseX, mouseY, mouseButton); - modList.mouseClicked(mouseX, mouseY, mouseButton); - descriptionListWidget.mouseClicked(mouseX, mouseY, mouseButton); - } - - @Override - public void mouseReleased(int mouseX, int mouseY, int mouseButton) { - super.mouseReleased(mouseX, mouseY, mouseButton); - if (mouseButton != -1) { - modList.mouseReleased(mouseX, mouseY, mouseButton); - descriptionListWidget.mouseReleased(mouseX, mouseY, mouseButton); - } - } - - @Override - public void render(int mouseX, int mouseY, float delta) { - I18n i18n = I18n.getInstance(); - int mouseDX = Mouse.getEventDX() * this.width / this.mc.resolution.getScaledWidthScreenCoords(); // field_6326_c - int mouseDY = this.height - Mouse.getEventDY() * this.height / this.mc.resolution.getScaledHeightScreenCoords() - 1; // field_6325_d - for (int button = 0; button < Mouse.getButtonCount(); button++) { - if (Mouse.isButtonDown(button)) { - modList.mouseDragged(mouseX, mouseY, button, mouseDX, mouseDY); - descriptionListWidget.mouseDragged(mouseX, mouseY, button, mouseDX, mouseDY); - } - } - Font font = this.font; - if (!searchBox.getText().equals(lastSearchString)) { - lastSearchString = searchBox.getText(); - modList.filter(lastSearchString, false); - } - overlayBackground(paneWidth, 0, rightPaneX, height, 64, 64, 64, 255, 255); - this.tooltip = null; - ModListEntry selectedEntry = selected; - if (selectedEntry != null) { - this.descriptionListWidget.render(mouseX, mouseY, delta); - } - this.modList.render(mouseX, mouseY, delta); - this.searchBox.drawTextBox(); - GL11.glDisable(GL11.GL_BLEND); - this.drawStringCentered(font, this.textTitle, this.modList.getWidth() / 2, 8, 0xffffff); - super.render(mouseX, mouseY, delta); - if (showModCount || !filterOptionsShown) { - String showModCountString = i18n.translateKeyAndFormat("modmenu.showingMods", NumberFormat.getInstance().format(modList.getDisplayedCount()) + "/" + NumberFormat.getInstance().format(FabricLoader.getInstance().getAllMods().size())); - font.drawString(showModCountString, searchBoxX, 52, 0xFFFFFF); - } - if (selectedEntry != null) { - ModMetadata metadata = selectedEntry.getMetadata(); - int x = rightPaneX; - GL11.glColor4f(1f, 1f, 1f, 1f); - this.selected.bindIconTexture(); - ModListEntry.internalRender(paneY, x); + modList.keyPressed(keyCode, 0, 0); + descriptionListWidget.keyPressed(keyCode, 0, 0); + } + + @Override + public void mouseClicked(int mouseX, int mouseY, int mouseButton) { + super.mouseClicked(mouseX, mouseY, mouseButton); + modList.mouseClicked(mouseX, mouseY, mouseButton); + descriptionListWidget.mouseClicked(mouseX, mouseY, mouseButton); + } + + @Override + public void mouseReleased(int mouseX, int mouseY, int mouseButton) { + super.mouseReleased(mouseX, mouseY, mouseButton); + if (mouseButton != -1) { + modList.mouseReleased(mouseX, mouseY, mouseButton); + descriptionListWidget.mouseReleased(mouseX, mouseY, mouseButton); + } + } + + @Override + public void render(int mouseX, int mouseY, float delta) { + I18n i18n = I18n.getInstance(); + + int mouseDX = Mouse.getDX() * this.width / this.mc.resolution.getWidthScreenCoords(); + int mouseDY = -Mouse.getDY() * this.height / this.mc.resolution.getHeightScreenCoords(); + + for (int button = 0; button < Mouse.getButtonCount(); button++) { + if (Mouse.isButtonDown(button)) { + modList.mouseDragged(mouseX, mouseY, button, mouseDX, mouseDY); + descriptionListWidget.mouseDragged(mouseX, mouseY, button, mouseDX, mouseDY); + } + } + + Font font = this.font; + + if (!searchBox.getText().equals(lastSearchString)) { + lastSearchString = searchBox.getText(); + modList.filter(lastSearchString, false); + } + + overlayBackground(paneWidth, 0, rightPaneX, height, 64, 64, 64, 255, 255); + this.tooltip = null; + + ModListEntry selectedEntry = selected; + if (selectedEntry != null) { + this.descriptionListWidget.render(mouseX, mouseY, delta); + } + + this.modList.render(mouseX, mouseY, delta); + this.searchBox.drawTextBox(); + + GL11.glDisable(GL11.GL_BLEND); + this.drawStringCentered(font, this.textTitle, this.modList.getWidth() / 2, 8, 0xffffff); + super.render(mouseX, mouseY, delta); + + if (showModCount || !filterOptionsShown) { + String showModCountString = i18n.translateKeyAndFormat( + "modmenu.showingMods", + NumberFormat.getInstance().format(modList.getDisplayedCount()) + + "/" + + NumberFormat.getInstance().format(FabricLoader.getInstance().getAllMods().size()) + ); + font.drawString(showModCountString, searchBoxX, 52, 0xFFFFFF); + } + + if (selectedEntry != null) { + ModMetadata metadata = selectedEntry.getMetadata(); + int x = rightPaneX; + + GL11.glColor4f(1f, 1f, 1f, 1f); + this.selected.bindIconTexture(); + drawIconQuad(x, paneY); // replaces ModListEntry.internalRender + int lineSpacing = 9 + 1; - int imageOffset = 36; - String name = metadata.getName(); - if (name.equals("Minecraft")){ // BAD CODE - name = "Better than Adventure"; - } - name = HardcodedUtil.formatFabricModuleName(name); - String trimmedName = name; - int maxNameWidth = this.width - (x + imageOffset); - trimmedName = getString(font, name, trimmedName, maxNameWidth); + int imageOffset = 36; + + String name = metadata.getName(); + if (name.equals("Minecraft")) { // BAD CODE + name = "Better than Adventure"; + } + name = HardcodedUtil.formatFabricModuleName(name); + + String trimmedName = getString(font, name, this.width - (x + imageOffset)); font.drawString(trimmedName, x + imageOffset, paneY + 1, 0xFFFFFF); - if (mouseX > x + imageOffset && mouseY > paneY + 1 && mouseY < paneY + 1 + 9 && mouseX < x + imageOffset + font.getStringWidth(trimmedName)) { - setTooltip(i18n.translateKeyAndFormat("modmenu.modIdToolTip", metadata.getId())); - } - if (init || badgeRenderer == null || badgeRenderer.getMetadata() != metadata) { - badgeRenderer = new BadgeRenderer(mc, x + imageOffset + font.getStringWidth(trimmedName) + 2, paneY, width - 28, selectedEntry.container, this); - init = false; - } - badgeRenderer.draw(mouseX, mouseY); - String versionString; - if (metadata.getName().equals("Minecraft")){ // BAD CODE - versionString = Global.VERSION; - } else { - versionString = metadata.getVersion().getFriendlyString(); - } - font.drawString("v" + versionString, x + imageOffset, paneY + 2 + lineSpacing, 0x808080); - String authors; - List names = new ArrayList<>(); - - metadata.getAuthors().stream() - .filter(Objects::nonNull) - .map(Person::getName) - .filter(Objects::nonNull) - .forEach(names::add); - - if (!names.isEmpty()) { - if (names.size() > 1) { - authors = Joiner.on(", ").join(names); - } else { - authors = names.get(0); - } - RenderUtils.INSTANCE.drawWrappedString(font, i18n.translateKeyAndFormat("modmenu.authorPrefix", authors), x + imageOffset, paneY + 2 + lineSpacing * 2, paneWidth - imageOffset - 4, 1, 0x808080); - } - if (this.tooltip != null) { - this.renderTooltip(Lists.newArrayList(Splitter.on("\n").split(this.tooltip)), mouseX, mouseY); - } - } - } - - static String getString(Font font, String name, String trimmedName, int maxNameWidth) { - if (font.getStringWidth(name) > maxNameWidth) { - int maxWidth = maxNameWidth - font.getStringWidth("..."); - trimmedName = ""; - while (font.getStringWidth(trimmedName) < maxWidth && trimmedName.length() < name.length()) { - trimmedName += name.charAt(trimmedName.length()); + + if (mouseX > x + imageOffset + && mouseY > paneY + 1 + && mouseY < paneY + 1 + 9 + && mouseX < x + imageOffset + font.getStringWidth(trimmedName)) { + setTooltip(i18n.translateKeyAndFormat("modmenu.modIdToolTip", metadata.getId())); + } + + if (init || badgeRenderer == null || badgeRenderer.getMetadata() != metadata) { + badgeRenderer = new BadgeRenderer( + mc, + x + imageOffset + font.getStringWidth(trimmedName) + 2, + paneY, + width - 28, + selectedEntry.container, + this + ); + init = false; + } + badgeRenderer.draw(mouseX, mouseY); + + String versionString; + if (metadata.getName().equals("Minecraft")) { // BAD CODE + versionString = Global.VERSION; + } else { + versionString = metadata.getVersion().getFriendlyString(); + } + font.drawString("v" + versionString, x + imageOffset, paneY + 2 + lineSpacing, 0x808080); + + List names = new ArrayList<>(); + metadata.getAuthors().stream() + .filter(Objects::nonNull) + .map(Person::getName) + .filter(Objects::nonNull) + .forEach(names::add); + + if (!names.isEmpty()) { + String authors = names.size() > 1 + ? Joiner.on(", ").join(names) + : names.get(0); + + RenderUtils.INSTANCE.drawWrappedString( + font, + i18n.translateKeyAndFormat("modmenu.authorPrefix", authors), + x + imageOffset, + paneY + 2 + lineSpacing * 2, + paneWidth - imageOffset - 4, + 1, + 0x808080 + ); + } + + if (this.tooltip != null) { + this.renderTooltip(Lists.newArrayList(Splitter.on("\n").split(this.tooltip)), mouseX, mouseY); + } + } + } + + private void drawIconQuad(int x, int y) { + GL11.glEnable(GL11.GL_BLEND); + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + tess.addVertexWithUV(x, y, 0, 0, 0); + tess.addVertexWithUV(x, y + 32.0, 0, 0, 1); + tess.addVertexWithUV(x + 32.0, y + 32.0, 0, 1, 1); + tess.addVertexWithUV(x + 32.0, y, 0, 1, 0); + tess.draw(); + GL11.glDisable(GL11.GL_BLEND); + } + + static String getString(Font font, String fullName, int maxNameWidth) { + // If it already fits, just keep what the caller gave us + if (font.getStringWidth(fullName) <= maxNameWidth) { + return fullName; + } + + int maxWidth = maxNameWidth - font.getStringWidth("..."); + StringBuilder sb = new StringBuilder(); + + // Build up characters until we exceed the allowed width or run out of chars + while (sb.length() < fullName.length() + && font.getStringWidth(sb.toString()) < maxWidth) { + sb.append(fullName.charAt(sb.length())); + } + + if (sb.length() == 0) { + return "..."; + } + + // Drop the last char to make room for "..." + sb.setLength(sb.length() - 1); + sb.append("..."); + + return sb.toString(); + } + + + public void overlayBackground(int x1, int y1, int x2, int y2, + int red, int green, int blue, + int startAlpha, int endAlpha) { + Tessellator tessellator = Tessellator.instance; + mc.textureManager.bindTexture(mc.textureManager.loadTexture("/gui/background.png")); + GL11.glColor4f(1f, 1f, 1f, 1f); + tessellator.startDrawingQuads(); + tessellator.setColorRGBA(red, green, blue, endAlpha); + tessellator.addVertexWithUV(x1, y2, 0.0D, x1 / 32.0F, y2 / 32.0F); + tessellator.addVertexWithUV(x2, y2, 0.0D, x2 / 32.0F, y2 / 32.0F); + tessellator.setColorRGBA(red, green, blue, startAlpha); + tessellator.addVertexWithUV(x2, y1, 0.0D, x2 / 32.0F, y1 / 32.0F); + tessellator.addVertexWithUV(x1, y1, 0.0D, x1 / 32.0F, y1 / 32.0F); + tessellator.draw(); + } + + @Override + public void removed() { + super.removed(); + this.modList.close(); + } + + public void setTooltip(String tooltip) { + this.tooltip = tooltip; + } + + public ModListEntry getSelectedEntry() { + return selected; + } + + public void updateSelectedEntry(ModListEntry entry) { + if (entry != null) { + this.selected = entry; + } + } + + public double getScrollPercent() { + return scrollPercent; + } + + public void updateScrollPercent(double scrollPercent) { + this.scrollPercent = scrollPercent; + } + + public String getSearchInput() { + return searchBox.getText(); + } + + @SuppressWarnings("unused") + public boolean showingFilterOptions() { + return filterOptionsShown; + } + + public void renderTooltip(List list, int i, int j) { + if (!list.isEmpty()) { + Font font = this.font; + + GL11.glDisable(GL12.GL_RESCALE_NORMAL); + GL11.glDisable(GL11.GL_DEPTH_TEST); + int maxWidth = 0; + + for (String string : list) { + int w = font.getStringWidth(string); + if (w > maxWidth) { + maxWidth = w; + } } - trimmedName = trimmedName.isEmpty() ? "..." : trimmedName.substring(0, trimmedName.length() - 1) + "..."; + + int x = i + 12; + int y = j - 12; + int height = 8; + if (list.size() > 1) { + height += 2 + (list.size() - 1) * 10; + } + + if (x + maxWidth > this.width) { + x -= 28 + maxWidth; + } + + if (y + height + 6 > this.height) { + y = this.height - height - 6; + } + + int transparentGrey = 0xC0000000; + int margin = 3; + this.fillGradient(x - margin, y - margin, x + maxWidth + margin, + y + height + margin, transparentGrey, transparentGrey); + + GL11.glPushMatrix(); + GL11.glTranslatef(0, 0, 300); + + for (int idx = 0; idx < list.size(); ++idx) { + String line = list.get(idx); + if (line != null) { + font.drawString(line, x, y, 0xffffff); + } + + if (idx == 0) { + y += 2; + } + + y += 10; + } + + GL11.glPopMatrix(); + GL11.glEnable(GL11.GL_DEPTH_TEST); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); } - return trimmedName; } - public void overlayBackground(int x1, int y1, int x2, int y2, int red, int green, int blue, int startAlpha, int endAlpha) { - Tessellator tessellator = Tessellator.instance; - mc.textureManager.bindTexture(mc.textureManager.loadTexture("/gui/background.png")); - GL11.glColor4f(1f, 1f, 1f, 1f); - tessellator.startDrawingQuads(); - tessellator.setColorRGBA(red, green, blue, endAlpha); - tessellator.addVertexWithUV(x1, y2, 0.0D, x1 / 32.0F, y2 / 32.0F); - tessellator.addVertexWithUV(x2, y2, 0.0D, x2 / 32.0F, y2 / 32.0F); - tessellator.setColorRGBA(red, green, blue, startAlpha); - tessellator.addVertexWithUV(x2, y1, 0.0D, x2 / 32.0F, y1 / 32.0F); - tessellator.addVertexWithUV(x1, y1, 0.0D, x1 / 32.0F, y1 / 32.0F); - tessellator.draw(); - } - - @Override - public void removed() { - super.removed(); - this.modList.close(); - } - - public void setTooltip(String tooltip) { - this.tooltip = tooltip; - } - - public ModListEntry getSelectedEntry() { - return selected; - } - - public void updateSelectedEntry(ModListEntry entry) { - if (entry != null) { - this.selected = entry; - } - } - - public double getScrollPercent() { - return scrollPercent; - } - - public void updateScrollPercent(double scrollPercent) { - this.scrollPercent = scrollPercent; - } - - public String getSearchInput() { - return searchBox.getText(); - } - - public boolean showingFilterOptions() { - return filterOptionsShown; - } - - public void renderTooltip(List list, int i, int j) { - if (!list.isEmpty()) { - Font font = this.font; - - GL11.glDisable(GL12.GL_RESCALE_NORMAL); - GL11.glDisable(GL11.GL_DEPTH_TEST); - int k = 0; - - for (String string : list) { - int l = font.getStringWidth(string); - if (l > k) { - k = l; - } - } - - int m = i + 12; - int n = j - 12; - int p = 8; - if (list.size() > 1) { - p += 2 + (list.size() - 1) * 10; - } - - if (m + k > this.width) { - m -= 28 + k; - } - - if (n + p + 6 > this.height) { - n = this.height - p - 6; - } - - int transparentGrey = -1073741824; - int margin = 3; - this.fillGradient(m - margin, n - margin, m + k + margin, - n + p + margin, transparentGrey, transparentGrey); - GL11.glPushMatrix(); - GL11.glTranslatef(0, 0, 300); - - for(int t = 0; t < list.size(); ++t) { - String string2 = list.get(t); - if (string2 != null) { - font.drawString(string2, m, n, 0xffffff); - } - - if (t == 0) { - n += 2; - } - - n += 10; - } - - GL11.glPopMatrix(); - GL11.glEnable(GL11.GL_DEPTH_TEST); - GL11.glEnable(GL12.GL_RESCALE_NORMAL); - } - } - - protected void fillGradient(int i, int j, int k, int l, int m, int n) { - float f = (float)(m >> 24 & 255) / 255.0F; - float g = (float)(m >> 16 & 255) / 255.0F; - float h = (float)(m >> 8 & 255) / 255.0F; - float o = (float)(m & 255) / 255.0F; - float p = (float)(n >> 24 & 255) / 255.0F; - float q = (float)(n >> 16 & 255) / 255.0F; - float r = (float)(n >> 8 & 255) / 255.0F; - float s = (float)(n & 255) / 255.0F; - GL11.glDisable(GL11.GL_TEXTURE_2D); - GL11.glEnable(GL11.GL_BLEND); - GL11.glDisable(GL11.GL_ALPHA_TEST); - GL14.glBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO); - GL11.glShadeModel(GL11.GL_SMOOTH); - Tessellator tessellator = Tessellator.instance; - tessellator.startDrawingQuads(); - tessellator.setColorRGBA_F(g, h, o, f); - tessellator.addVertex(k, j, 300); - tessellator.addVertex(i, j, 300); - tessellator.setColorRGBA_F(q, r, s, p); - tessellator.addVertex(i, l, 300); - tessellator.addVertex(k, l, 300); - tessellator.draw(); - GL11.glShadeModel(GL11.GL_FLAT); - GL11.glDisable(GL11.GL_BLEND); - GL11.glEnable(GL11.GL_ALPHA_TEST); - GL11.glEnable(GL11.GL_TEXTURE_2D); - } + protected void fillGradient(int i, int j, int k, int l, int m, int n) { + float a1 = (m >> 24 & 255) / 255.0F; + float r1 = (m >> 16 & 255) / 255.0F; + float g1 = (m >> 8 & 255) / 255.0F; + float b1 = (m & 255) / 255.0F; + float a2 = (n >> 24 & 255) / 255.0F; + float r2 = (n >> 16 & 255) / 255.0F; + float g2 = (n >> 8 & 255) / 255.0F; + float b2 = (n & 255) / 255.0F; + + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glDisable(GL11.GL_ALPHA_TEST); + GL14.glBlendFuncSeparate( + GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, + GL11.GL_ONE, GL11.GL_ZERO + ); + GL11.glShadeModel(GL11.GL_SMOOTH); + + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_F(r1, g1, b1, a1); + tessellator.addVertex(k, j, 300); + tessellator.addVertex(i, j, 300); + tessellator.setColorRGBA_F(r2, g2, b2, a2); + tessellator.addVertex(i, l, 300); + tessellator.addVertex(k, l, 300); + tessellator.draw(); + + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + + public Set getShowModChildren() { + return showModChildren; + } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModListWidget.java b/src/main/java/io/github/prospector/modmenu/gui/ModListWidget.java index 197accc4c..3880aac25 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListWidget.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModListWidget.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.gui; - import io.github.prospector.modmenu.ModMenu; import io.github.prospector.modmenu.config.ModMenuConfigManager; import io.github.prospector.modmenu.gui.entries.ChildEntry; @@ -14,298 +13,316 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.render.tessellator.Tessellator; import net.minecraft.core.util.helper.MathHelper; +import org.lwjgl.glfw.GLFW; import org.lwjgl.opengl.GL11; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.awt.image.BufferedImage; import java.nio.file.Path; import java.util.*; public class ModListWidget extends AlwaysSelectedEntryListWidget implements AutoCloseable { - private static final Logger LOGGER = LoggerFactory.getLogger(ModMenu.MOD_ID); - public static final boolean DEBUG = Boolean.getBoolean("modmenu.debug"); - - private final Map modIconsCache = new HashMap<>(); - private final ModListScreen parent; - private List modContainerList = null; - private Set addedMods = new HashSet<>(); - private String selectedModId = null; - private boolean scrolling; - private boolean isFocused; - - public ModListWidget(Minecraft client, int width, int height, int y1, int y2, int entryHeight, String searchTerm, ModListWidget list, ModListScreen parent) { - super(client, width, height, y1, y2, entryHeight); - this.parent = parent; - if (list != null) { - this.modContainerList = list.modContainerList; - } - this.filter(searchTerm, false); - setScrollAmount(parent.getScrollPercent() * Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4))); - } - - @Override - public void setScrollAmount(double amount) { - super.setScrollAmount(amount); - int denominator = Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4)); - if (denominator <= 0) { - parent.updateScrollPercent(0); - } else { - parent.updateScrollPercent(getScrollAmount() / Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4))); - } - } - - @Override - protected boolean isFocused() { - return isFocused; - } - - public void select(ModListEntry entry) { - this.setSelected(entry); - } - - @Override - public void setSelected(ModListEntry entry) { - super.setSelected(entry); - selectedModId = entry.getMetadata().getId(); - parent.updateSelectedEntry(getSelected()); - } - - @Override - protected boolean isSelectedItem(int index) { - ModListEntry selected = getSelected(); - return selected != null && selected.getMetadata().getId().equals(getEntry(index).getMetadata().getId()); - } - - @Override - public int addEntry(ModListEntry entry) { - if (addedMods.contains(entry.container)) { - return 0; - } - addedMods.add(entry.container); - int i = super.addEntry(entry); - if (entry.getMetadata().getId().equals(selectedModId)) { - setSelected(entry); - } - return i; - } - - @Override - protected boolean removeEntry(ModListEntry entry) { - addedMods.remove(entry.container); - return super.removeEntry(entry); - } - - @Override - protected ModListEntry remove(int index) { - addedMods.remove(getEntry(index).container); - return super.remove(index); - } - - public void reloadFilters() { - filter(parent.getSearchInput(), true, false); - } - - - public void filter(String searchTerm, boolean refresh) { - filter(searchTerm, refresh, true); - } - - private void filter(String searchTerm, boolean refresh, boolean search) { - this.clearEntries(); - addedMods.clear(); - Collection mods = FabricLoader.getInstance().getAllMods(); - - if (DEBUG) { - mods = new ArrayList<>(mods); - mods.addAll(TestModContainer.getTestModContainers()); - } - - if (this.modContainerList == null || refresh) { - this.modContainerList = new ArrayList<>(); - modContainerList.addAll(mods); - this.modContainerList.sort(ModMenuConfigManager.getConfig().getSorting().getComparator()); - } - - boolean validSearch = ModListSearch.validSearchQuery(searchTerm); - List matched = ModListSearch.search(parent, searchTerm, modContainerList); - - for (ModContainer container : matched) { - ModMetadata metadata = container.getMetadata(); - String modId = metadata.getId(); - boolean library = ModMenu.LIBRARY_MODS.contains(modId); - - //Hide parent lib mods when the config is set to hide - if (library && !ModMenuConfigManager.getConfig().showLibraries()) { - continue; - } - - if (!ModMenu.PARENT_MAP.values().contains(container)) { - if (ModMenu.PARENT_MAP.keySet().contains(container)) { - //Add parent mods when not searching - List children = ModMenu.PARENT_MAP.get(container); - children.sort(ModMenuConfigManager.getConfig().getSorting().getComparator()); - ParentEntry parent = new ParentEntry(minecraft, container, children, this); - this.addEntry(parent); - //Add children if they are meant to be shown - if (this.parent.showModChildren.contains(modId)) { - List validChildren = ModListSearch.search(this.parent, searchTerm, children); - for (ModContainer child : validChildren) { - this.addEntry(new ChildEntry(minecraft, child, parent, this, validChildren.indexOf(child) == validChildren.size() - 1)); - } - } - } else { - //A mod with no children - this.addEntry(new IndependentEntry(minecraft, container, this)); - } - } - } - - if (parent.getSelectedEntry() != null && !children().isEmpty() || this.getSelected() != null && getSelected().getMetadata() != parent.getSelectedEntry().getMetadata()) { - for (ModListEntry entry : children()) { - if (entry.getMetadata().equals(parent.getSelectedEntry().getMetadata())) { - setSelected(entry); - } - } - } else { - if (getSelected() == null && !children().isEmpty() && getEntry(0) != null) { - setSelected(getEntry(0)); - } - } - - if (getScrollAmount() > Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4))) { - setScrollAmount(Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4))); - } - } - @Override - protected void renderList(int x, int y, int mouseX, int mouseY, float delta) { - int itemCount = this.getItemCount(); - Tessellator tessellator_1 = Tessellator.instance; - - for (int index = 0; index < itemCount; ++index) { - int entryTop = this.getRowTop(index) + 2; - int entryBottom = this.getRowTop(index) + this.itemHeight; - if (entryBottom >= this.top && entryTop <= this.bottom) { - int entryHeight = this.itemHeight - 4; - ModListEntry entry = this.getEntry(index); - int rowWidth = this.getRowWidth(); - int entryLeft; - if (this.renderSelection && this.isSelectedItem(index)) { - entryLeft = getRowLeft() - 2 + entry.getXOffset(); - int selectionRight = x + rowWidth + 2; - GL11.glDisable(GL11.GL_TEXTURE_2D); - float float_2 = this.isFocused() ? 1.0F : 0.5F; - GL11.glColor4f(float_2, float_2, float_2, 1f); - tessellator_1.startDrawingQuads(); - tessellator_1.addVertex((double) entryLeft, (double) (entryTop + entryHeight + 2), 0.0D); - tessellator_1.addVertex((double) selectionRight, (double) (entryTop + entryHeight + 2), 0.0D); - tessellator_1.addVertex((double) selectionRight, (double) (entryTop - 2), 0.0D); - tessellator_1.addVertex((double) entryLeft, (double) (entryTop - 2), 0.0D); - tessellator_1.draw(); - GL11.glColor4f(0f, 0f, 0f, 1f); - tessellator_1.startDrawingQuads(); - tessellator_1.addVertex((double) (entryLeft + 1), (double) (entryTop + entryHeight + 1), 0.0D); - tessellator_1.addVertex((double) (selectionRight - 1), (double) (entryTop + entryHeight + 1), 0.0D); - tessellator_1.addVertex((double) (selectionRight - 1), (double) (entryTop - 1), 0.0D); - tessellator_1.addVertex((double) (entryLeft + 1), (double) (entryTop - 1), 0.0D); - tessellator_1.draw(); - GL11.glEnable(GL11.GL_TEXTURE_2D); - } - - entryLeft = this.getRowLeft(); - entry.render(index, entryTop, entryLeft, rowWidth, entryHeight, mouseX, mouseY, this.isMouseOver((double) mouseX, (double) mouseY) && Objects.equals(this.getEntryAtPos((double) mouseX, (double) mouseY), entry), delta); - } - } - - } - - @Override - protected void updateScrollingState(double double_1, double double_2, int int_1) { - super.updateScrollingState(double_1, double_2, int_1); - this.scrolling = int_1 == 0 && double_1 >= (double) this.getScrollbarPosition() && double_1 < (double) (this.getScrollbarPosition() + 6); - } - - @Override - public void mouseClicked(int double_1, int double_2, int int_1) { - this.updateScrollingState(double_1, double_2, int_1); - if (this.isMouseOver(double_1, double_2)) { - ModListEntry entry = this.getEntryAtPos(double_1, double_2); - if (entry != null) { - if (entry.list.getFocused() != null) { - if (!entry.list.getFocused().equals(entry)) { - this.setFocused(entry); - this.setSelected(entry); - this.setDragging(true); - super.mouseClicked(double_1, double_2, int_1); + public static final boolean DEBUG = Boolean.getBoolean("modmenu.debug"); + + private final Map modIconsCache = new HashMap<>(); + private final ModListScreen parent; + + private List modContainerList = null; + private final Set addedMods = new HashSet<>(); + private String selectedModId = null; + + public ModListWidget(Minecraft client, int width, int height, int top, int bottom, int entryHeight, String searchTerm, ModListWidget previousList, ModListScreen parent) { + super(client, width, height, top, bottom, entryHeight); + this.parent = parent; + + if (previousList != null) { + this.modContainerList = previousList.modContainerList; + } + + this.filter(searchTerm, false); + + int maxScrollRange = Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4)); + setScrollAmount(parent.getScrollPercent() * maxScrollRange); + } + + @Override + public void setScrollAmount(double amount) { + super.setScrollAmount(amount); + int maxScrollRange = Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4)); + if (maxScrollRange == 0) { + parent.updateScrollPercent(0); + } else { + parent.updateScrollPercent(getScrollAmount() / maxScrollRange); + } + } + + public void select(ModListEntry entry) { + this.setSelected(entry); + } + + @Override + public void setSelected(ModListEntry entry) { + super.setSelected(entry); + selectedModId = entry.getMetadata().getId(); + parent.updateSelectedEntry(getSelected()); + } + + @Override + protected boolean isSelectedItem(int index) { + ModListEntry selected = getSelected(); + if (selected == null) { + return false; + } + return selected.getMetadata().getId().equals(getEntry(index).getMetadata().getId()); + } + + @Override + public int addEntry(ModListEntry entry) { + if (addedMods.contains(entry.container)) { + return 0; + } + addedMods.add(entry.container); + int idx = super.addEntry(entry); + if (entry.getMetadata().getId().equals(selectedModId)) { + setSelected(entry); + } + return idx; + } + + @Override + protected boolean removeEntry(ModListEntry entry) { + addedMods.remove(entry.container); + return super.removeEntry(entry); + } + + @Override + protected ModListEntry remove(int index) { + addedMods.remove(getEntry(index).container); + return super.remove(index); + } + + public void reloadFilters() { + filter(parent.getSearchInput(), true); + } + + public void filter(String searchTerm, boolean refresh) { + this.clearEntries(); + addedMods.clear(); + + Collection mods = FabricLoader.getInstance().getAllMods(); + + if (DEBUG) { + mods = new ArrayList<>(mods); + mods.addAll(TestModContainer.getTestModContainers()); + } + + if (this.modContainerList == null || refresh) { + this.modContainerList = new ArrayList<>(); + this.modContainerList.addAll(mods); + this.modContainerList.sort(ModMenuConfigManager.getConfig().getSorting().getComparator()); + } + + List matched = ModListSearch.search(parent, searchTerm, modContainerList); + + for (ModContainer container : matched) { + ModMetadata metadata = container.getMetadata(); + String modId = metadata.getId(); + boolean library = ModMenu.LIBRARY_MODS.contains(modId); + + if (library && !ModMenuConfigManager.getConfig().showLibraries()) { + continue; + } + + if (!ModMenu.PARENT_MAP.values().contains(container)) { + if (ModMenu.PARENT_MAP.keySet().contains(container)) { + List children = ModMenu.PARENT_MAP.get(container); + children.sort(ModMenuConfigManager.getConfig().getSorting().getComparator()); + ParentEntry parentEntry = new ParentEntry(minecraft, container, children, this); + this.addEntry(parentEntry); + + if (this.parent.getShowModChildren().contains(modId)) { + List validChildren = ModListSearch.search(this.parent, searchTerm, children); + for (ModContainer child : validChildren) { + boolean lastChild = validChildren.indexOf(child) == validChildren.size() - 1; + this.addEntry(new ChildEntry(minecraft, child, this, lastChild)); + } } } else { - this.setFocused(entry); - this.setSelected(entry); - this.setDragging(true); - super.mouseClicked(double_1, double_2, int_1); + this.addEntry(new IndependentEntry(minecraft, container, this)); + } + } + } + ModListEntry theSelected = this.getSelected(); + if ((parent.getSelectedEntry() != null && !children().isEmpty()) + || (theSelected != null && theSelected.getMetadata() != parent.getSelectedEntry().getMetadata())) { + for (ModListEntry entry : children()) { + if (entry.getMetadata().equals(parent.getSelectedEntry().getMetadata())) { + setSelected(entry); } - } else if (int_1 == 0) { - this.clickedHeader((int) (double_1 - (double) (this.left + this.width / 2 - this.getRowWidth() / 2)), (int) (double_2 - (double) this.top) + (int) this.getScrollAmount() - 4); - } - } - } - - public final ModListEntry getEntryAtPos(double x, double y) { - int int_5 = MathHelper.floor(y - (double) this.top) - this.headerHeight + (int) this.getScrollAmount() - 4; // convertToBlockCoord - int index = int_5 / this.itemHeight; - return x < (double) this.getScrollbarPosition() && x >= (double) getRowLeft() && x <= (double) (getRowLeft() + getRowWidth()) && index >= 0 && int_5 >= 0 && index < this.getItemCount() ? this.children().get(index) : null; - } - - @Override - protected int getScrollbarPosition() { - return this.width - 6; - } - - @Override - public int getRowWidth() { - return this.width - (Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4)) > 0 ? 18 : 12); - } - - @Override - protected int getRowLeft() { - return left + 6; - } - - public int getWidth() { - return width; - } - - public int getTop() { - return this.top; - } - - public ModListScreen getParent() { - return parent; - } - - @Override - protected int getMaxPosition() { - return super.getMaxPosition() + 4; - } - - public int getDisplayedCount() { - return children().size(); - } - - @Override - public void close() { - this.children().forEach(ModListEntry::deleteTexture); - } - - BufferedImage getCachedModIcon(Path path) { - return this.modIconsCache.get(path); - } - - void cacheModIcon(Path path, BufferedImage tex) { - this.modIconsCache.put(path, tex); - } - - public Set getCurrentModSet() { - return addedMods; - } + } + } else if (getSelected() == null && !children().isEmpty() && getEntry(0) != null) { + setSelected(getEntry(0)); + } + + int maxScrollRange = Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4)); + if (getScrollAmount() > maxScrollRange) { + setScrollAmount(maxScrollRange); + } + } + + @Override + protected void renderList(int rowLeft, int baseRowTop, int mouseX, int mouseY, float delta) { + int itemCount = this.getItemCount(); + Tessellator tessellator = Tessellator.instance; + + for (int index = 0; index < itemCount; ++index) { + int rowTop = this.getRowTop(index); + int rowBottom = this.getRowBottom(index); + + if (rowBottom >= this.top && rowTop <= this.bottom) { + ModListEntry entry = this.getEntry(index); + int entryTop = rowTop + 2; + int rowHeightInner = this.itemHeight - 4; + int rowWidth = this.getRowWidth(); + + if (this.renderSelection && this.isSelectedItem(index)) { + int selectionLeft = this.getRowLeft() - 2 + entry.getXOffset(); + int selectionRight = rowLeft + rowWidth + 2; + + GL11.glDisable(GL11.GL_TEXTURE_2D); + float brightness = this.isFocused() ? 1.0F : 0.5F; + GL11.glColor4f(brightness, brightness, brightness, 1f); + + tessellator.startDrawingQuads(); + tessellator.addVertex(selectionLeft, entryTop + rowHeightInner + 2.0, 0.0D); + tessellator.addVertex(selectionRight, entryTop + rowHeightInner + 2.0, 0.0D); + tessellator.addVertex(selectionRight, entryTop - 2.0, 0.0D); + tessellator.addVertex(selectionLeft, entryTop - 2.0, 0.0D); + tessellator.draw(); + + GL11.glColor4f(0f, 0f, 0f, 1f); + tessellator.startDrawingQuads(); + tessellator.addVertex(selectionLeft + 1.0, entryTop + rowHeightInner + 1.0, 0.0D); + tessellator.addVertex(selectionRight - 1.0, entryTop + rowHeightInner + 1.0, 0.0D); + tessellator.addVertex(selectionRight - 1.0, entryTop - 1.0, 0.0D); + tessellator.addVertex(selectionLeft + 1.0, entryTop - 1.0, 0.0D); + tessellator.draw(); + + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + + int entryLeft = this.getRowLeft(); + boolean hovered = this.isMouseOver(mouseX, mouseY) + && Objects.equals(this.getEntryAtPosition(mouseX, mouseY), entry); + + entry.render( + index, + entryTop, + entryLeft, + rowWidth, + rowHeightInner, + mouseX, + mouseY, + hovered, + delta + ); + } + } + } + @Override + public void mouseClicked(int mouseX, int mouseY, int button) { + if (!this.isMouseOver(mouseX, mouseY)) { + return; + } + + ModListEntry clickedEntry = this.getEntryAtPosition(mouseX, mouseY); + if (clickedEntry != null) { + if (this.getFocused() != clickedEntry) { + this.setFocused(clickedEntry); + } + // use setter so ModListWidget.setSelected runs + this.setSelected(clickedEntry); + + // let the entry react (ModListEntry will call list.select(this) etc) + clickedEntry.mouseClicked(mouseX, mouseY, button); + } else if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT) { + int headerClickX = (int)(mouseX - (this.left + this.width / 2.0 - this.getRowWidth() / 2.0)); + int headerClickY = (int)(mouseY - (double)this.top) + (int)this.getScrollAmount() - TOP_PADDING; + this.onHeaderClicked(headerClickX, headerClickY); + } + } + + @Override + protected ModListEntry getEntryAtPosition(double mouseX, double mouseY) { + int yInList = MathHelper.floor(mouseY - this.top) - this.headerHeight + (int) this.getScrollAmount() - 4; + int rowIndex = yInList / this.itemHeight; + + int rowLeft = this.getRowLeft(); + int rowRight = rowLeft + this.getRowWidth(); + + if (mouseX >= this.getScrollbarPosition() || mouseX < rowLeft || mouseX > rowRight) { + return null; + } + + if (yInList < 0 || rowIndex < 0 || rowIndex >= this.getItemCount()) { + return null; + } + + return this.children().get(rowIndex); + } + + @Override + protected int getScrollbarPosition() { + return this.width - SCROLLBAR_WIDTH; + } + + @Override + public int getRowWidth() { + int scrollRange = Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4)); + return this.width - (scrollRange > 0 ? 18 : 12); + } + + @Override + protected int getRowLeft() { + return this.left + 6; + } + + public int getWidth() { + return this.width; + } + + @SuppressWarnings("unused") + public int getTop() { + return this.top; + } + + public ModListScreen getParent() { + return parent; + } + + @Override + protected int getMaxPosition() { + return super.getMaxPosition() + 4; + } + + public int getDisplayedCount() { + return children().size(); + } + + @Override + public void close() { + this.children().forEach(ModListEntry::deleteTexture); + } + + BufferedImage getCachedModIcon(Path path) { + return this.modIconsCache.get(path); + } + + void cacheModIcon(Path path, BufferedImage texture) { + this.modIconsCache.put(path, texture); + } + + @SuppressWarnings("unused") + public Set getCurrentModSet() { + return addedMods; + } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModMenuButtonWidget.java b/src/main/java/io/github/prospector/modmenu/gui/ModMenuButtonWidget.java index 29c7a7234..6baeb1230 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModMenuButtonWidget.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModMenuButtonWidget.java @@ -1,10 +1,9 @@ package io.github.prospector.modmenu.gui; - import net.minecraft.client.gui.ButtonElement; public class ModMenuButtonWidget extends ButtonElement { - public ModMenuButtonWidget(int buttonId, int x, int y, int width, int height, String text) { - super(buttonId, x, y, width, height, text); - } + public ModMenuButtonWidget(int id, int xPosition, int yPosition, int width, int height, String text) { + super(id, xPosition, yPosition, width, height, text); + } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModMenuTexturedButtonWidget.java b/src/main/java/io/github/prospector/modmenu/gui/ModMenuTexturedButtonWidget.java index 9f5ef3f26..7c5bcdf41 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModMenuTexturedButtonWidget.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModMenuTexturedButtonWidget.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.gui; - import net.minecraft.client.Minecraft; import net.minecraft.client.gui.ButtonElement; import net.minecraft.client.render.Font; @@ -8,74 +7,77 @@ import org.lwjgl.opengl.GL11; public class ModMenuTexturedButtonWidget extends ButtonElement { - private final String texture; - private final int u; - private final int v; - private final int uWidth; - private final int vHeight; + private final String texture; + private final int u; + private final int v; + private final int uWidth; + private final int vHeight; - protected ModMenuTexturedButtonWidget(int buttonId, int x, int y, int width, int height, int u, int v, String texture) { - this(buttonId, x, y, width, height, u, v, texture, 256, 256); - } + @SuppressWarnings("unused") + protected ModMenuTexturedButtonWidget(int id, int xPosition, int yPosition, int width, int height, int u, int v, String texture) { + this(id, xPosition, yPosition, width, height, u, v, texture, 256, 256); + } - protected ModMenuTexturedButtonWidget(int buttonId, int x, int y, int width, int height, int u, int v, String texture, int uWidth, int vHeight) { - this(buttonId, x, y, width, height, u, v, texture, uWidth, vHeight, ""); - } + protected ModMenuTexturedButtonWidget(int id, int xPosition, int yPosition, int width, int height, int u, int v, String texture, int uWidth, int vHeight) { + this(id, xPosition, yPosition, width, height, u, v, texture, uWidth, vHeight, ""); + } - protected ModMenuTexturedButtonWidget(int buttonId, int x, int y, int width, int height, int u, int v, String texture, int uWidth, int vHeight, String message) { - super(buttonId, x, y, width, height, message); - this.uWidth = uWidth; - this.vHeight = vHeight; - this.u = u; - this.v = v; - this.texture = texture; - } + protected ModMenuTexturedButtonWidget(int id, int xPosition, int yPosition, int width, int height, int u, int v, String texture, int uWidth, int vHeight, String message) { + super(id, xPosition, yPosition, width, height, message); + this.uWidth = uWidth; + this.vHeight = vHeight; + this.u = u; + this.v = v; + this.texture = texture; + } - protected void setPos(int x, int y) { - this.xPosition = x; - this.yPosition = y; - } + @SuppressWarnings("unused") + protected void setPos(int x, int y) { + this.xPosition = x; + this.yPosition = y; + } - public boolean isHovered(int mouseX, int mouseY) { - return mouseX >= this.xPosition && mouseY >= this.yPosition && mouseX < this.xPosition + this.width && mouseY < this.yPosition + this.height; - } + @Override + public boolean isHovered(int mouseX, int mouseY) { + return mouseX >= this.xPosition && mouseY >= this.yPosition && mouseX < this.xPosition + this.width && mouseY < this.yPosition + this.height; + } @Override - public void drawButton(Minecraft minecraft, int i, int j) { - render(minecraft, i, j); + public void drawButton(Minecraft minecraft, int mouseX, int mouseY) { + render(minecraft, mouseX, mouseY); } - public void render(Minecraft mc, int mouseX, int mouseY) { - if (this.visible) { - Font font = mc.font; - mc.textureManager.bindTexture(mc.textureManager.loadTexture(texture)); - GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); - boolean hovered = isHovered(mouseX, mouseY); + public void render(Minecraft mc, int mouseX, int mouseY) { + if (this.visible) { + Font font = mc.font; + mc.textureManager.bindTexture(mc.textureManager.loadTexture(texture)); + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + boolean hovered = isHovered(mouseX, mouseY); - int adjustedV = this.v; - if (!enabled) { - adjustedV += this.height * 2; - } else if (hovered) { - adjustedV += this.height; - } - float uScale = 1f / uWidth; - float vScale = 1f / vHeight; - Tessellator tess = Tessellator.instance; - tess.startDrawingQuads(); - tess.addVertexWithUV(xPosition, yPosition + height, this.zLevel, (float) u * uScale, (float)(adjustedV + height) * vScale); - tess.addVertexWithUV(xPosition + width, yPosition + height, this.zLevel, ((float)(u + width) * uScale), (float)(adjustedV + height) * vScale); - tess.addVertexWithUV(xPosition + width, yPosition, this.zLevel, (float)(u + width) * uScale, (float)adjustedV * vScale); - tess.addVertexWithUV(xPosition, yPosition, this.zLevel, (float) u * uScale, (float) adjustedV * vScale); - tess.draw(); + int adjustedV = this.v; + if (!enabled) { + adjustedV += this.height * 2; + } else if (hovered) { + adjustedV += this.height; + } + float uScale = 1f / uWidth; + float vScale = 1f / vHeight; + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + tess.addVertexWithUV(xPosition, (double) yPosition + height, this.zLevel, u * uScale, (adjustedV + height) * vScale); + tess.addVertexWithUV((double) xPosition + width, (double) yPosition + height, this.zLevel, ((u + width) * uScale), (adjustedV + height) * vScale); + tess.addVertexWithUV((double) xPosition + width, yPosition, this.zLevel, (u + width) * uScale, adjustedV * vScale); + tess.addVertexWithUV(xPosition, yPosition, this.zLevel, u * uScale, adjustedV * vScale); + tess.draw(); - this.mouseDragged(mc, mouseX, mouseY); - if (!this.enabled) { - this.drawStringCentered(font, this.displayString, this.xPosition + this.width / 2, this.yPosition + (this.height - 8) / 2, 0xffa0a0a0); - } else if (hovered) { - this.drawStringCentered(font, this.displayString, this.xPosition + this.width / 2, this.yPosition + (this.height - 8) / 2, 0xffffa0); - } else { - this.drawStringCentered(font, this.displayString, this.xPosition + this.width / 2, this.yPosition + (this.height - 8) / 2, 0xe0e0e0); - } - } - } + this.mouseDragged(mc, mouseX, mouseY); + if (!this.enabled) { + this.drawStringCentered(font, this.displayString, this.xPosition + this.width / 2, this.yPosition + (this.height - 8) / 2, 0xffa0a0a0); + } else if (hovered) { + this.drawStringCentered(font, this.displayString, this.xPosition + this.width / 2, this.yPosition + (this.height - 8) / 2, 0xffffa0); + } else { + this.drawStringCentered(font, this.displayString, this.xPosition + this.width / 2, this.yPosition + (this.height - 8) / 2, 0xe0e0e0); + } + } + } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/TextFieldWidget.java b/src/main/java/io/github/prospector/modmenu/gui/TextFieldWidget.java index 79d5dcd65..c65616c2d 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/TextFieldWidget.java +++ b/src/main/java/io/github/prospector/modmenu/gui/TextFieldWidget.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.gui; - import io.github.prospector.modmenu.mixin.MinecraftAccessor; import io.github.prospector.modmenu.mixin.TextFieldEditorAccessor; import net.minecraft.client.gui.Screen; @@ -9,7 +8,7 @@ import net.minecraft.client.render.Font; import net.minecraft.client.render.tessellator.Tessellator; import net.minecraft.core.enums.EnumOS; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.Nullable; import org.lwjgl.input.Keyboard; import org.lwjgl.opengl.GL11; @@ -18,322 +17,319 @@ import java.util.function.Predicate; public class TextFieldWidget extends Screen implements ITextField { - private final Font font; - private final TextFieldEditor handler; - public int x; - public int y; - /** - * The width of this text field. - */ - private final int width; - private final int height; - /** - * Has the current text being edited on the textbox. - */ - private String text = ""; - private int maxStringLength = 32; - private int cursorCounter; - private boolean enableBackgroundDrawing = true; - /** - * if true the textbox can lose focus by clicking elsewhere on the screen - */ - private boolean canLoseFocus = true; - /** - * If this value is true along with isEnabled, keyTyped will process the keys. - */ - private boolean isFocused; - /** - * If this value is true along with isFocused, keyTyped will process the keys. - */ - private boolean isEnabled = true; - /** - * The current character index that should be used as start of the rendered text. - */ - private int lineScrollOffset; - private int cursorPosition; - /** - * other selection position, maybe the same as the cursor - */ - private int selectionEnd; - private int enabledColor = 0xe0e0e0; - private int disabledColor = 0x707070; - /** - * True if this textbox is visible - */ - private boolean visible = true; - /** - * Called to check if the text is valid - */ - private Predicate validator = s -> true; - - private final @Nullable String emptyText; - - public TextFieldWidget(Font font, int x, int y, int width, int height, String emptyText) { - this.font = font; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - this.handler = new TextFieldEditor(this); - this.emptyText = emptyText; - } - - public TextFieldWidget(Font font, int x, int y, int width, int height) { - this(font, x, y, width, height, null); - } - - /** - * Increments the cursor counter - */ - public void updateCursorCounter() { - cursorCounter++; - } - - /** - * Sets the text of the textbox, and moves the cursor to the end. - */ - public void setText(String textIn) { - if (validator.test(textIn)) { - if (textIn.length() > maxStringLength) { - text = textIn.substring(0, maxStringLength); - } else { - text = textIn; - } - - setCursorPositionEnd(); - } - } - - /** - * Returns the contents of the textbox - */ - public String getText() { - return text; - } - - @Override - public int maxLength() { - return getMaxStringLength(); - } - - /** - * returns the text between the cursor and selectionEnd - */ - public String getSelectedText() { - int i = Math.min(cursorPosition, selectionEnd); - int j = Math.max(cursorPosition, selectionEnd); - return text.substring(i, j); - } - - public void setValidator(Predicate theValidator) { - validator = theValidator; - } - - private static boolean isAllowedCharacter(char c) { - return c != 167 && c >= ' ' && c != 127; - } - - private static String filterAllowedCharacters(String s) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (isAllowedCharacter(c)) - sb.append(c); - } - return sb.toString(); - } - - /** - * Adds the given text after the cursor, or replaces the currently selected text if there is a selection. - */ - public void writeText(String textToWrite) { - String s = ""; - String s1 = filterAllowedCharacters(textToWrite); - int i = Math.min(cursorPosition, selectionEnd); - int j = Math.max(cursorPosition, selectionEnd); - int k = maxStringLength - text.length() - (i - j); - - if (!text.isEmpty()) { - s = s + text.substring(0, i); - } - - int l; - - if (k < s1.length()) { - s = s + s1.substring(0, k); - l = k; - } else { - s = s + s1; - l = s1.length(); - } - - if (!text.isEmpty() && j < text.length()) { - s = s + text.substring(j); - } - - if (validator.test(s)) { - text = s; - moveCursorBy(i - selectionEnd + l); - } - } - - /** - * Deletes the given number of words from the current cursor's position, unless there is currently a selection, in - * which case the selection is deleted instead. - */ - public void deleteWords(int num) { - if (!text.isEmpty()) { - if (selectionEnd != cursorPosition) { - writeText(""); - } else { - deleteFromCursor(getNthWordFromCursor(num) - cursorPosition); - } - } - } - - /** - * Deletes the given number of characters from the current cursor's position, unless there is currently a selection, - * in which case the selection is deleted instead. - */ - public void deleteFromCursor(int num) { - if (!text.isEmpty()) { - if (selectionEnd != cursorPosition) { - writeText(""); - } else { - boolean flag = num < 0; - int i = flag ? cursorPosition + num : cursorPosition; - int j = flag ? cursorPosition : cursorPosition + num; - String s = ""; - - if (i >= 0) { - s = text.substring(0, i); - } - - if (j < text.length()) { - s = s + text.substring(j); - } - - if (validator.test(s)) { - text = s; - - if (flag) { - moveCursorBy(num); - } - } - } - } - } - - /** - * Gets the starting index of the word at the specified number of words away from the cursor position. - */ - public int getNthWordFromCursor(int numWords) { - return getNthWordFromPos(numWords, getCursorPosition()); - } - - /** - * Gets the starting index of the word at a distance of the specified number of words away from the given position. - */ - public int getNthWordFromPos(int n, int pos) { - return getNthWordFromPosWS(n, pos, true); - } - - /** - * Like getNthWordFromPos (which wraps this), but adds option for skipping consecutive spaces - */ - public int getNthWordFromPosWS(int n, int pos, boolean skipWs) { - int i = pos; - boolean flag = n < 0; - int j = Math.abs(n); - - for (int k = 0; k < j; ++k) { - if (!flag) { - int l = text.length(); - i = text.indexOf(32, i); - - if (i == -1) { - i = l; - } else { - while (skipWs && i < l && text.charAt(i) == ' ') { - i++; - } - } - } else { - while (skipWs && i > 0 && text.charAt(i - 1) == ' ') { - i--; - } - - while (i > 0 && text.charAt(i - 1) != ' ') { - i--; - } - } - } - - return i; - } - - /** - * Moves the text cursor by a specified number of characters and clears the selection - */ - public void moveCursorBy(int num) { - setCursorPosition(selectionEnd + num); - } - - /** - * Sets the current position of the cursor. - */ - public void setCursorPosition(int pos) { - int i = text.length(); - if (pos < 0) - pos = 0; - if (pos > i) - pos = i; - cursorPosition = pos; - setSelectionPos(cursorPosition); - } - - /** - * Moves the cursor to the very start of this text box. - */ - public void setCursorPositionZero() { - setCursorPosition(0); - } - - /** - * Moves the cursor to the very end of this text box. - */ - public void setCursorPositionEnd() { - setCursorPosition(text.length()); - } - - /** - * Call this method from your GuiScreen to process the keys into the textbox - */ - public boolean textboxKeyTyped(char typedChar, int keyCode) { - if (!isFocused) { - return false; - } else if (isKeyComboCtrlA(keyCode)) { - setCursorPositionEnd(); - setSelectionPos(0); - return true; - } else if (isKeyComboCtrlC(keyCode)) { - setClipboardString(getSelectedText()); - return true; - } else if (isKeyComboCtrlV(keyCode)) { - if (isEnabled) { - writeText(((TextFieldEditorAccessor) handler).getClipboardContentString()); - } - - return true; - } else if (isKeyComboCtrlX(keyCode)) { - setClipboardString(getSelectedText()); - - if (isEnabled) { - writeText(""); - } - - return true; - } else { + private final TextFieldEditor handler; + private final int x; + private final int y; + /** + * Has the current text being edited on the textbox. + */ + private String text = ""; + private int maxStringLength = 32; + private int cursorCounter; + private boolean enableBackgroundDrawing = true; + /** + * if true the textbox can lose focus by clicking elsewhere on the screen + */ + private boolean canLoseFocus = true; + /** + * If this value is true along with isEnabled, keyTyped will process the keys. + */ + private boolean isFocused; + /** + * If this value is true along with isFocused, keyTyped will process the keys. + */ + private boolean isEnabled = true; + /** + * The current character index that should be used as start of the rendered text. + */ + private int lineScrollOffset; + private int cursorPosition; + /** + * other selection position, maybe the same as the cursor + */ + private int selectionEnd; + private int enabledColor = 0xe0e0e0; + private int disabledColor = 0x707070; + /** + * True if this textbox is visible + */ + private boolean visible = true; + /** + * Called to check if the text is valid + */ + private Predicate validator = s -> true; + + private final @Nullable String emptyText; + + public TextFieldWidget(Font font, int x, int y, int width, int height, @Nullable String emptyText) { + this.font = font; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.handler = new TextFieldEditor(this); + this.emptyText = emptyText; + } + + @SuppressWarnings("unused") + public TextFieldWidget(Font font, int x, int y, int width, int height) { + this(font, x, y, width, height, null); + } + + /** + * Increments the cursor counter + */ + public void updateCursorCounter() { + cursorCounter++; + } + + /** + * Sets the text of the textbox, and moves the cursor to the end. + */ + public void setText(String textIn) { + if (validator.test(textIn)) { + if (textIn.length() > maxStringLength) { + text = textIn.substring(0, maxStringLength); + } else { + text = textIn; + } + + setCursorPositionEnd(); + } + } + + /** + * Returns the contents of the textbox + */ + public String getText() { + return text; + } + + @Override + public int maxLength() { + return getMaxStringLength(); + } + + /** + * returns the text between the cursor and selectionEnd + */ + public String getSelectedText() { + int i = Math.min(cursorPosition, selectionEnd); + int j = Math.max(cursorPosition, selectionEnd); + return text.substring(i, j); + } + + @SuppressWarnings("unused") + public void setValidator(Predicate theValidator) { + validator = theValidator; + } + + private static boolean isAllowedCharacter(char c) { + return c != 167 && c >= ' ' && c != 127; + } + + private static String filterAllowedCharacters(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (isAllowedCharacter(c)) + sb.append(c); + } + return sb.toString(); + } + + /** + * Adds the given text after the cursor, or replaces the currently selected text if there is a selection. + */ + public void writeText(String textToWrite) { + String s = ""; + String s1 = filterAllowedCharacters(textToWrite); + int i = Math.min(cursorPosition, selectionEnd); + int j = Math.max(cursorPosition, selectionEnd); + int k = maxStringLength - text.length() - (i - j); + + if (!text.isEmpty()) { + s = s + text.substring(0, i); + } + + int l; + + if (k < s1.length()) { + s = s + s1.substring(0, k); + l = k; + } else { + s = s + s1; + l = s1.length(); + } + + if (!text.isEmpty() && j < text.length()) { + s = s + text.substring(j); + } + + if (validator.test(s)) { + text = s; + moveCursorBy(i - selectionEnd + l); + } + } + + /** + * Deletes the given number of words from the current cursor's position, unless there is currently a selection, in + * which case the selection is deleted instead. + */ + public void deleteWords(int num) { + if (!text.isEmpty()) { + if (selectionEnd != cursorPosition) { + writeText(""); + } else { + deleteFromCursor(getNthWordFromCursor(num) - cursorPosition); + } + } + } + + /** + * Deletes the given number of characters from the current cursor's position, unless there is currently a selection, + * in which case the selection is deleted instead. + */ + public void deleteFromCursor(int num) { + if (!text.isEmpty()) { + if (selectionEnd != cursorPosition) { + writeText(""); + } else { + boolean flag = num < 0; + int i = flag ? cursorPosition + num : cursorPosition; + int j = flag ? cursorPosition : cursorPosition + num; + String s = ""; + + if (i >= 0) { + s = text.substring(0, i); + } + + if (j < text.length()) { + s = s + text.substring(j); + } + + if (validator.test(s)) { + text = s; + + if (flag) { + moveCursorBy(num); + } + } + } + } + } + + /** + * Gets the starting index of the word at the specified number of words away from the cursor position. + */ + public int getNthWordFromCursor(int numWords) { + return getNthWordFromPos(numWords, getCursorPosition()); + } + + /** + * Gets the starting index of the word at a distance of the specified number of words away from the given position. + */ + public int getNthWordFromPos(int n, int pos) { + return getNthWordFromPosWS(n, pos, true); + } + + /** + * Like getNthWordFromPos (which wraps this), but adds option for skipping consecutive spaces + */ + public int getNthWordFromPosWS(int n, int pos, boolean skipWs) { + int i = pos; + boolean flag = n < 0; + int j = Math.abs(n); + + for (int k = 0; k < j; ++k) { + if (!flag) { + int l = text.length(); + i = text.indexOf(32, i); + + if (i == -1) { + i = l; + } else { + while (skipWs && i < l && text.charAt(i) == ' ') { + i++; + } + } + } else { + while (skipWs && i > 0 && text.charAt(i - 1) == ' ') { + i--; + } + + while (i > 0 && text.charAt(i - 1) != ' ') { + i--; + } + } + } + + return i; + } + + /** + * Moves the text cursor by a specified number of characters and clears the selection + */ + public void moveCursorBy(int num) { + setCursorPosition(selectionEnd + num); + } + + /** + * Sets the current position of the cursor. + */ + public void setCursorPosition(int pos) { + int i = text.length(); + if (pos < 0) + pos = 0; + if (pos > i) + pos = i; + cursorPosition = pos; + setSelectionPos(cursorPosition); + } + + /** + * Moves the cursor to the very start of this text box. + */ + public void setCursorPositionZero() { + setCursorPosition(0); + } + + /** + * Moves the cursor to the very end of this text box. + */ + public void setCursorPositionEnd() { + setCursorPosition(text.length()); + } + + /** + * Call this method from your GuiScreen to process the keys into the textbox + */ + @SuppressWarnings("UnusedReturnValue") + public boolean textboxKeyTyped(char typedChar, int keyCode) { + if (!isFocused) { + return false; + } else if (isKeyComboCtrlA(keyCode)) { + setCursorPositionEnd(); + setSelectionPos(0); + return true; + } else if (isKeyComboCtrlC(keyCode)) { + setClipboardString(getSelectedText()); + return true; + } else if (isKeyComboCtrlV(keyCode)) { + if (isEnabled) { + writeText(((TextFieldEditorAccessor) handler).getClipboardContentString()); + } + + return true; + } else if (isKeyComboCtrlX(keyCode)) { + setClipboardString(getSelectedText()); + + if (isEnabled) { + writeText(""); + } + + return true; + } else { if (keyCode == Keyboard.KEY_BACK) { if (isCtrlKeyDown()) { if (isEnabled) { @@ -409,341 +405,346 @@ public boolean textboxKeyTyped(char typedChar, int keyCode) { return false; } } - } - - /** - * Called when mouse is clicked, regardless as to whether it is over this button or not. - */ - public void mouseClicked(int mouseX, int mouseY, int mouseButton) { - boolean flag = mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + height; - - if (canLoseFocus) { - setFocused(flag); - } - - if (isFocused && flag && mouseButton == 0) { - int i = mouseX - x; - - if (enableBackgroundDrawing) { - i -= 4; - } - - String s = trimStringToWidth(font, text.substring(lineScrollOffset), getWidth()); - setCursorPosition(trimStringToWidth(font, s, i).length() + lineScrollOffset); - //return true; - } else { - //return false; - } - } - - /** - * Draws the textbox - */ - public void drawTextBox() { - if (getVisible()) { - if (getEnableBackgroundDrawing()) { - drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xffa0a0a0); - drawRect(x, y, x + width, y + height, 0xff000000); - } - - int i = isEnabled ? enabledColor : disabledColor; - int j = cursorPosition - lineScrollOffset; - int k = selectionEnd - lineScrollOffset; - String s = trimStringToWidth(font, text.substring(lineScrollOffset), getWidth()); - boolean flag = j >= 0 && j <= s.length(); - boolean flag1 = isFocused && cursorCounter / 6 % 2 == 0 && flag; - int l = enableBackgroundDrawing ? x + 4 : x; - int i1 = enableBackgroundDrawing ? y + (height - 8) / 2 : y; - int j1 = l; - - if (k > s.length()) { - k = s.length(); - } - - if (!s.isEmpty()) { - String s1 = flag ? s.substring(0, j) : s; - font.drawStringWithShadow(s1, l, i1, i); - j1 += font.getStringWidth(s1) + 1; - } else if (emptyText != null && !this.isFocused) { - font.drawStringWithShadow(emptyText, l, i1, 6250335); - } - - boolean flag2 = cursorPosition < text.length() || text.length() >= getMaxStringLength(); - int k1 = j1; - - if (!flag) { - k1 = j > 0 ? l + width : l; - } else if (flag2) { - k1 = j1 - 1; - --j1; - } - - if (!s.isEmpty() && flag && j < s.length()) { - font.drawStringWithShadow(s.substring(j), j1, i1, i); - //j1 += this.font.getStringWidth(s.substring(j)); - } - - if (flag1) { - if (flag2) { - drawRect(k1, i1 - 1, k1 + 1, i1 + 1 + 9, 0xffd0d0d0); - } else { - font.drawStringWithShadow("_", k1, i1, i); - } - } - - if (k != j) { - int l1 = l + font.getStringWidth(s.substring(0, k)); - drawSelectionBox(k1, i1 - 1, l1 - 1, i1 + 1 + 9); - } - } - } - - /** - * Draws the blue selection box. - */ - private void drawSelectionBox(int startX, int startY, int endX, int endY) { - if (startX < endX) { - int i = startX; - startX = endX; - endX = i; - } - - if (startY < endY) { - int j = startY; - startY = endY; - endY = j; - } - - if (endX > x + width) { - endX = x + width; - } - - if (startX > x + width) { - startX = x + width; - } - - Tessellator tessellator = Tessellator.instance; - GL11.glColor4f(0f, 0f, 255f, 255f); - GL11.glDisable(GL11.GL_TEXTURE_2D); - GL11.glEnable(GL11.GL_COLOR_LOGIC_OP); - GL11.glLogicOp(GL11.GL_OR_REVERSE); - tessellator.startDrawingQuads(); - tessellator.addVertex(startX, endY, 0.0D); - tessellator.addVertex(endX, endY, 0.0D); - tessellator.addVertex(endX, startY, 0.0D); - tessellator.addVertex(startX, startY, 0.0D); - tessellator.draw(); - GL11.glDisable(GL11.GL_COLOR_LOGIC_OP); - GL11.glEnable(GL11.GL_TEXTURE_2D); - } - - /** - * Sets the maximum length for the text in this text box. If the current text is longer than this length, the - * current text will be trimmed. - */ - public void setMaxStringLength(int length) { - maxStringLength = length; - - if (text.length() > length) { - text = text.substring(0, length); - } - } - - /** - * returns the maximum number of character that can be contained in this textbox - */ - public int getMaxStringLength() { - return maxStringLength; - } - - /** - * returns the current position of the cursor - */ - public int getCursorPosition() { - return cursorPosition; - } - - /** - * Gets whether the background and outline of this text box should be drawn (true if so). - */ - public boolean getEnableBackgroundDrawing() { - return enableBackgroundDrawing; - } - - /** - * Sets whether or not the background and outline of this text box should be drawn. - */ - public void setEnableBackgroundDrawing(boolean enableBackgroundDrawingIn) { - enableBackgroundDrawing = enableBackgroundDrawingIn; - } - - /** - * Sets the color to use when drawing this text box's text. A different color is used if this text box is disabled. - */ - public void setTextColor(int color) { - enabledColor = color; - } - - /** - * Sets the color to use for text in this text box when this text box is disabled. - */ - public void setDisabledTextColor(int color) { - disabledColor = color; - } - - /** - * Sets focus to this gui element - */ - public void setFocused(boolean isFocusedIn) { - if (isFocusedIn && !isFocused) { - cursorCounter = 0; - } - - isFocused = isFocusedIn; - } - - /** - * Getter for the focused field - */ - public boolean isFocused() { - return isFocused; - } - - /** - * Sets whether this text box is enabled. Disabled text boxes cannot be typed in. - */ - public void setEnabled(boolean enabled) { - isEnabled = enabled; - } - - /** - * the side of the selection that is not the cursor, may be the same as the cursor - */ - public int getSelectionEnd() { - return selectionEnd; - } - - /** - * returns the width of the textbox depending on if background drawing is enabled - */ - public int getWidth() { - return getEnableBackgroundDrawing() ? width - 8 : width; - } - - /** - * Sets the position of the selection anchor (the selection anchor and the cursor position mark the edges of the - * selection). If the anchor is set beyond the bounds of the current text, it will be put back inside. - */ - public void setSelectionPos(int position) { - int i = text.length(); - - if (position > i) { - position = i; - } - - if (position < 0) { - position = 0; - } - - selectionEnd = position; - - if (font != null) { - if (lineScrollOffset > i) { - lineScrollOffset = i; - } - - int j = getWidth(); - String s = trimStringToWidth(font, text.substring(lineScrollOffset), j); - int k = s.length() + lineScrollOffset; - - if (position == lineScrollOffset) { - lineScrollOffset -= trimStringToWidth(font, text, j, true).length(); - } - - if (position > k) { - lineScrollOffset += position - k; - } else if (position <= lineScrollOffset) { - lineScrollOffset -= lineScrollOffset - position; - } - - if (lineScrollOffset < 0) - lineScrollOffset = 0; - else if (lineScrollOffset > i) - lineScrollOffset = i; - } - } - - /** - * Sets whether this text box loses focus when something other than it is clicked. - */ - public void setCanLoseFocus(boolean canLoseFocusIn) { - canLoseFocus = canLoseFocusIn; - } - - /** - * returns true if this textbox is visible - */ - public boolean getVisible() { - return visible; - } - - /** - * Sets whether or not this textbox is visible - */ - public void setVisible(boolean isVisible) { - visible = isVisible; - } - - private static boolean isCtrlKeyDown() { - if (MinecraftAccessor.getOS() == EnumOS.macos) { - return Keyboard.isKeyDown(Keyboard.KEY_LMETA) || Keyboard.isKeyDown(Keyboard.KEY_RMETA); - } else { - return Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); - } - } - - private static boolean isShiftKeyDown() { - return Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); - } - - private static boolean isAltKeyDown() { - return Keyboard.isKeyDown(Keyboard.KEY_LMENU) || Keyboard.isKeyDown(Keyboard.KEY_RMENU); - } - - private static boolean isKeyComboCtrlA(int keyCode) { - return keyCode == Keyboard.KEY_A && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); - } - - private static boolean isKeyComboCtrlC(int keyCode) { - return keyCode == Keyboard.KEY_C && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); - } - - private static boolean isKeyComboCtrlV(int keyCode) { - return keyCode == Keyboard.KEY_V && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); - } - - private static boolean isKeyComboCtrlX(int keyCode) { - return keyCode == Keyboard.KEY_X && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); - } - - private static void setClipboardString(String text) { - try { - StringSelection selection = new StringSelection(text); - Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null); - } catch (Exception ignore) { - } - } - - private static String trimStringToWidth(Font font, String text, int maxWidth) { - return trimStringToWidth(font, text, maxWidth, false); - } - - private static String trimStringToWidth(Font font, String text, int maxWidth, boolean reverse) { - int width = 0; - int length; - for (length = 0; length < text.length() && width < maxWidth; length++) - width += font.getStringWidth(Character.toString(text.charAt(reverse ? text.length() - 1 - length : length))); - return reverse ? text.substring(text.length() - length) : text.substring(0, length); - } + } + + /** + * Called when mouse is clicked, regardless whether it is over this button or not. + */ + @Override + public void mouseClicked(int mouseX, int mouseY, int mouseButton) { + boolean flag = mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + height; + + if (canLoseFocus) { + setFocused(flag); + } + + if (isFocused && flag && mouseButton == 0) { + int i = mouseX - x; + + if (enableBackgroundDrawing) { + i -= 4; + } + + String s = trimStringToWidth(font, text.substring(lineScrollOffset), getWidth()); + setCursorPosition(trimStringToWidth(font, s, i).length() + lineScrollOffset); + } + } + + /** + * Draws the textbox + */ + public void drawTextBox() { + if (getVisible()) { + if (getEnableBackgroundDrawing()) { + drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xffa0a0a0); + drawRect(x, y, x + width, y + height, 0xff000000); + } + + int i = isEnabled ? enabledColor : disabledColor; + int j = cursorPosition - lineScrollOffset; + int k = selectionEnd - lineScrollOffset; + String s = trimStringToWidth(font, text.substring(lineScrollOffset), getWidth()); + boolean flag = j >= 0 && j <= s.length(); + boolean flag1 = isFocused && cursorCounter / 6 % 2 == 0 && flag; + int l = enableBackgroundDrawing ? x + 4 : x; + int i1 = enableBackgroundDrawing ? y + (height - 8) / 2 : y; + int j1 = l; + + if (k > s.length()) { + k = s.length(); + } + + if (!s.isEmpty()) { + String s1 = flag ? s.substring(0, j) : s; + font.drawStringWithShadow(s1, l, i1, i); + j1 += font.getStringWidth(s1) + 1; + } else if (emptyText != null && !this.isFocused) { + font.drawStringWithShadow(emptyText, l, i1, 6250335); + } + + boolean flag2 = cursorPosition < text.length() || text.length() >= getMaxStringLength(); + int k1 = j1; + + if (!flag) { + k1 = j > 0 ? l + width : l; + } else if (flag2) { + k1 = j1 - 1; + --j1; + } + + if (!s.isEmpty() && flag && j < s.length()) { + font.drawStringWithShadow(s.substring(j), j1, i1, i); + } + + if (flag1) { + if (flag2) { + drawRect(k1, i1 - 1, k1 + 1, i1 + 1 + 9, 0xffd0d0d0); + } else { + font.drawStringWithShadow("_", k1, i1, i); + } + } + + if (k != j) { + int l1 = l + font.getStringWidth(s.substring(0, k)); + drawSelectionBox(k1, i1 - 1, l1 - 1, i1 + 1 + 9); + } + } + } + + /** + * Draws the blue selection box. + */ + private void drawSelectionBox(int startX, int startY, int endX, int endY) { + if (startX < endX) { + int i = startX; + startX = endX; + endX = i; + } + + if (startY < endY) { + int j = startY; + startY = endY; + endY = j; + } + + if (endX > x + width) { + endX = x + width; + } + + if (startX > x + width) { + startX = x + width; + } + + Tessellator tessellator = Tessellator.instance; + GL11.glColor4f(0f, 0f, 255f, 255f); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_COLOR_LOGIC_OP); + GL11.glLogicOp(GL11.GL_OR_REVERSE); + tessellator.startDrawingQuads(); + tessellator.addVertex(startX, endY, 0.0D); + tessellator.addVertex(endX, endY, 0.0D); + tessellator.addVertex(endX, startY, 0.0D); + tessellator.addVertex(startX, startY, 0.0D); + tessellator.draw(); + GL11.glDisable(GL11.GL_COLOR_LOGIC_OP); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + + /** + * Sets the maximum length for the text in this text box. If the current text is longer than this length, the + * current text will be trimmed. + */ + @SuppressWarnings("unused") + public void setMaxStringLength(int length) { + maxStringLength = length; + + if (text.length() > length) { + text = text.substring(0, length); + } + } + + /** + * returns the maximum number of character that can be contained in this textbox + */ + public int getMaxStringLength() { + return maxStringLength; + } + + /** + * returns the current position of the cursor + */ + public int getCursorPosition() { + return cursorPosition; + } + + /** + * Gets whether the background and outline of this text box should be drawn (true if so). + */ + public boolean getEnableBackgroundDrawing() { + return enableBackgroundDrawing; + } + + /** + * Sets whether the background and outline of this text box should be drawn. + */ + @SuppressWarnings("unused") + public void setEnableBackgroundDrawing(boolean enableBackgroundDrawingIn) { + enableBackgroundDrawing = enableBackgroundDrawingIn; + } + + /** + * Sets the color to use when drawing this text box's text. A different color is used if this text box is disabled. + */ + @SuppressWarnings("unused") + public void setTextColor(int color) { + enabledColor = color; + } + + /** + * Sets the color to use for text in this text box when this text box is disabled. + */ + @SuppressWarnings("unused") + public void setDisabledTextColor(int color) { + disabledColor = color; + } + + /** + * Sets focus to this gui element + */ + public void setFocused(boolean isFocusedIn) { + if (isFocusedIn && !isFocused) { + cursorCounter = 0; + } + + isFocused = isFocusedIn; + } + + /** + * Getter for the focused field + */ + @SuppressWarnings("unused") + public boolean isFocused() { + return isFocused; + } + + /** + * Sets whether this text box is enabled. Disabled text boxes cannot be typed in. + */ + @SuppressWarnings("unused") + public void setEnabled(boolean enabled) { + isEnabled = enabled; + } + + /** + * the side of the selection that is not the cursor, may be the same as the cursor + */ + public int getSelectionEnd() { + return selectionEnd; + } + + /** + * returns the width of the textbox depending on if background drawing is enabled + */ + public int getWidth() { + return getEnableBackgroundDrawing() ? width - 8 : width; + } + + /** + * Sets the position of the selection anchor (the selection anchor and the cursor position mark the edges of the + * selection). If the anchor is set beyond the bounds of the current text, it will be put back inside. + */ + public void setSelectionPos(int position) { + int i = text.length(); + + if (position > i) { + position = i; + } + + if (position < 0) { + position = 0; + } + + selectionEnd = position; + + if (font != null) { + if (lineScrollOffset > i) { + lineScrollOffset = i; + } + + int j = getWidth(); + String s = trimStringToWidth(font, text.substring(lineScrollOffset), j); + int k = s.length() + lineScrollOffset; + + if (position == lineScrollOffset) { + lineScrollOffset -= trimStringToWidth(font, text, j, true).length(); + } + + if (position > k) { + lineScrollOffset += position - k; + } else if (position <= lineScrollOffset) { + lineScrollOffset -= lineScrollOffset - position; + } + + if (lineScrollOffset < 0) + lineScrollOffset = 0; + else if (lineScrollOffset > i) + lineScrollOffset = i; + } + } + + /** + * Sets whether this text box loses focus when something other than it is clicked. + */ + @SuppressWarnings("unused") + public void setCanLoseFocus(boolean canLoseFocusIn) { + canLoseFocus = canLoseFocusIn; + } + + /** + * returns true if this textbox is visible + */ + public boolean getVisible() { + return visible; + } + + /** + * Sets whether this textbox is visible + */ + @SuppressWarnings("unused") + public void setVisible(boolean isVisible) { + visible = isVisible; + } + + private static boolean isCtrlKeyDown() { + if (MinecraftAccessor.getOS() == EnumOS.macos) { + return Keyboard.isKeyDown(Keyboard.KEY_LMETA) || Keyboard.isKeyDown(Keyboard.KEY_RMETA); + } else { + return Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); + } + } + + private static boolean isShiftKeyDown() { + return Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private static boolean isAltKeyDown() { + return Keyboard.isKeyDown(Keyboard.KEY_LMENU) || Keyboard.isKeyDown(Keyboard.KEY_RMENU); + } + + private static boolean isKeyComboCtrlA(int keyCode) { + return keyCode == Keyboard.KEY_A && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } + + private static boolean isKeyComboCtrlC(int keyCode) { + return keyCode == Keyboard.KEY_C && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } + + private static boolean isKeyComboCtrlV(int keyCode) { + return keyCode == Keyboard.KEY_V && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } + + private static boolean isKeyComboCtrlX(int keyCode) { + return keyCode == Keyboard.KEY_X && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } + + private static void setClipboardString(String text) { + try { + StringSelection selection = new StringSelection(text); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null); + } catch (Exception ignore) { /* noop */ } + } + + private static String trimStringToWidth(Font font, String text, int maxWidth) { + return trimStringToWidth(font, text, maxWidth, false); + } + + private static String trimStringToWidth(Font font, String text, int maxWidth, boolean reverse) { + int width = 0; + int length; + for (length = 0; length < text.length() && width < maxWidth; length++) + width += font.getStringWidth(Character.toString(text.charAt(reverse ? text.length() - 1 - length : length))); + return reverse ? text.substring(text.length() - length) : text.substring(0, length); + } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/entries/ChildEntry.java b/src/main/java/io/github/prospector/modmenu/gui/entries/ChildEntry.java index 65831e077..69efc9820 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/entries/ChildEntry.java +++ b/src/main/java/io/github/prospector/modmenu/gui/entries/ChildEntry.java @@ -1,32 +1,37 @@ package io.github.prospector.modmenu.gui.entries; - import io.github.prospector.modmenu.gui.ModListEntry; import io.github.prospector.modmenu.gui.ModListWidget; import net.fabricmc.loader.api.ModContainer; import net.minecraft.client.Minecraft; public class ChildEntry extends ModListEntry { - private boolean bottomChild; - private ParentEntry parent; - - public ChildEntry(Minecraft mc, ModContainer container, ParentEntry parent, ModListWidget list, boolean bottomChild) { - super(mc, container, list); - this.bottomChild = bottomChild; - this.parent = parent; - } - - @Override - public void render(int index, int y, int x, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean isSelected, float delta) { - super.render(index, y, x, rowWidth, rowHeight, mouseX, mouseY, isSelected, delta); - x += 4; - int color = 0xFFA0A0A0; - drawRect(x, y - 2, x + 1, y + (bottomChild ? rowHeight / 2 : rowHeight + 2), color); - drawRect(x, y + rowHeight / 2, x + 7, y + rowHeight / 2 + 1, color); - } - - @Override - public int getXOffset() { - return 13; - } + private final boolean bottomChild; + + public ChildEntry(Minecraft client, ModContainer container, ModListWidget list, boolean bottomChild) { + super(client, container, list); + this.bottomChild = bottomChild; + } + + @Override + public void render(int index, int rowTop, int rowLeft, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean hovered, float delta) { + // Render the normal mod entry row (icon, name, badges, description) + super.render(index, rowTop, rowLeft, rowWidth, rowHeight, mouseX, mouseY, hovered, delta); + + // Draw the tree branch lines + int branchX = rowLeft + 4; + int color = 0xFFA0A0A0; + + // Vertical line + drawRect(branchX, rowTop - 2, branchX + 1, rowTop + (bottomChild ? rowHeight / 2 : rowHeight + 2), color); + + // Horizontal connector + int centerY = rowTop + rowHeight / 2; + drawRect(branchX, centerY, branchX + 7, centerY + 1, color); + } + + @Override + public int getXOffset() { + return 13; + } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/entries/IndependentEntry.java b/src/main/java/io/github/prospector/modmenu/gui/entries/IndependentEntry.java index cecc1aaa8..a573f6ec7 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/entries/IndependentEntry.java +++ b/src/main/java/io/github/prospector/modmenu/gui/entries/IndependentEntry.java @@ -1,14 +1,12 @@ package io.github.prospector.modmenu.gui.entries; - import io.github.prospector.modmenu.gui.ModListEntry; import io.github.prospector.modmenu.gui.ModListWidget; import net.fabricmc.loader.api.ModContainer; import net.minecraft.client.Minecraft; public class IndependentEntry extends ModListEntry { - - public IndependentEntry(Minecraft mc, ModContainer container, ModListWidget list) { - super(mc, container, list); - } + public IndependentEntry(Minecraft client, ModContainer container, ModListWidget list) { + super(client, container, list); + } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/entries/ParentEntry.java b/src/main/java/io/github/prospector/modmenu/gui/entries/ParentEntry.java index 604f1f0db..ae5b5cc60 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/entries/ParentEntry.java +++ b/src/main/java/io/github/prospector/modmenu/gui/entries/ParentEntry.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.gui.entries; - import io.github.prospector.modmenu.ModMenu; import io.github.prospector.modmenu.gui.ModListEntry; import io.github.prospector.modmenu.gui.ModListWidget; @@ -9,109 +8,126 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.render.Font; import net.minecraft.client.render.tessellator.Tessellator; -import org.lwjgl.input.Keyboard; +import org.lwjgl.glfw.GLFW; import org.lwjgl.opengl.GL11; import java.util.Arrays; import java.util.List; -import java.util.Objects; public class ParentEntry extends ModListEntry { - private static final String PARENT_MOD_TEXTURE = "/assets/" + ModMenu.MOD_ID + "/textures/gui/parent_mod.png"; - protected List children; - protected ModListWidget list; - protected boolean hoveringIcon = false; - - public ParentEntry(Minecraft mc, ModContainer parent, List children, ModListWidget list) { - super(mc, parent, list); - this.children = children; - this.list = list; - } - - @Override - public void render(int index, int y, int x, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean isSelected, float delta) { - super.render(index, y, x, rowWidth, rowHeight, mouseX, mouseY, isSelected, delta); - Font font = client.font; - int childrenBadgeHeight = 9; - int childrenBadgeWidth = 9; - int children = ModListSearch.search(list.getParent(), list.getParent().getSearchInput(), getChildren()).size(); - int childrenWidth = font.getStringWidth(Integer.toString(children)) - 1; - if (childrenBadgeWidth < childrenWidth + 4) { - childrenBadgeWidth = childrenWidth + 4; - } - int childrenBadgeX = x + 32 - childrenBadgeWidth; - int childrenBadgeY = y + 32 - childrenBadgeHeight; - int childrenOutlineColor = 0x8810d098; - int childrenFillColor = 0x88046146; - drawRect(childrenBadgeX + 1, childrenBadgeY, childrenBadgeX + childrenBadgeWidth - 1, childrenBadgeY + 1, childrenOutlineColor); - drawRect(childrenBadgeX, childrenBadgeY + 1, childrenBadgeX + 1, childrenBadgeY + childrenBadgeHeight - 1, childrenOutlineColor); - drawRect(childrenBadgeX + childrenBadgeWidth - 1, childrenBadgeY + 1, childrenBadgeX + childrenBadgeWidth, childrenBadgeY + childrenBadgeHeight - 1, childrenOutlineColor); - drawRect(childrenBadgeX + 1, childrenBadgeY + 1, childrenBadgeX + childrenBadgeWidth - 1, childrenBadgeY + childrenBadgeHeight - 1, childrenFillColor); - drawRect(childrenBadgeX + 1, childrenBadgeY + childrenBadgeHeight - 1, childrenBadgeX + childrenBadgeWidth - 1, childrenBadgeY + childrenBadgeHeight, childrenOutlineColor); - font.drawString(Integer.toString(children), childrenBadgeX + childrenBadgeWidth / 2 - childrenWidth / 2, childrenBadgeY + 1, 0xCACACA); - this.hoveringIcon = mouseX >= x - 1 && mouseX <= x - 1 + 32 && mouseY >= y - 1 && mouseY <= y - 1 + 32; - if (isMouseOver(mouseX, mouseY)) { - drawRect(x, y, x + 32, y + 32, 0xA0909090); - this.client.textureManager.bindTexture(this.client.textureManager.loadTexture(PARENT_MOD_TEXTURE)); - int xOffset = list.getParent().showModChildren.contains(getMetadata().getId()) ? 32 : 0; - int yOffset = hoveringIcon ? 32 : 0; - GL11.glColor4f(1f, 1f, 1f, 1f); - Tessellator tess = Tessellator.instance; - tess.startDrawingQuads(); - tess.addVertexWithUV(x, y, 0, xOffset / 256f, yOffset / 256f); - tess.addVertexWithUV(x, y + 32, 0, xOffset / 256f, (yOffset + 32) / 256f); - tess.addVertexWithUV(x + 32, y + 32, 0, (xOffset + 32) / 256f, (yOffset + 32) / 256f); - tess.addVertexWithUV(x + 32, y, 0, (xOffset + 32) / 256f, yOffset / 256f); - tess.draw(); - } - } - - @Override - public void mouseClicked(int mouseX, int mouseY, int i) { - if (hoveringIcon) { - String id = getMetadata().getId(); - if (list.getParent().showModChildren.contains(id)) { - list.getParent().showModChildren.remove(id); - } else { - list.getParent().showModChildren.add(id); - } - list.filter(list.getParent().getSearchInput(), false); - } - super.mouseClicked(mouseX, mouseY, i); - } - - @Override - public boolean keyPressed(int int_1, int int_2, int int_3) { - if (int_1 == Keyboard.KEY_RETURN) { - String id = getMetadata().getId(); - if (list.getParent().showModChildren.contains(id)) { - list.getParent().showModChildren.remove(id); - } else { - list.getParent().showModChildren.add(id); - } - list.filter(list.getParent().getSearchInput(), false); - return true; - } - return super.keyPressed(int_1, int_2, int_3); - } - - public void setChildren(List children) { - this.children = children; - } - - public void addChildren(List children) { - this.children.addAll(children); - } - - public void addChildren(ModContainer... children) { - this.children.addAll(Arrays.asList(children)); - } - - public List getChildren() { - return children; - } - - public boolean isMouseOver(double double_1, double double_2) { - return Objects.equals(this.list.getEntryAtPos(double_1, double_2), this); - } + private static final String PARENT_MOD_TEXTURE = "/assets/" + ModMenu.MOD_ID + "/textures/gui/parent_mod.png"; + + protected List children; + protected boolean hoveringIcon = false; + + public ParentEntry(Minecraft client, ModContainer parent, List children, ModListWidget list) { + super(client, parent, list); + this.children = children; + } + + @Override + public void render(int index, int rowTop, int rowLeft, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean hovered, float delta) { + super.render(index, rowTop, rowLeft, rowWidth, rowHeight, mouseX, mouseY, hovered, delta); + + Font font = this.client.font; + + int childrenBadgeHeight = 9; + int childrenBadgeWidth = 9; + + int childrenCount = ModListSearch.search(list.getParent(), list.getParent().getSearchInput(), getChildren()).size(); + + int childrenTextWidth = font.getStringWidth(Integer.toString(childrenCount)) - 1; + if (childrenBadgeWidth < childrenTextWidth + 4) { + childrenBadgeWidth = childrenTextWidth + 4; + } + + int childrenBadgeX = rowLeft + 32 - childrenBadgeWidth; + int childrenBadgeY = rowTop + 32 - childrenBadgeHeight; + + int outlineColor = 0x8810d098; + int fillColor = 0x88046146; + + // Outline + drawRect(childrenBadgeX + 1, childrenBadgeY, childrenBadgeX + childrenBadgeWidth - 1, childrenBadgeY + 1, outlineColor); + drawRect(childrenBadgeX, childrenBadgeY + 1, childrenBadgeX + 1, childrenBadgeY + childrenBadgeHeight - 1, outlineColor); + drawRect(childrenBadgeX + childrenBadgeWidth - 1, childrenBadgeY + 1, childrenBadgeX + childrenBadgeWidth, childrenBadgeY + childrenBadgeHeight - 1, outlineColor); + drawRect(childrenBadgeX + 1, childrenBadgeY + childrenBadgeHeight - 1, childrenBadgeX + childrenBadgeWidth - 1, childrenBadgeY + childrenBadgeHeight, outlineColor); + + // Fill + drawRect(childrenBadgeX + 1, childrenBadgeY + 1, childrenBadgeX + childrenBadgeWidth - 1, childrenBadgeY + childrenBadgeHeight - 1, fillColor); + + // Text + font.drawString(Integer.toString(childrenCount), childrenBadgeX + childrenBadgeWidth / 2 - childrenTextWidth / 2, childrenBadgeY + 1, 0xCACACA); + + // Icon hover detection (over the 32x32 icon area) + this.hoveringIcon = mouseX >= rowLeft - 1 && mouseX <= rowLeft - 1 + 32 && mouseY >= rowTop - 1 && mouseY <= rowTop - 1 + 32; + + // If the entry row is hovered, draw overlay and the parent-mod indicator texture + if (hovered) { + drawRect(rowLeft, rowTop, rowLeft + 32, rowTop + 32, 0xA0909090); + + this.client.textureManager.bindTexture(this.client.textureManager.loadTexture(PARENT_MOD_TEXTURE)); + + boolean childrenVisible = list.getParent().getShowModChildren().contains(getMetadata().getId()); + int xOffset = childrenVisible ? 32 : 0; + int yOffset = hoveringIcon ? 32 : 0; + + GL11.glColor4f(1f, 1f, 1f, 1f); + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + tess.addVertexWithUV(rowLeft, rowTop, 0, xOffset / 256f, yOffset / 256f); + tess.addVertexWithUV(rowLeft, rowTop + 32.0, 0, xOffset / 256f, (yOffset + 32) / 256f); + tess.addVertexWithUV(rowLeft + 32.0, rowTop + 32.0, 0, (xOffset + 32) / 256f, (yOffset + 32) / 256f); + tess.addVertexWithUV(rowLeft + 32.0, rowTop, 0, (xOffset + 32) / 256f, yOffset / 256f); + tess.draw(); + } + } + + @Override + public void mouseClicked(int mouseX, int mouseY, int button) { + if (hoveringIcon) { + String id = getMetadata().getId(); + if (list.getParent().getShowModChildren().contains(id)) { + list.getParent().getShowModChildren().remove(id); + } else { + list.getParent().getShowModChildren().add(id); + } + list.filter(list.getParent().getSearchInput(), false); + } + super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_ENTER) { + String id = getMetadata().getId(); + if (list.getParent().getShowModChildren().contains(id)) { + list.getParent().getShowModChildren().remove(id); + } else { + list.getParent().getShowModChildren().add(id); + } + list.filter(list.getParent().getSearchInput(), false); + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @SuppressWarnings("unused") + public void setChildren(List children) { + this.children = children; + } + + @SuppressWarnings("unused") + public void addChildren(List children) { + this.children.addAll(children); + } + + @SuppressWarnings("unused") + public void addChildren(ModContainer... children) { + this.children.addAll(Arrays.asList(children)); + } + + public List getChildren() { + return children; + } } diff --git a/src/main/java/io/github/prospector/modmenu/impl/ModMenuApiImpl.java b/src/main/java/io/github/prospector/modmenu/impl/ModMenuApiImpl.java index 4496da662..a84d68b2f 100644 --- a/src/main/java/io/github/prospector/modmenu/impl/ModMenuApiImpl.java +++ b/src/main/java/io/github/prospector/modmenu/impl/ModMenuApiImpl.java @@ -1,15 +1,9 @@ package io.github.prospector.modmenu.impl; - import io.github.prospector.modmenu.api.ModMenuApi; import io.github.prospector.modmenu.util.TriConsumer; public class ModMenuApiImpl implements ModMenuApi { - @Override - public String getModId() { - return "modmenu"; - } - @Override public void attachCustomBadges(TriConsumer consumer) { consumer.accept("Mod Menu", 0xff7a2b7c, 0xff510d54); diff --git a/src/main/java/io/github/prospector/modmenu/mixin/GuiButtonAccessor.java b/src/main/java/io/github/prospector/modmenu/mixin/GuiButtonAccessor.java index f7fb5fe43..62e9d6b4c 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/GuiButtonAccessor.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/GuiButtonAccessor.java @@ -1,18 +1,15 @@ package io.github.prospector.modmenu.mixin; - import net.minecraft.client.gui.ButtonElement; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(value = ButtonElement.class, remap = false) public interface GuiButtonAccessor { - @Accessor - int getWidth(); - - @Accessor - void setWidth(int width); - - @Accessor - void setHeight(int height); + @Accessor + int getWidth(); + @Accessor + void setWidth(int width); + @Accessor + void setHeight(int height); } diff --git a/src/main/java/io/github/prospector/modmenu/mixin/LanguageAccessor.java b/src/main/java/io/github/prospector/modmenu/mixin/LanguageAccessor.java index 6ea8a752f..83356ebba 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/LanguageAccessor.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/LanguageAccessor.java @@ -6,10 +6,7 @@ import java.util.Properties; -@Mixin( - value = Language.class, - remap = false -) +@Mixin(value = Language.class, remap = false) public interface LanguageAccessor { @Accessor Properties getEntries(); diff --git a/src/main/java/io/github/prospector/modmenu/mixin/MinecraftAccessor.java b/src/main/java/io/github/prospector/modmenu/mixin/MinecraftAccessor.java index fd5e6eb41..36b76c841 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/MinecraftAccessor.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/MinecraftAccessor.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.mixin; - import net.minecraft.client.Minecraft; import net.minecraft.core.enums.EnumOS; import org.spongepowered.asm.mixin.Mixin; @@ -8,8 +7,8 @@ @Mixin(value = Minecraft.class, remap = false) public interface MinecraftAccessor { - @Invoker("getOs") - static EnumOS getOS() { - throw new AssertionError("This should never be thrown"); - } + @Invoker("getOs") + static EnumOS getOS() { + throw new AssertionError("This should never be thrown"); + } } diff --git a/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiIngameMenu.java b/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiIngameMenu.java index 50678bb64..4f1cb0a75 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiIngameMenu.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiIngameMenu.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.mixin; - import io.github.prospector.modmenu.ModMenu; import io.github.prospector.modmenu.gui.ModListScreen; import io.github.prospector.modmenu.gui.ModMenuButtonWidget; @@ -14,19 +13,17 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(value = ScreenPause.class, remap = false) -public class MixinGuiIngameMenu extends Screen { - @SuppressWarnings("unchecked") - @Inject(at = @At("RETURN"), method = "init") - public void modmenu$drawMenuButton(CallbackInfo info) { - I18n i18n = I18n.getInstance(); - String buttonText = i18n.translateKey("modmenu.title") + " " + i18n.translateKeyAndFormat("modmenu.loaded", ModMenu.getFormattedModCount()); - this.buttons.add(new ModMenuButtonWidget(100, this.width / 2 - 100, this.height / 4 + 72 - 16, 200, 20, buttonText)); - } - - @Inject(method = "buttonClicked", at = @At("HEAD")) - private void modmenu$onActionPerformed(ButtonElement button, CallbackInfo ci) { - if (button.id == 100) { - mc.displayScreen(new ModListScreen(this)); - } - } +public abstract class MixinGuiIngameMenu extends Screen { + @Inject(method = "init", at = @At("RETURN")) + private void modmenu$drawMenuButton(CallbackInfo info) { + I18n i18n = I18n.getInstance(); + String buttonText = i18n.translateKey("modmenu.title") + " " + i18n.translateKeyAndFormat("modmenu.loaded", ModMenu.getFormattedModCount()); + this.buttons.add(new ModMenuButtonWidget(100, this.width / 2 - 100, this.height / 4 + 72 - 16, 200, 20, buttonText)); + } + @Inject(method = "buttonClicked", at = @At("HEAD")) + private void modmenu$onActionPerformed(ButtonElement button, CallbackInfo ci) { + if (button.id == 100) { + mc.displayScreen(new ModListScreen(this)); + } + } } diff --git a/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiMainMenu.java b/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiMainMenu.java index 001a19714..6b87682f2 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiMainMenu.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiMainMenu.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.mixin; - import io.github.prospector.modmenu.ModMenu; import io.github.prospector.modmenu.gui.ModListScreen; import io.github.prospector.modmenu.gui.ModMenuButtonWidget; @@ -8,7 +7,9 @@ import net.minecraft.client.gui.Screen; import net.minecraft.client.gui.ScreenMainMenu; import net.minecraft.core.lang.I18n; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -16,22 +17,26 @@ import java.util.Random; @Mixin(value = ScreenMainMenu.class, remap = false) -public class MixinGuiMainMenu extends Screen { - @Inject(at = @At("RETURN"), method = "init") - public void modmenu$drawMenuButton(CallbackInfo info) { - I18n i18n = I18n.getInstance(); - ButtonElement texturePackButton = this.buttons.get(2); - texturePackButton.displayString = new Random().nextInt(1000) == 0 ? "Twin Peaks" : i18n.translateKey("gui.main_menu.button.texture_packs"); - int newWidth = ((GuiButtonAccessor) texturePackButton).getWidth() / 2 - 1; - ((GuiButtonAccessor) texturePackButton).setWidth(newWidth); - String buttonText = i18n.translateKey("modmenu.title") + " " + i18n.translateKeyAndFormat("modmenu.loaded", ModMenu.getFormattedModCount()); - this.buttons.add(new ModMenuButtonWidget(100, this.width / 2 + 2, texturePackButton.yPosition, newWidth, 20, buttonText)); - } +public abstract class MixinGuiMainMenu extends Screen { + @Shadow + @Final + private static Random rand; + + @Inject(method = "init", at = @At("RETURN")) + private void modmenu$drawMenuButton(CallbackInfo info) { + I18n i18n = I18n.getInstance(); + ButtonElement texturePackButton = this.buttons.get(2); + texturePackButton.displayString = rand.nextInt(1000) == 0 ? "Twin Peaks" : i18n.translateKey("gui.main_menu.button.texture_packs"); + int newWidth = ((GuiButtonAccessor) texturePackButton).getWidth() / 2 - 1; + ((GuiButtonAccessor) texturePackButton).setWidth(newWidth); + String buttonText = i18n.translateKey("modmenu.title") + " " + i18n.translateKeyAndFormat("modmenu.loaded", ModMenu.getFormattedModCount()); + this.buttons.add(new ModMenuButtonWidget(100, this.width / 2 + 2, texturePackButton.yPosition, newWidth, 20, buttonText)); + } - @Inject(method = "buttonClicked", at = @At("HEAD")) - private void modmenu$onActionPerformed(ButtonElement button, CallbackInfo ci) { - if (button.id == 100) { - mc.displayScreen(new ModListScreen(this)); - } - } + @Inject(method = "buttonClicked", at = @At("HEAD")) + private void modmenu$onActionPerformed(ButtonElement button, CallbackInfo ci) { + if (button.id == 100) { + mc.displayScreen(new ModListScreen(this)); + } + } } diff --git a/src/main/java/io/github/prospector/modmenu/mixin/MixinI18n.java b/src/main/java/io/github/prospector/modmenu/mixin/MixinI18n.java index e24d43af9..05eedc6ee 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/MixinI18n.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/MixinI18n.java @@ -1,5 +1,6 @@ package io.github.prospector.modmenu.mixin; +import io.github.prospector.modmenu.ModMenu; import net.minecraft.core.lang.I18n; import net.minecraft.core.lang.Language; import org.spongepowered.asm.mixin.Mixin; @@ -15,18 +16,14 @@ import java.util.Properties; @Mixin(value = I18n.class, remap = false) -public class MixinI18n { - @Shadow private Language currentLanguage; - +public abstract class MixinI18n { + @Shadow + private Language currentLanguage; @Shadow public static InputStream getResourceAsStream(String path) { throw new AssertionError(); } - - @Inject( - method = "reload(Ljava/lang/String;Z)V", - at = @At("TAIL") - ) + @Inject(method = "reload(Ljava/lang/String;Z)V", at = @At("TAIL")) private void modmenu$addLangEntries(String languageCode, boolean save, CallbackInfo ci) { Properties entries = ((LanguageAccessor) currentLanguage).getEntries(); String lang = "/lang/modmenu/" + currentLanguage.getId() + ".lang"; @@ -36,7 +33,7 @@ public static InputStream getResourceAsStream(String path) { entries.load(r); } } catch (IOException e) { - e.printStackTrace(); + ModMenu.LOGGER.error("Something went wrong!", e); } String defaultLang = "/lang/modmenu/en_US.lang"; try (InputStream stream = getResourceAsStream(defaultLang)) { @@ -45,7 +42,7 @@ public static InputStream getResourceAsStream(String path) { ((LanguageAccessor) (Object) Language.Default.INSTANCE).getEntries().load(r); } } catch (IOException e) { - e.printStackTrace(); + ModMenu.LOGGER.error("Something went wrong!", e); } } } diff --git a/src/main/java/io/github/prospector/modmenu/mixin/MixinTexturePacks.java b/src/main/java/io/github/prospector/modmenu/mixin/MixinTexturePacks.java index 5ad18fdd3..2f4cf6f9d 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/MixinTexturePacks.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/MixinTexturePacks.java @@ -1,21 +1,20 @@ package io.github.prospector.modmenu.mixin; - +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import io.github.prospector.modmenu.ModMenu; import net.minecraft.client.render.texturepack.TexturePack; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.io.InputStream; -@Mixin(value = {TexturePack.class}, remap = false) -public class MixinTexturePacks { - @Inject(method = "getResourceAsStream", at = @At(value = "INVOKE", target = "Ljava/lang/Class;getResourceAsStream(Ljava/lang/String;)Ljava/io/InputStream;", remap = false), cancellable = true) - private void modmenu$onGetResource(String resource, CallbackInfoReturnable ci) { - InputStream in = ModMenu.class.getClassLoader().getResourceAsStream(resource); - if (in != null) - ci.setReturnValue(in); - } +@Mixin(value = TexturePack.class, remap = false) +public abstract class MixinTexturePacks { + @WrapOperation(method = "getResourceAsStream", at = @At(value = "INVOKE", target = "Ljava/lang/Class;getResourceAsStream(Ljava/lang/String;)Ljava/io/InputStream;", remap = false)) + private InputStream modmenu$onGetResource(Class instance, String name, Operation original) { + InputStream inputStream = ModMenu.class.getClassLoader().getResourceAsStream(name); + if (inputStream != null) return inputStream; + return original.call(instance, name); + } } diff --git a/src/main/java/io/github/prospector/modmenu/mixin/ModMenuMixinConfigPlugin.java b/src/main/java/io/github/prospector/modmenu/mixin/ModMenuMixinConfigPlugin.java index cdae7711b..254c355d9 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/ModMenuMixinConfigPlugin.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/ModMenuMixinConfigPlugin.java @@ -5,18 +5,17 @@ import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; import org.spongepowered.asm.mixin.extensibility.IMixinInfo; +import java.util.Collections; import java.util.List; import java.util.Set; public class ModMenuMixinConfigPlugin implements IMixinConfigPlugin { @Override public void onLoad(String mixinPackage) {} - @Override public String getRefMapperConfig() { return null; } - @Override public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { // Only applies when Halplibe is not present @@ -25,18 +24,14 @@ public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { } return true; } - @Override public void acceptTargets(Set myTargets, Set otherTargets) {} - @Override public List getMixins() { - return null; + return Collections.emptyList(); } - @Override public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {} - @Override public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {} } diff --git a/src/main/java/io/github/prospector/modmenu/mixin/TextFieldEditorAccessor.java b/src/main/java/io/github/prospector/modmenu/mixin/TextFieldEditorAccessor.java index ae9815373..501fc5fc3 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/TextFieldEditorAccessor.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/TextFieldEditorAccessor.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.mixin; - import net.minecraft.client.gui.text.TextFieldEditor; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Invoker; diff --git a/src/main/java/io/github/prospector/modmenu/util/BadgeRenderer.java b/src/main/java/io/github/prospector/modmenu/util/BadgeRenderer.java index 5c4422320..3af3de6eb 100644 --- a/src/main/java/io/github/prospector/modmenu/util/BadgeRenderer.java +++ b/src/main/java/io/github/prospector/modmenu/util/BadgeRenderer.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.util; - import io.github.prospector.modmenu.ModMenu; import io.github.prospector.modmenu.gui.ModListScreen; import net.fabricmc.loader.api.ModContainer; @@ -12,68 +11,73 @@ import java.util.Map; public class BadgeRenderer { - protected int startX, startY, badgeX, badgeY, badgeMax; - protected ModContainer container; - protected ModMetadata metadata; - protected Minecraft client; - protected final ModListScreen screen; + protected int startX; + protected int startY; + protected int badgeX; + protected int badgeY; + protected int badgeMax; + protected ModContainer container; + protected ModMetadata metadata; + protected Minecraft client; + protected final ModListScreen screen; - public BadgeRenderer(Minecraft client, int startX, int startY, int endX, ModContainer container, ModListScreen screen) { - this.startX = startX; - this.startY = startY; - this.badgeMax = endX; - this.container = container; - this.metadata = container.getMetadata(); - this.screen = screen; - this.client = client; - } + public BadgeRenderer(Minecraft client, int startX, int startY, int endX, ModContainer container, ModListScreen screen) { + this.startX = startX; + this.startY = startY; + this.badgeMax = endX; + this.container = container; + this.metadata = container.getMetadata(); + this.screen = screen; + this.client = client; + } - public void draw(int mouseX, int mouseY) { - I18n i18n = I18n.getInstance(); - this.badgeX = startX; - this.badgeY = startY; - if (ModMenu.LIBRARY_MODS.contains(metadata.getId())) { - drawBadge(i18n.translateKey("modmenu.library"), 0x8810d098, 0x88046146, mouseX, mouseY); - } - if (ModMenu.CLIENTSIDE_MODS.contains(metadata.getId())) { - drawBadge(i18n.translateKey("modmenu.clientsideOnly"), 0x884383E3, 0x880E4699, mouseX, mouseY); - } + public void draw(int mouseX, int mouseY) { + I18n i18n = I18n.getInstance(); + this.badgeX = startX; + this.badgeY = startY; + if (ModMenu.LIBRARY_MODS.contains(metadata.getId())) { + drawBadge(i18n.translateKey("modmenu.library"), 0x8810d098, 0x88046146, mouseX, mouseY); + } + if (ModMenu.CLIENTSIDE_MODS.contains(metadata.getId())) { + drawBadge(i18n.translateKey("modmenu.clientsideOnly"), 0x884383E3, 0x880E4699, mouseX, mouseY); + } if (ModMenu.DEPRECATED_MODS.contains(metadata.getId())) { drawBadge(i18n.translateKey("modmenu.deprecated"), 0xFF841426, 0xFF530C17, mouseX, mouseY); } - if (ModMenu.PATCHWORK_FORGE_MODS.contains(metadata.getId())) { - drawBadge(i18n.translateKey("modmenu.forge"), 0x887C89A3, 0x88202C43, mouseX, mouseY); - } - if (metadata.getId().equals("minecraft")) { - drawBadge(i18n.translateKey("modmenu.minecraft"), 0x88BCBCBC, 0x88535353, mouseX, mouseY); - } + if (ModMenu.PATCHWORK_FORGE_MODS.contains(metadata.getId())) { + drawBadge(i18n.translateKey("modmenu.forge"), 0x887C89A3, 0x88202C43, mouseX, mouseY); + } + if (metadata.getId().equals("minecraft")) { + drawBadge(i18n.translateKey("modmenu.minecraft"), 0x88BCBCBC, 0x88535353, mouseX, mouseY); + } if (ModMenu.CUSTOM_BADGE_MODS.containsKey(metadata.getId())) { Map> map = ModMenu.CUSTOM_BADGE_MODS.get(metadata.getId()); for (Map.Entry> entry : map.entrySet()) { drawBadge(entry.getKey(), entry.getValue().getKey(), entry.getValue().getValue(), mouseX, mouseY); } } - //noinspection MagicConstant - if (Calendar.getInstance().get(0b10) == 0b11 && Calendar.getInstance().get(0b101) == 0x1) { - if (metadata.getId().equals(new String(new byte[]{109, 111, 100, 109, 101, 110, 117}))) { - drawBadge(new String(new byte[]{-30, -100, -104, 32, 86, 105, 114, 117, 115, 32, 68, 101, 116, 101, 99, 116, 101, 100}), 0b10001000111111110010001000100010, 0b10001000011111110000100000001000, mouseX, mouseY); - } else if (metadata.getId().contains(new String(new byte[]{116, 97, 116, 101, 114}))) { - drawBadge(new String(new byte[]{116, 97, 116, 101, 114}), 0b10001000111010111011001100101011, 0b10001000100110010111000100010010, mouseX, mouseY); - } else { - drawBadge(new String(new byte[]{-30, -100, -108, 32, 98, 121, 32, 77, 99, 65, 102, 101, 101}), 0b10001000000111011111111101001000, 0b10001000000001110110100100001110, mouseX, mouseY); - } - } - } + //noinspection MagicConstant + if (Calendar.getInstance().get(0b10) == 0b11 && Calendar.getInstance().get(0b101) == 0x1) { + if (metadata.getId().equals(new String(new byte[]{109, 111, 100, 109, 101, 110, 117}))) { + drawBadge(new String(new byte[]{-30, -100, -104, 32, 86, 105, 114, 117, 115, 32, 68, 101, 116, 101, 99, 116, 101, 100}), 0b10001000111111110010001000100010, 0b10001000011111110000100000001000, mouseX, mouseY); + } else if (metadata.getId().contains(new String(new byte[]{116, 97, 116, 101, 114}))) { + drawBadge(new String(new byte[]{116, 97, 116, 101, 114}), 0b10001000111010111011001100101011, 0b10001000100110010111000100010010, mouseX, mouseY); + } else { + drawBadge(new String(new byte[]{-30, -100, -108, 32, 98, 121, 32, 77, 99, 65, 102, 101, 101}), 0b10001000000111011111111101001000, 0b10001000000001110110100100001110, mouseX, mouseY); + } + } + } - public void drawBadge(String text, int outlineColor, int fillColor, int mouseX, int mouseY) { - int width = client.font.getStringWidth(text) + 6; - if (badgeX + width < badgeMax) { - RenderUtils.INSTANCE.drawBadge(client.font, badgeX, badgeY, width, text, outlineColor, fillColor, 0xCACACA); - badgeX += width + 3; - } - } + @SuppressWarnings("unused") + public void drawBadge(String text, int outlineColor, int fillColor, int mouseX, int mouseY) { + int width = client.font.getStringWidth(text) + 6; + if (badgeX + width < badgeMax) { + RenderUtils.INSTANCE.drawBadge(client.font, badgeX, badgeY, width, text, outlineColor, fillColor, 0xCACACA); + badgeX += width + 3; + } + } - public ModMetadata getMetadata() { - return metadata; - } + public ModMetadata getMetadata() { + return metadata; + } } diff --git a/src/main/java/io/github/prospector/modmenu/util/ButtonUtil.java b/src/main/java/io/github/prospector/modmenu/util/ButtonUtil.java index 29ba70825..d1a10bbfd 100644 --- a/src/main/java/io/github/prospector/modmenu/util/ButtonUtil.java +++ b/src/main/java/io/github/prospector/modmenu/util/ButtonUtil.java @@ -1,15 +1,14 @@ package io.github.prospector.modmenu.util; - import io.github.prospector.modmenu.mixin.GuiButtonAccessor; import net.minecraft.client.gui.ButtonElement; public final class ButtonUtil { - public static ButtonElement createButton(int buttonId, int x, int y, int width, int height, String text) { - ButtonElement button = new ButtonElement(buttonId, x, y, text); - GuiButtonAccessor accessor = (GuiButtonAccessor) button; - accessor.setWidth(width); - accessor.setHeight(height); - return button; - } + public static ButtonElement createButton(int buttonId, int x, int y, int width, int height, String text) { + ButtonElement button = new ButtonElement(buttonId, x, y, text); + GuiButtonAccessor accessor = (GuiButtonAccessor) button; + accessor.setWidth(width); + accessor.setHeight(height); + return button; + } } diff --git a/src/main/java/io/github/prospector/modmenu/util/HardcodedUtil.java b/src/main/java/io/github/prospector/modmenu/util/HardcodedUtil.java index 94b509d61..31ffed15d 100644 --- a/src/main/java/io/github/prospector/modmenu/util/HardcodedUtil.java +++ b/src/main/java/io/github/prospector/modmenu/util/HardcodedUtil.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.util; - import io.github.prospector.modmenu.ModMenu; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; @@ -11,97 +10,71 @@ import java.util.regex.Pattern; public final class HardcodedUtil { - private static final Pattern FABRIC_PATTERN = Pattern.compile("^fabric-.*(-v\\d+)$"); - private static final Set FABRIC_MODS = new HashSet<>(); - private static final HashMap HARDCODED_DESCRIPTIONS = new HashMap<>(); + private static final Pattern FABRIC_PATTERN = Pattern.compile("^fabric-.*(-v\\d+)$"); + private static final Set FABRIC_MODS = new HashSet<>(); + private static final HashMap HARDCODED_DESCRIPTIONS = new HashMap<>(); + private static final Random RANDOM = new Random(); - public static void initializeHardcodings() { - /*FABRIC_MODS.add("fabric"); - FABRIC_MODS.add("fabricloader"); - HARDCODED_DESCRIPTIONS.put("fabric-api-base", "Contains the essentials for Fabric API modules."); - HARDCODED_DESCRIPTIONS.put("fabric-biomes-v1", "Hooks for adding biomes to the default world generator."); - HARDCODED_DESCRIPTIONS.put("fabric-commands-v0", "Adds command-related hooks."); - HARDCODED_DESCRIPTIONS.put("fabric-containers-v0", "Adds hooks for containers."); - HARDCODED_DESCRIPTIONS.put("fabric-content-registries-v0", "Adds registries for vanilla mechanics that are missing them."); - HARDCODED_DESCRIPTIONS.put("fabric-crash-report-info-v1", "Adds Fabric-related debug info to crash reports."); - HARDCODED_DESCRIPTIONS.put("fabric-events-interaction-v0", "Events for player interaction with blocks and entities."); - HARDCODED_DESCRIPTIONS.put("fabric-events-lifecycle-v0", "Events for the game's lifecycle."); - HARDCODED_DESCRIPTIONS.put("fabric-item-groups-v0", "An API for adding custom item groups."); - HARDCODED_DESCRIPTIONS.put("fabric-keybindings-v0", "Keybinding registry API."); - HARDCODED_DESCRIPTIONS.put("fabric-loot-tables-v1", "Hooks for manipulating loot tables."); - HARDCODED_DESCRIPTIONS.put("fabric-mining-levels-v0", "Block mining level tags for tools."); - HARDCODED_DESCRIPTIONS.put("fabric-models-v0", "Hooks for models and model loading."); - HARDCODED_DESCRIPTIONS.put("fabric-networking-blockentity-v0", "Networking hooks for block entities."); - HARDCODED_DESCRIPTIONS.put("fabric-networking-v0", "Networking packet hooks and registries."); - HARDCODED_DESCRIPTIONS.put("fabric-object-builders-v0", "Builders for objects vanilla has locked down."); - HARDCODED_DESCRIPTIONS.put("fabric-registry-sync-v0", "Syncs registry mappings."); - HARDCODED_DESCRIPTIONS.put("fabric-renderer-api-v1", "Defines rendering extensions for dynamic/fancy block and item models."); - HARDCODED_DESCRIPTIONS.put("fabric-renderer-indigo", "Default implementation of the Fabric Renderer API."); - HARDCODED_DESCRIPTIONS.put("fabric-rendering-data-attachment-v1", "Thread-safe hooks for BlockEntity data use during terrain rendering."); - HARDCODED_DESCRIPTIONS.put("fabric-rendering-fluids-v1", "Hooks for registering fluid renders."); - HARDCODED_DESCRIPTIONS.put("fabric-rendering-v0", "Hooks and registries for rendering-related things"); - HARDCODED_DESCRIPTIONS.put("fabric-resource-loader-v0", "Asset and data resource loading."); - HARDCODED_DESCRIPTIONS.put("fabric-tag-extensions-v0", "Hooks for tags."); - HARDCODED_DESCRIPTIONS.put("fabric-textures-v0", "Hooks for texture loading and registration.");*/ - HARDCODED_DESCRIPTIONS.put("minecraft", new Random().nextInt(1000) == 0 ? "The based game." : "The base game."); - } + public static void initializeHardcodings() { + HARDCODED_DESCRIPTIONS.put("minecraft", RANDOM.nextInt(1000) == 0 ? "The based game." : "The base game."); + } - public static void hardcodeModuleMetadata(ModContainer mod, ModMetadata metadata, String id) { - Matcher matcher = FABRIC_PATTERN.matcher(id); - if (matcher.matches() || id.equals("fabric-api-base") || id.equals("fabric-renderer-indigo")) { - FABRIC_MODS.add(id); - if (FabricLoader.getInstance().isModLoaded("fabric")) { - Optional parent = FabricLoader.getInstance().getModContainer("fabric"); - parent.ifPresent(modContainer -> ModMenu.PARENT_MAP.put(modContainer, mod)); - } - ModMenu.addLibraryMod(id); - if (id.equals("fabric-keybindings-v0") || id.equals("fabric-models-v0") || id.equals("fabric-renderer-api-v1") || id.equals("fabric-renderer-indigo") || id.equals("fabric-rendering-fluids-v1") || id.equals("fabric-rendering-v0") || id.equals("fabric-textures-v0")) { - ModMenu.CLIENTSIDE_MODS.add(id); - } - } - if (id.equals("fabricloader") || id.equals("fabric") || metadata.getName().endsWith(" API")) { - ModMenu.addLibraryMod(id); - } - } + public static void hardcodeModuleMetadata(ModContainer mod, ModMetadata metadata, String id) { + Matcher matcher = FABRIC_PATTERN.matcher(id); + if (matcher.matches() || id.equals("fabric-api-base") || id.equals("fabric-renderer-indigo")) { + FABRIC_MODS.add(id); + if (FabricLoader.getInstance().isModLoaded("fabric")) { + Optional parent = FabricLoader.getInstance().getModContainer("fabric"); + parent.ifPresent(modContainer -> ModMenu.PARENT_MAP.put(modContainer, mod)); + } + ModMenu.addLibraryMod(id); + if (id.equals("fabric-keybindings-v0") || id.equals("fabric-models-v0") || id.equals("fabric-renderer-api-v1") || id.equals("fabric-renderer-indigo") || id.equals("fabric-rendering-fluids-v1") || id.equals("fabric-rendering-v0") || id.equals("fabric-textures-v0")) { + ModMenu.CLIENTSIDE_MODS.add(id); + } + } + if (id.equals("fabricloader") || id.equals("fabric") || metadata.getName().endsWith(" API")) { + ModMenu.addLibraryMod(id); + } + } - public static String formatFabricModuleName(String name) { - Matcher matcher = FABRIC_PATTERN.matcher(name); - if (matcher.matches() || name.equals("fabric-renderer-indigo") || name.equals("fabric-api-base")) { - if (matcher.matches()) { - String v = matcher.group(1); - name = capitalize(name.replace(v, "").replace("-", " ")); - name = name + " (" + v.replace("-", "") + ")"; - } else { - name = capitalize(name.replace("-", " ")); - } - name = name.replace("Api", "API"); - name = name.replace("Blockentity", "BlockEntity"); - } - return name; - } + public static String formatFabricModuleName(String name) { + Matcher matcher = FABRIC_PATTERN.matcher(name); + if (matcher.matches() || name.equals("fabric-renderer-indigo") || name.equals("fabric-api-base")) { + if (matcher.matches()) { + String v = matcher.group(1); + name = capitalize(name.replace(v, "").replace("-", " ")); + name = name + " (" + v.replace("-", "") + ")"; + } else { + name = capitalize(name.replace("-", " ")); + } + name = name.replace("Api", "API"); + name = name.replace("Blockentity", "BlockEntity"); + } + return name; + } - private static String capitalize(String str) { - StringBuilder sb = new StringBuilder(); - boolean capitalizeNext = true; - for (int i = 0; i < str.length(); i++) { - char c = str.charAt(i); - if (capitalizeNext) - c = Character.toTitleCase(c); - capitalizeNext = Character.isWhitespace(c); - sb.append(c); - } - return sb.toString(); - } + private static String capitalize(String str) { + StringBuilder sb = new StringBuilder(); + boolean capitalizeNext = true; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (capitalizeNext) + c = Character.toTitleCase(c); + capitalizeNext = Character.isWhitespace(c); + sb.append(c); + } + return sb.toString(); + } - public static String getHardcodedDescription(String id) { - return HARDCODED_DESCRIPTIONS.getOrDefault(id, ""); - } + public static String getHardcodedDescription(String id) { + return HARDCODED_DESCRIPTIONS.getOrDefault(id, ""); + } - public static Set getFabricMods() { - return FABRIC_MODS; - } + public static Set getFabricMods() { + return FABRIC_MODS; + } - public static HashMap getHardcodedDescriptions() { - return HARDCODED_DESCRIPTIONS; - } + public static Map getHardcodedDescriptions() { + return HARDCODED_DESCRIPTIONS; + } } diff --git a/src/main/java/io/github/prospector/modmenu/util/ModListSearch.java b/src/main/java/io/github/prospector/modmenu/util/ModListSearch.java index a21e9e567..106056b76 100644 --- a/src/main/java/io/github/prospector/modmenu/util/ModListSearch.java +++ b/src/main/java/io/github/prospector/modmenu/util/ModListSearch.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.util; - import io.github.prospector.modmenu.ModMenu; import io.github.prospector.modmenu.gui.ModListScreen; import net.fabricmc.loader.api.ModContainer; @@ -14,54 +13,56 @@ public final class ModListSearch { - public static boolean validSearchQuery(String query) { - return query != null && !query.isEmpty(); - } + public static boolean validSearchQuery(String query) { + return query != null && !query.isEmpty(); + } - public static List search(ModListScreen screen, String query, List candidates) { - if (!validSearchQuery(query)) { - return candidates; - } - return candidates.stream() - .filter(modContainer -> passesFilters(screen, modContainer, query.toLowerCase(Locale.ROOT))) - .collect(Collectors.toList()); - } + public static List search(ModListScreen screen, String query, List candidates) { + if (!validSearchQuery(query)) { + return candidates; + } + return candidates.stream() + .filter(modContainer -> passesFilters(screen, modContainer, query.toLowerCase(Locale.ROOT))) + .collect(Collectors.toList()); + } - private static boolean passesFilters(ModListScreen screen, ModContainer container, String query) { - ModMetadata metadata = container.getMetadata(); - String modId = metadata.getId(); + @SuppressWarnings("unused") + private static boolean passesFilters(ModListScreen screen, ModContainer container, String query) { + ModMetadata metadata = container.getMetadata(); + String modId = metadata.getId(); - //Some basic search, could do with something more advanced but this will do for now - if (HardcodedUtil.formatFabricModuleName(metadata.getName()).toLowerCase(Locale.ROOT).contains(query) //Search mod name - || modId.toLowerCase(Locale.ROOT).contains(query) // Search mod name - || authorMatches(container, query) //Search via author - || (ModMenu.LIBRARY_MODS.contains(modId) && "api library".contains(query)) //Search for lib mods - || ("clientside".contains(query) && ModMenu.CLIENTSIDE_MODS.contains(modId)) //Search for clientside mods - || ("deprecated".contains(query) && ModMenu.DEPRECATED_MODS.contains(modId)) //Search for clientside mods - || ("configurations configs configures configurable".contains(query) && ModMenu.hasConfigScreenFactory(modId)) //Search for mods that can be configured - ) { - return true; - } + //Some basic search, could do with something more advanced but this will do for now + if (HardcodedUtil.formatFabricModuleName(metadata.getName()).toLowerCase(Locale.ROOT).contains(query) //Search mod name + || modId.toLowerCase(Locale.ROOT).contains(query) // Search mod name + || authorMatches(container, query) //Search via author + || (ModMenu.LIBRARY_MODS.contains(modId) && "api library".contains(query)) //Search for lib mods + || ("clientside".contains(query) && ModMenu.CLIENTSIDE_MODS.contains(modId)) //Search for clientside mods + || ("deprecated".contains(query) && ModMenu.DEPRECATED_MODS.contains(modId)) //Search for clientside mods + || ("configurations configs configures configurable".contains(query) && ModMenu.hasConfigScreenFactory(modId)) //Search for mods that can be configured + ) { + return true; + } - //Allow parent to pass filter if a child passes - if (ModMenu.PARENT_MAP.keySet().contains(container)) { - for (ModContainer child : ModMenu.PARENT_MAP.get(container)) { - if (passesFilters(screen, child, query)) { - return true; - } - } - } - return false; - } + //Allow parent to pass filter if a child passes + if (ModMenu.PARENT_MAP.keySet().contains(container)) { + for (ModContainer child : ModMenu.PARENT_MAP.get(container)) { + if (child == null) continue; + if (passesFilters(screen, child, query)) { + return true; + } + } + } + return false; + } - private static boolean authorMatches(ModContainer modContainer, String query) { - return modContainer.getMetadata().getAuthors().stream() - .filter(Objects::nonNull) - .map(Person::getName) - .filter(Objects::nonNull) - .map(s -> s.toLowerCase(Locale.ROOT)) - .anyMatch(s -> s.contains(query.toLowerCase(Locale.ROOT))); - } + private static boolean authorMatches(ModContainer modContainer, String query) { + return modContainer.getMetadata().getAuthors().stream() + .filter(Objects::nonNull) + .map(Person::getName) + .filter(Objects::nonNull) + .map(s -> s.toLowerCase(Locale.ROOT)) + .anyMatch(s -> s.contains(query.toLowerCase(Locale.ROOT))); + } } diff --git a/src/main/java/io/github/prospector/modmenu/util/RenderUtils.java b/src/main/java/io/github/prospector/modmenu/util/RenderUtils.java index fabb12d01..cfcbc67c6 100644 --- a/src/main/java/io/github/prospector/modmenu/util/RenderUtils.java +++ b/src/main/java/io/github/prospector/modmenu/util/RenderUtils.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.util; - import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.gui.Screen; @@ -10,61 +9,61 @@ import java.util.Collections; import java.util.List; +@SuppressWarnings("java:S6548") @Environment(EnvType.CLIENT) public final class RenderUtils extends Screen { - public static final RenderUtils INSTANCE = new RenderUtils(); - private RenderUtils() {} + public static final RenderUtils INSTANCE = new RenderUtils(); + private RenderUtils() {} - public List wrapStringToWidthAsList(Font font, String text, int width) { - List words = new ArrayList<>(); - if (text != null) - Collections.addAll(words, text.split(" ")); + public List wrapStringToWidthAsList(Font font, String text, int width) { + List words = new ArrayList<>(); + if (text != null) + Collections.addAll(words, text.split(" ")); - List strings = new ArrayList<>(); - String current = ""; - while (!words.isEmpty()) { - String nextWord = words.remove(0); - String next = current.isEmpty() ? nextWord : current + " " + nextWord; - if (font.getStringWidth(next) > width) { - strings.add(current); - current = nextWord; - } else { - current = next; - } - } - if (!current.isEmpty()) { - strings.add(current); - } + List strings = new ArrayList<>(); + String current = ""; + while (!words.isEmpty()) { + String nextWord = words.remove(0); + String next = current.isEmpty() ? nextWord : current + " " + nextWord; + if (font.getStringWidth(next) > width) { + strings.add(current); + current = nextWord; + } else { + current = next; + } + } + if (!current.isEmpty()) { + strings.add(current); + } - return strings; - } + return strings; + } - public void drawWrappedString(Font font, String string, int x, int y, int wrapWidth, int lines, int color) { - while (string != null && string.endsWith("\n")) { - string = string.substring(0, string.length() - 1); - } - List strings = wrapStringToWidthAsList(font, string, wrapWidth); + public void drawWrappedString(Font font, String string, int x, int y, int wrapWidth, int lines, int color) { + while (string != null && string.endsWith("\n")) { + string = string.substring(0, string.length() - 1); + } + List strings = wrapStringToWidthAsList(font, string, wrapWidth); - for (int i = 0; i < strings.size(); i++) { - if (i >= lines) { - break; - } - String line = strings.get(i); - if (i == lines - 1 && strings.size() > lines) { - line += "..."; - } - int x1 = x; - font.drawString(line, x1, y + i * 9, color); - } - } + for (int i = 0; i < strings.size(); i++) { + if (i >= lines) { + break; + } + String line = strings.get(i); + if (i == lines - 1 && strings.size() > lines) { + line += "..."; + } + font.drawString(line, x, y + i * 9, color); + } + } - public void drawBadge(Font font, int x, int y, int tagWidth, String text, int outlineColor, int fillColor, int textColor) { - drawRect(x + 1, y - 1, x + tagWidth, y, outlineColor); + public void drawBadge(Font font, int x, int y, int tagWidth, String text, int outlineColor, int fillColor, int textColor) { + drawRect(x + 1, y - 1, x + tagWidth, y, outlineColor); drawRect(x, y, x + 1, y + 9, outlineColor); drawRect(x + 1, y + 1 + 9 - 1, x + tagWidth, y + 9 + 1, outlineColor); drawRect(x + tagWidth, y, x + tagWidth + 1, y + 9, outlineColor); drawRect(x + 1, y, x + tagWidth, y + 9, fillColor); - font.drawString(text, (x + 1 + (tagWidth - font.getStringWidth(text)) / 2), y + 1, textColor); - } + font.drawString(text, (x + 1 + (tagWidth - font.getStringWidth(text)) / 2), y + 1, textColor); + } } diff --git a/src/main/java/io/github/prospector/modmenu/util/TestModContainer.java b/src/main/java/io/github/prospector/modmenu/util/TestModContainer.java index 8657b19a8..6e4f6411b 100644 --- a/src/main/java/io/github/prospector/modmenu/util/TestModContainer.java +++ b/src/main/java/io/github/prospector/modmenu/util/TestModContainer.java @@ -1,12 +1,7 @@ package io.github.prospector.modmenu.util; - -import net.fabricmc.loader.api.FabricLoader; -import net.fabricmc.loader.api.ModContainer; -import net.fabricmc.loader.api.SemanticVersion; -import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.*; import net.fabricmc.loader.api.metadata.*; -import net.fabricmc.loader.util.version.VersionParsingException; import java.nio.file.Path; import java.util.*; @@ -14,30 +9,30 @@ public class TestModContainer implements ModContainer { - public static final Random RAND = new Random(); - private static Collection testModContainers; + public static final Random RAND = new Random(); + private static Collection testModContainers; - public static Collection getTestModContainers() { - if (testModContainers == null) { - testModContainers = new ArrayList<>(); - for (int i = 0; i < 1000; i++) { - testModContainers.add(new TestModContainer()); - } - } - return testModContainers; - } + public static Collection getTestModContainers() { + if (testModContainers == null) { + testModContainers = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + testModContainers.add(new TestModContainer()); + } + } + return testModContainers; + } - private final ModMetadata metadata = new TestModMetadata(); - private final Path rootPath = FabricLoader.getInstance().getModContainer("fabricloader").orElseThrow(IllegalStateException::new).getRootPath(); + private final ModMetadata metadata = new TestModMetadata(); + private final Path rootPath = FabricLoader.getInstance().getModContainer("fabricloader").orElseThrow(IllegalStateException::new).getRootPaths().get(0); - @Override - public ModMetadata getMetadata() { - return this.metadata; - } + @Override + public ModMetadata getMetadata() { + return this.metadata; + } @Override public List getRootPaths() { - return null; + return Collections.emptyList(); } @Override @@ -52,170 +47,145 @@ public Optional getContainingMod() { @Override public Collection getContainedMods() { - return null; + return Collections.emptyList(); } @Override - public Path getRootPath() { - return this.rootPath; - } + public Path getRootPath() { + return this.rootPath; + } @Override public Path getPath(String file) { return null; } - private static String randomAlphabetic(int minLen, int maxLen) { - int len = ThreadLocalRandom.current().nextInt(maxLen - minLen + 1) + minLen; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < len; i++) { - int rand = ThreadLocalRandom.current().nextInt(26 * 2); - if (rand < 26) - sb.append((char) ('A' + rand)); - else - sb.append((char) ('a' - 26 + rand)); - } - return sb.toString(); - } - - private static String randomAlphanumeric(int minLen, int maxLen) { - int len = ThreadLocalRandom.current().nextInt(maxLen - minLen + 1) + minLen; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < len; i++) { - int rand = ThreadLocalRandom.current().nextInt(26 * 2 + 10); - if (rand < 26) - sb.append((char) ('A' + rand)); - else if (rand < 26 * 2) - sb.append((char) ('a' - 26 + rand)); - else - sb.append((char) ('0' - (26 * 2) + rand)); - } - return sb.toString(); - } - - public static class TestModMetadata implements ModMetadata { - private final String id; - private final String description; - private final Version version; - - public TestModMetadata() { - super(); - this.id = randomAlphabetic(10, 50).toLowerCase(Locale.ROOT); - this.description = randomAlphabetic(0, 500); - try { - this.version = SemanticVersion.parse(String.format("%d.%d.%d+%s", RAND.nextInt(10), RAND.nextInt(50), RAND.nextInt(200), randomAlphanumeric(2, 10))); - } catch (VersionParsingException e) { - throw new AssertionError("Generated version is not semantic", e); - } - } - - @Override - public String getType() { - return "test"; - } - - @Override - public String getId() { - return this.id; - } - - @Override - public Collection getProvides() { - return null; - } - - @Override - public Version getVersion() { - return this.version; - } - - @Override - public ModEnvironment getEnvironment() { - return null; - } + public static class TestModMetadata implements ModMetadata { + private final String id; + private final String description; + private final Version version; + + public TestModMetadata() { + super(); + this.id = randomAlphabetic(10, 50).toLowerCase(Locale.ROOT); + this.description = randomAlphabetic(0, 500); + try { + this.version = SemanticVersion.parse(String.format("%d.%d.%d+%s", RAND.nextInt(10), RAND.nextInt(50), RAND.nextInt(200), randomAlphanumeric(2, 10))); + } catch (VersionParsingException e) { + throw new AssertionError("Generated version is not semantic", e); + } + } + + private static String randomAlphabetic(int minLen, int maxLen) { + int len = ThreadLocalRandom.current().nextInt(maxLen - minLen + 1) + minLen; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) { + int rand = ThreadLocalRandom.current().nextInt(26 * 2); + if (rand < 26) + sb.append((char) ('A' + rand)); + else + sb.append((char) ('a' - 26 + rand)); + } + return sb.toString(); + } + + @SuppressWarnings("SameParameterValue") + private static String randomAlphanumeric(int minLen, int maxLen) { + int len = ThreadLocalRandom.current().nextInt(maxLen - minLen + 1) + minLen; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) { + int rand = ThreadLocalRandom.current().nextInt(26 * 2 + 10); + if (rand < 26) + sb.append((char) ('A' + rand)); + else if (rand < 26 * 2) + sb.append((char) ('a' - 26 + rand)); + else + sb.append((char) ('0' - (26 * 2) + rand)); + } + return sb.toString(); + } + + @Override + public String getType() { + return "test"; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public Collection getProvides() { + return Collections.emptyList(); + } + + @Override + public Version getVersion() { + return this.version; + } + + @Override + public ModEnvironment getEnvironment() { + return null; + } @Override public Collection getDependencies() { + return Collections.emptyList(); + } + + @Override + public String getName() { + return this.getId(); + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public Collection getAuthors() { + return Collections.emptyList(); + } + + @Override + public Collection getContributors() { + return Collections.emptyList(); + } + + @Override + public ContactInformation getContact() { + return ContactInformation.EMPTY; + } + + @Override + public Collection getLicense() { + return Collections.emptyList(); + } + + @Override + public Optional getIconPath(int size) { + return Optional.empty(); + } + + @Override + public boolean containsCustomValue(String key) { + return false; + } + + @Override + public CustomValue getCustomValue(String key) { return null; } @Override - public Collection getDepends() { - return Collections.emptyList(); - } - - @Override - public Collection getRecommends() { - return Collections.emptyList(); - } - - @Override - public Collection getSuggests() { - return Collections.emptyList(); - } - - @Override - public Collection getConflicts() { - return Collections.emptyList(); - } - - @Override - public Collection getBreaks() { - return Collections.emptyList(); - } - - @Override - public String getName() { - return this.getId(); - } - - @Override - public String getDescription() { - return this.description; - } - - @Override - public Collection getAuthors() { - return Collections.emptyList(); - } - - @Override - public Collection getContributors() { - return Collections.emptyList(); - } - - @Override - public ContactInformation getContact() { - return ContactInformation.EMPTY; - } - - @Override - public Collection getLicense() { - return Collections.emptyList(); - } - - @Override - public Optional getIconPath(int size) { - return Optional.empty(); - } - - @Override - public boolean containsCustomValue(String key) { - return false; - } - - @Override - public CustomValue getCustomValue(String key) { - return null; - } - - @Override - public Map getCustomValues() { - return new HashMap<>(); - } - - @SuppressWarnings("UnstableApiUsage") - @Override + public Map getCustomValues() { + return new HashMap<>(); + } + + @Override public boolean containsCustomElement(String key) { return false; } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index b7a82a0d9..db1082c74 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -20,7 +20,9 @@ "issues": "https://github.com/Turnip-Labs/ModMenu/issues" }, "depends": { - "fabricloader": "*" + "fabricloader": ">=${fabricloader}", + "minecraft": "*", + "java": ">=${java}" }, "authors": [ "Prospector" @@ -48,12 +50,13 @@ "LeonXu98", "magneticflux", "Earthcomputer", - "Ambos", + "Ambos", "Flamarine", - "sunsetsatellite" + "sunsetsatellite", + "SmushyTaco" ], "description": "Adds a mod menu to view the list of mods you have installed.", "mixins": [ - "mixins.modmenu.json" + "modmenu.mixins.json" ] } \ No newline at end of file diff --git a/src/main/resources/mixins.modmenu.json b/src/main/resources/modmenu.mixins.json similarity index 91% rename from src/main/resources/mixins.modmenu.json rename to src/main/resources/modmenu.mixins.json index 8e8298ddf..bbd0bd053 100644 --- a/src/main/resources/mixins.modmenu.json +++ b/src/main/resources/modmenu.mixins.json @@ -1,7 +1,7 @@ { "required": true, "package": "io.github.prospector.modmenu.mixin", - "compatibilityLevel": "JAVA_8", + "compatibilityLevel": "JAVA_${java}", "plugin": "io.github.prospector.modmenu.mixin.ModMenuMixinConfigPlugin", "client": [ "MixinGuiIngameMenu", From ab7ea29daa4926f6ba776bc69db91fc49274cfd3 Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Mon, 17 Nov 2025 00:01:26 -0800 Subject: [PATCH 02/14] Use Path instead of legacy File --- .../prospector/modmenu/gui/ModListScreen.java | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java index 824717d08..68520925e 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java @@ -24,12 +24,9 @@ import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; import org.lwjgl.opengl.GL14; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.spongepowered.include.com.google.common.base.Joiner; -import java.io.File; -import java.net.MalformedURLException; +import java.nio.file.Path; import java.text.NumberFormat; import java.util.*; @@ -38,7 +35,6 @@ public class ModListScreen extends Screen { "/assets/" + ModMenu.MOD_ID + "/textures/gui/filters_button.png"; private static final String CONFIGURE_BUTTON_LOCATION = "/assets/" + ModMenu.MOD_ID + "/textures/gui/configure_button.png"; - private static final Logger LOGGER = LoggerFactory.getLogger("modlistscreen"); private final String textTitle; private final Screen parent; @@ -335,12 +331,8 @@ protected void buttonClicked(ButtonElement button) { break; } case MODS_FOLDER_BUTTON_ID: { - File modsFolder = new File(FabricLoader.getInstance().getGameDir().toFile(), "mods"); - try { - Sys.openURL(modsFolder.toURI().toURL().toString()); - } catch (MalformedURLException e) { - LOGGER.error("Malformed mods folder URL", e); - } + Path modsFolder = FabricLoader.getInstance().getGameDir().resolve("mods"); + Sys.openURL(modsFolder.toUri().toString()); break; } case DONE_BUTTON_ID: { From 4b6b392a4affae365e1222309fd91ebd44e970f9 Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Mon, 17 Nov 2025 00:51:34 -0800 Subject: [PATCH 03/14] Mixin Improvement --- .../io/github/prospector/modmenu/mixin/MixinTexturePacks.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/github/prospector/modmenu/mixin/MixinTexturePacks.java b/src/main/java/io/github/prospector/modmenu/mixin/MixinTexturePacks.java index 2f4cf6f9d..27915a47a 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/MixinTexturePacks.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/MixinTexturePacks.java @@ -13,7 +13,7 @@ public abstract class MixinTexturePacks { @WrapOperation(method = "getResourceAsStream", at = @At(value = "INVOKE", target = "Ljava/lang/Class;getResourceAsStream(Ljava/lang/String;)Ljava/io/InputStream;", remap = false)) private InputStream modmenu$onGetResource(Class instance, String name, Operation original) { - InputStream inputStream = ModMenu.class.getClassLoader().getResourceAsStream(name); + InputStream inputStream = original.call(ModMenu.class, name); if (inputStream != null) return inputStream; return original.call(instance, name); } From 701e7bf7ab8775757801c1a0ccdce38be3b5fddf Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Mon, 17 Nov 2025 01:03:30 -0800 Subject: [PATCH 04/14] Fix Opening Mods Folder The original method used was outdated and unreliable. This will always work. --- .../prospector/modmenu/gui/ModListScreen.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java index 68520925e..6ebe83055 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java @@ -332,7 +332,22 @@ protected void buttonClicked(ButtonElement button) { } case MODS_FOLDER_BUTTON_ID: { Path modsFolder = FabricLoader.getInstance().getGameDir().resolve("mods"); - Sys.openURL(modsFolder.toUri().toString()); + try { + String os = System.getProperty("os.name").toLowerCase(); + + if (os.contains("win")) { + // Windows Explorer + new ProcessBuilder("explorer.exe", modsFolder.toString()).start(); + } else if (os.contains("mac")) { + // macOS Finder + new ProcessBuilder("open", modsFolder.toString()).start(); + } else { + // Linux / BSD: xdg-open (freedesktop standard) + new ProcessBuilder("xdg-open", modsFolder.toString()).start(); + } + } catch (Exception e) { + ModMenu.LOGGER.error("Failed to open mods folder", e); + } break; } case DONE_BUTTON_ID: { @@ -535,9 +550,7 @@ static String getString(Font font, String fullName, int maxNameWidth) { } - public void overlayBackground(int x1, int y1, int x2, int y2, - int red, int green, int blue, - int startAlpha, int endAlpha) { + public void overlayBackground(int x1, int y1, int x2, int y2, int red, int green, int blue, int startAlpha, int endAlpha) { Tessellator tessellator = Tessellator.instance; mc.textureManager.bindTexture(mc.textureManager.loadTexture("/gui/background.png")); GL11.glColor4f(1f, 1f, 1f, 1f); From b21c7f9323428f3999c0f52b56c59755d98f4a73 Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Mon, 17 Nov 2025 01:08:00 -0800 Subject: [PATCH 05/14] Minor Tweak --- .../java/io/github/prospector/modmenu/gui/ModListScreen.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java index 6ebe83055..3d390fd2a 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java @@ -26,6 +26,7 @@ import org.lwjgl.opengl.GL14; import org.spongepowered.include.com.google.common.base.Joiner; +import java.io.IOException; import java.nio.file.Path; import java.text.NumberFormat; import java.util.*; @@ -345,7 +346,7 @@ protected void buttonClicked(ButtonElement button) { // Linux / BSD: xdg-open (freedesktop standard) new ProcessBuilder("xdg-open", modsFolder.toString()).start(); } - } catch (Exception e) { + } catch (IOException e) { ModMenu.LOGGER.error("Failed to open mods folder", e); } break; From 578e00da351bc53bc69cd8fb1da435eb4ea12529 Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Mon, 17 Nov 2025 01:45:53 -0800 Subject: [PATCH 06/14] Open URL Tweaks Moves URL opening off LWJGL 2. --- .PVS-Studio/ide-warnings.json | 144 ++++++++++++++++++ .PVS-Studio/settings.json | 46 ++++++ .../modmenu/config/ModMenuConfigManager.java | 1 - .../prospector/modmenu/gui/ModListScreen.java | 26 +++- 4 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 .PVS-Studio/ide-warnings.json create mode 100644 .PVS-Studio/settings.json diff --git a/.PVS-Studio/ide-warnings.json b/.PVS-Studio/ide-warnings.json new file mode 100644 index 000000000..bb3b679a6 --- /dev/null +++ b/.PVS-Studio/ide-warnings.json @@ -0,0 +1,144 @@ +{ + "languages": { + "JAVA": { + "groups": { + "OWASP": "DISABLED", + "GA": "SHOW_ALL" + } + } + }, + "warnings": { + "V6027": true, + "V6026": true, + "V6029": true, + "V6028": true, + "V6023": true, + "V6022": true, + "V6025": true, + "V6024": true, + "V6021": true, + "V6020": true, + "V6038": true, + "V6037": true, + "V6039": true, + "V6034": true, + "V6033": true, + "V6036": true, + "V6035": true, + "V6030": true, + "V6032": true, + "V6031": true, + "V6126": true, + "V6005": true, + "V6125": true, + "V6004": true, + "V6128": true, + "V6007": true, + "V6127": true, + "V6006": true, + "V6089": true, + "V6122": true, + "V6001": true, + "V6088": true, + "V6121": true, + "V6003": true, + "V6124": true, + "V6002": true, + "V6123": true, + "V6009": true, + "V6008": true, + "V6129": true, + "V6085": true, + "V6084": true, + "V6087": true, + "V6120": true, + "V6086": true, + "V6081": true, + "V6080": true, + "V6083": true, + "V6082": true, + "V6016": true, + "V6015": true, + "V6018": true, + "V6017": true, + "V6012": true, + "V6099": true, + "V6011": true, + "V6132": true, + "V6014": true, + "V6013": true, + "V6019": true, + "V6090": true, + "V6096": true, + "V6095": true, + "V6010": true, + "V6131": true, + "V6098": true, + "V6130": true, + "V6097": true, + "V6092": true, + "V6091": true, + "V6094": true, + "V6093": true, + "V6104": true, + "V6103": true, + "V6106": true, + "V6105": true, + "V6100": true, + "V6067": true, + "V6066": true, + "V6069": true, + "V6102": true, + "V6101": true, + "V6068": true, + "V6108": true, + "V6107": true, + "V6109": true, + "V6063": true, + "V6062": true, + "V6065": true, + "V6064": true, + "V6061": true, + "V6060": true, + "V6115": true, + "V6114": true, + "V6117": true, + "V6116": true, + "V6078": true, + "V6111": true, + "V6110": true, + "V6077": true, + "V6113": true, + "V6112": true, + "V6079": true, + "V6119": true, + "V6118": true, + "V6074": true, + "V6073": true, + "V6076": true, + "V6075": true, + "V6070": true, + "V6072": true, + "V6071": true, + "V6049": true, + "V6048": true, + "V6045": true, + "V6044": true, + "V6047": true, + "V6046": true, + "V6041": true, + "V6040": true, + "V6043": true, + "V6042": true, + "V6059": true, + "V6056": true, + "V6055": true, + "V6058": true, + "V6057": true, + "V6052": true, + "V6051": true, + "V6054": true, + "V6053": true, + "V6050": true + } +} \ No newline at end of file diff --git a/.PVS-Studio/settings.json b/.PVS-Studio/settings.json new file mode 100644 index 000000000..822a70257 --- /dev/null +++ b/.PVS-Studio/settings.json @@ -0,0 +1,46 @@ +{ + "src": [], + "ext": [], + "threads": 15, + "output-type": "json", + "securityRelatedIssues": false, + "annotation-file": [], + "incremental": false, + "force-rebuild": false, + "disable-cache": false, + "exclude": [], + "analyze-only": [], + "fail-on-warnings": false, + "analysis-mode": [ + "GA" + ], + "disabled-warnings": [], + "enabled-warnings": [], + "additional-warnings": [], + "suppress-base": ".PVS-Studio/suppress_base.json", + "timeout": 10, + "compatibility": false, + "exclude-packages": [], + "activate-license": false, + "version": false, + "is-java-core-subprocess-with-add-opens-for-native-libs-field-reflective-access": false, + "write-to-stdout": false, + "verbose": false, + "useOfflineDocumentation": false, + "skip-modules": [], + "include-generated": false, + "launch-mode": "IDEA", + "enable-all-warnings": false, + "benchmark": false, + "java": "java", + "jvm-arguments": [ + "-Xss64m" + ], + "traceLogFileName": "pvs.log", + "traceSpoonLogFileName": "spoon.log", + "check-license": false, + "logging": "OFF", + "project": "C:/Users/organ/Documents/GitHub/ModMenu", + "type": "sources", + "disableDiag": false +} \ No newline at end of file diff --git a/src/main/java/io/github/prospector/modmenu/config/ModMenuConfigManager.java b/src/main/java/io/github/prospector/modmenu/config/ModMenuConfigManager.java index 9aef3375f..aad5a0449 100644 --- a/src/main/java/io/github/prospector/modmenu/config/ModMenuConfigManager.java +++ b/src/main/java/io/github/prospector/modmenu/config/ModMenuConfigManager.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.config; - import io.github.prospector.modmenu.ModMenu; import net.fabricmc.loader.api.FabricLoader; diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java index 3d390fd2a..ba7368bf6 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java @@ -18,8 +18,6 @@ import net.minecraft.client.render.tessellator.Tessellator; import net.minecraft.core.Global; import net.minecraft.core.lang.I18n; -import org.lwjgl.Sys; -import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; @@ -92,7 +90,26 @@ public void updateEvents() { } } + private static void openUrl(String url) { + try { + String os = System.getProperty("os.name").toLowerCase(Locale.ROOT); + List cmd; + if (os.contains("win")) { + // Use the standard Windows URL handler + cmd = Arrays.asList("rundll32", "url.dll,FileProtocolHandler", url); + } else if (os.contains("mac")) { + cmd = Arrays.asList("open", url); + } else { + // Linux / BSD + cmd = Arrays.asList("xdg-open", url); + } + + new ProcessBuilder(cmd).start(); + } catch (Exception e) { + ModMenu.LOGGER.error("Failed to open URL {}", url, e); + } + } @SuppressWarnings("unused") @@ -113,7 +130,6 @@ public void tick() { @Override public void init() { I18n i18n = I18n.getInstance(); - Keyboard.enableRepeatEvents(true); Font font = this.font; paneY = 48; @@ -309,12 +325,12 @@ protected void buttonClicked(ButtonElement button) { } case WEBSITE_BUTTON_ID: { ModMetadata metadata = Objects.requireNonNull(selected).getMetadata(); - metadata.getContact().get("homepage").ifPresent(Sys::openURL); + metadata.getContact().get("homepage").ifPresent(ModListScreen::openUrl); break; } case ISSUES_BUTTON_ID: { ModMetadata metadata = Objects.requireNonNull(selected).getMetadata(); - metadata.getContact().get("issues").ifPresent(Sys::openURL); + metadata.getContact().get("issues").ifPresent(ModListScreen::openUrl); break; } case TOGGLE_FILTER_OPTIONS_BUTTON_ID: { From 180f9de028c06197399c7b745d5c4b55cd0793fd Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Mon, 17 Nov 2025 01:57:15 -0800 Subject: [PATCH 07/14] Don't Make Empty URLs Clickable --- .../io/github/prospector/modmenu/gui/ModListScreen.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java index ba7368bf6..16909a0a6 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java @@ -185,8 +185,8 @@ public void render(Minecraft mc, int mouseX, int mouseY) { @Override public void drawButton(Minecraft mc, int mouseX, int mouseY) { visible = selected != null; - enabled = visible - && selected.getMetadata().getContact().get("homepage").isPresent(); + Optional link = selected != null ? selected.getMetadata().getContact().get("homepage") : Optional.empty(); + enabled = visible && link.isPresent() && !link.get().isEmpty(); super.drawButton(mc, mouseX, mouseY); } }; @@ -201,8 +201,8 @@ public void drawButton(Minecraft mc, int mouseX, int mouseY) { @Override public void drawButton(Minecraft mc, int mouseX, int mouseY) { visible = selected != null; - enabled = visible - && selected.getMetadata().getContact().get("issues").isPresent(); + Optional link = selected != null ? selected.getMetadata().getContact().get("issues") : Optional.empty(); + enabled = visible && link.isPresent() && !link.get().isEmpty(); super.drawButton(mc, mouseX, mouseY); } }; From 1efa97d88585347b3bc49018c7da1269c3ff3fcb Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Mon, 17 Nov 2025 17:25:11 -0800 Subject: [PATCH 08/14] Delete Static Analysis Files oops! --- .PVS-Studio/ide-warnings.json | 144 ---------------------------------- .PVS-Studio/settings.json | 46 ----------- 2 files changed, 190 deletions(-) delete mode 100644 .PVS-Studio/ide-warnings.json delete mode 100644 .PVS-Studio/settings.json diff --git a/.PVS-Studio/ide-warnings.json b/.PVS-Studio/ide-warnings.json deleted file mode 100644 index bb3b679a6..000000000 --- a/.PVS-Studio/ide-warnings.json +++ /dev/null @@ -1,144 +0,0 @@ -{ - "languages": { - "JAVA": { - "groups": { - "OWASP": "DISABLED", - "GA": "SHOW_ALL" - } - } - }, - "warnings": { - "V6027": true, - "V6026": true, - "V6029": true, - "V6028": true, - "V6023": true, - "V6022": true, - "V6025": true, - "V6024": true, - "V6021": true, - "V6020": true, - "V6038": true, - "V6037": true, - "V6039": true, - "V6034": true, - "V6033": true, - "V6036": true, - "V6035": true, - "V6030": true, - "V6032": true, - "V6031": true, - "V6126": true, - "V6005": true, - "V6125": true, - "V6004": true, - "V6128": true, - "V6007": true, - "V6127": true, - "V6006": true, - "V6089": true, - "V6122": true, - "V6001": true, - "V6088": true, - "V6121": true, - "V6003": true, - "V6124": true, - "V6002": true, - "V6123": true, - "V6009": true, - "V6008": true, - "V6129": true, - "V6085": true, - "V6084": true, - "V6087": true, - "V6120": true, - "V6086": true, - "V6081": true, - "V6080": true, - "V6083": true, - "V6082": true, - "V6016": true, - "V6015": true, - "V6018": true, - "V6017": true, - "V6012": true, - "V6099": true, - "V6011": true, - "V6132": true, - "V6014": true, - "V6013": true, - "V6019": true, - "V6090": true, - "V6096": true, - "V6095": true, - "V6010": true, - "V6131": true, - "V6098": true, - "V6130": true, - "V6097": true, - "V6092": true, - "V6091": true, - "V6094": true, - "V6093": true, - "V6104": true, - "V6103": true, - "V6106": true, - "V6105": true, - "V6100": true, - "V6067": true, - "V6066": true, - "V6069": true, - "V6102": true, - "V6101": true, - "V6068": true, - "V6108": true, - "V6107": true, - "V6109": true, - "V6063": true, - "V6062": true, - "V6065": true, - "V6064": true, - "V6061": true, - "V6060": true, - "V6115": true, - "V6114": true, - "V6117": true, - "V6116": true, - "V6078": true, - "V6111": true, - "V6110": true, - "V6077": true, - "V6113": true, - "V6112": true, - "V6079": true, - "V6119": true, - "V6118": true, - "V6074": true, - "V6073": true, - "V6076": true, - "V6075": true, - "V6070": true, - "V6072": true, - "V6071": true, - "V6049": true, - "V6048": true, - "V6045": true, - "V6044": true, - "V6047": true, - "V6046": true, - "V6041": true, - "V6040": true, - "V6043": true, - "V6042": true, - "V6059": true, - "V6056": true, - "V6055": true, - "V6058": true, - "V6057": true, - "V6052": true, - "V6051": true, - "V6054": true, - "V6053": true, - "V6050": true - } -} \ No newline at end of file diff --git a/.PVS-Studio/settings.json b/.PVS-Studio/settings.json deleted file mode 100644 index 822a70257..000000000 --- a/.PVS-Studio/settings.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "src": [], - "ext": [], - "threads": 15, - "output-type": "json", - "securityRelatedIssues": false, - "annotation-file": [], - "incremental": false, - "force-rebuild": false, - "disable-cache": false, - "exclude": [], - "analyze-only": [], - "fail-on-warnings": false, - "analysis-mode": [ - "GA" - ], - "disabled-warnings": [], - "enabled-warnings": [], - "additional-warnings": [], - "suppress-base": ".PVS-Studio/suppress_base.json", - "timeout": 10, - "compatibility": false, - "exclude-packages": [], - "activate-license": false, - "version": false, - "is-java-core-subprocess-with-add-opens-for-native-libs-field-reflective-access": false, - "write-to-stdout": false, - "verbose": false, - "useOfflineDocumentation": false, - "skip-modules": [], - "include-generated": false, - "launch-mode": "IDEA", - "enable-all-warnings": false, - "benchmark": false, - "java": "java", - "jvm-arguments": [ - "-Xss64m" - ], - "traceLogFileName": "pvs.log", - "traceSpoonLogFileName": "spoon.log", - "check-license": false, - "logging": "OFF", - "project": "C:/Users/organ/Documents/GitHub/ModMenu", - "type": "sources", - "disableDiag": false -} \ No newline at end of file From 8a4584b8d64a5e679ecaf41f3059502be97eb53d Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Mon, 17 Nov 2025 17:37:26 -0800 Subject: [PATCH 09/14] Make Bottom Buttons Translatable --- .../io/github/prospector/modmenu/gui/ModListScreen.java | 6 ++++-- src/main/resources/lang/modmenu/en_US.lang | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java index 16909a0a6..c172934ba 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java @@ -290,19 +290,21 @@ public void drawButton(Minecraft mc, int mouseX, int mouseY) { this.buttons.add(configureButton); this.buttons.add(websiteButton); this.buttons.add(issuesButton); + String modsFolder = i18n.translateKey("modmenu.modsFolder"); this.buttons.add(ButtonUtil.createButton( MODS_FOLDER_BUTTON_ID, this.width / 2 - 154, this.height - 28, 150, 20, - "Open Mods Folder" + modsFolder == null || modsFolder.equals("modmenu.modsFolder") ? "Open Mods Folder" : modsFolder )); + String done = i18n.translateKey("modmenu.done"); this.buttons.add(ButtonUtil.createButton( DONE_BUTTON_ID, this.width / 2 + 4, this.height - 28, 150, 20, - "Done" + done == null || done.equals("modmenu.done") ? "Done" : done )); this.searchBox.setFocused(true); diff --git a/src/main/resources/lang/modmenu/en_US.lang b/src/main/resources/lang/modmenu/en_US.lang index 2c34ca8df..899411a92 100644 --- a/src/main/resources/lang/modmenu/en_US.lang +++ b/src/main/resources/lang/modmenu/en_US.lang @@ -2,6 +2,7 @@ modmenu.title=Mods modmenu.loaded=(%s Loaded) modmenu.config=Edit Config modmenu.modsFolder=Open Mods Folder +modmenu.done=Done modmenu.configsFolder=Open Configs Folder modmenu.configure=Configure... modmenu.modIdToolTip=Mod ID: %s From 3b2487fd911385143ee0cbfe566b19134f1a9203 Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Mon, 17 Nov 2025 21:07:00 -0800 Subject: [PATCH 10/14] Daemon Toolchain Configuration + Dependency Updates --- build.gradle.kts | 5 +++++ gradle.properties | 3 +++ gradle/gradle-daemon-jvm.properties | 13 +++++++++++++ gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 gradle/gradle-daemon-jvm.properties diff --git a/build.gradle.kts b/build.gradle.kts index f9d6b5b26..51466f992 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,6 +33,7 @@ val gsonVersion = providers.gradleProperty("gson_version") val commonsLang3Version = providers.gradleProperty("commons_lang3_version") val javaVersion = providers.gradleProperty("java_version") +val gradleJavaVersion = providers.gradleProperty("gradle_java_version") group = modGroup.get() base.archivesName = modName.get() @@ -113,6 +114,10 @@ tasks { targetCompatibility = javaVersion.get() if (javaVersion.get().toInt() > 8) options.release = javaVersion.get().toInt() } + named("updateDaemonJvm") { + languageVersion = JavaLanguageVersion.of(gradleJavaVersion.get().toInt()) + vendor = JvmVendorSpec.ADOPTIUM + } withType().configureEach { defaultCharacterEncoding = "UTF-8" } withType().configureEach { options.encoding = "UTF-8" } withType().configureEach { defaultCharacterEncoding = "UTF-8" } diff --git a/gradle.properties b/gradle.properties index 1a3cac984..944ffad59 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,10 @@ bta_channel = release loader_version = 0.17.3-bta.8 # Check this on https://maven.thesignalumproject.net/#/infrastructure/fabric-loom/fabric-loom.gradle.plugin/ loom_version = 1.13.0-bta +# The Java version the JDK will be for compiling and running code. java_version = 8 +# The Java version the JDK will be for running Gradle. +gradle_java_version = 21 ########################################################################## # Mod Properties mod_version = 4.0.0 diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 000000000..e409a46b0 --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,13 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/df211d3c3eefdc408b462041881bc575/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/b41931cf1e70bc8e08d7dd19c343ef00/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/df211d3c3eefdc408b462041881bc575/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/b41931cf1e70bc8e08d7dd19c343ef00/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/46949723aaa20c7b64d7ecfed7207034/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/d6690dfd71c4c91e08577437b5b2beb0/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/df211d3c3eefdc408b462041881bc575/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/b41931cf1e70bc8e08d7dd19c343ef00/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/3cd7045fca9a72cd9bc7d14a385e594c/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/552c7bffe0370c66410a51c55985b511/redirect +toolchainVendor=ADOPTIUM +toolchainVersion=21 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2397460ad..071ff21b9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ # Check this on https://gradle.org/releases/ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From e9fec569fe837f6a0dc0058329fd48acf960913c Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Mon, 17 Nov 2025 22:03:58 -0800 Subject: [PATCH 11/14] Removed unused maven repository --- build.gradle.kts | 1 - settings.gradle.kts | 1 - 2 files changed, 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 51466f992..7b163f54f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,7 +47,6 @@ loom { repositories { mavenCentral() maven("https://jitpack.io") - maven("https://maven.glass-launcher.net/babric") { name = "Babric" } maven("https://maven.fabricmc.net/") { name = "Fabric" } maven("https://maven.thesignalumproject.net/infrastructure") { name = "SignalumMavenInfrastructure" } maven("https://maven.thesignalumproject.net/releases") { name = "SignalumMavenReleases" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 9603aa224..255222fab 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,7 +5,6 @@ pluginManagement { gradlePluginPortal() maven("https://maven.fabricmc.net/") { name = "Fabric" } maven("https://jitpack.io") { name = "Jitpack" } - maven("https://maven.glass-launcher.net/babric") { name = "Babric" } maven("https://maven.thesignalumproject.net/infrastructure") { name = "SignalumMavenInfrastructure" } } val foojayResolverVersion = providers.gradleProperty("foojay_resolver_version") From d041d0d7bdc96831cef664210176fd97bd56c0a9 Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Mon, 24 Nov 2025 19:18:32 -0800 Subject: [PATCH 12/14] Build Modernization --- build.gradle.kts | 160 +++++++++++++++----------------------- gradle.properties | 32 -------- gradle/libs.versions.toml | 54 +++++++++++++ settings.gradle.kts | 51 ++++++++++-- 4 files changed, 161 insertions(+), 136 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/build.gradle.kts b/build.gradle.kts index 7b163f54f..4c82a2164 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,49 +1,24 @@ -@file:Suppress("UnstableApiUsage", "PropertyName") - -import org.apache.tools.ant.taskdefs.condition.Os - +@file:Suppress("UnstableApiUsage") +import com.smushytaco.lwjgl_gradle.Preset plugins { - id("fabric-loom") + alias(libs.plugins.loom) + alias(libs.plugins.lwjgl) java `maven-publish` } - -val lwjglVersion = providers.gradleProperty("lwjgl_version") -val lwjglNatives = when { - Os.isFamily(Os.FAMILY_UNIX) && !Os.isFamily(Os.FAMILY_MAC) -> "natives-linux" - Os.isFamily(Os.FAMILY_WINDOWS) -> "natives-windows" - Os.isFamily(Os.FAMILY_MAC) -> "natives-macos${if (Os.isArch("aarch64")) "-arm64" else ""}" - else -> error("Unsupported OS") -} - val modVersion = providers.gradleProperty("mod_version") val modGroup = providers.gradleProperty("mod_group") val modName = providers.gradleProperty("mod_name") -val btaChannel = providers.gradleProperty("bta_channel") -val btaVersion = providers.gradleProperty("bta_version") - -val loaderVersion = providers.gradleProperty("loader_version") -val legacyLwjglVersion = providers.gradleProperty("legacy_lwjgl_version") - -val slf4jApiVersion = providers.gradleProperty("slf4j_api_version") -val log4jVersion = providers.gradleProperty("log4j_version") -val guavaVersion = providers.gradleProperty("guava_version") -val gsonVersion = providers.gradleProperty("gson_version") -val commonsLang3Version = providers.gradleProperty("commons_lang3_version") - -val javaVersion = providers.gradleProperty("java_version") -val gradleJavaVersion = providers.gradleProperty("gradle_java_version") +val javaVersion = libs.versions.java.map { it.toInt() } +base.archivesName = modName group = modGroup.get() -base.archivesName = modName.get() version = modVersion.get() - loom { noIntermediateMappings() - customMinecraftMetadata.set("https://downloads.betterthanadventure.net/bta-client/${btaChannel.get()}/v${btaVersion.get()}/manifest.json") + customMinecraftMetadata.set("https://downloads.betterthanadventure.net/bta-client/${libs.versions.btaChannel.get()}/v${libs.versions.bta.get()}/manifest.json") } - repositories { mavenCentral() maven("https://jitpack.io") @@ -54,11 +29,11 @@ repositories { patternLayout { artifact("[organisation]/releases/download/v[revision]/[module].jar") } metadataSources { artifact() } } - ivy("https://downloads.betterthanadventure.net/bta-client/${btaChannel.get()}/") { + ivy("https://downloads.betterthanadventure.net/bta-client/${libs.versions.btaChannel.get()}/") { patternLayout { artifact("/v[revision]/client.jar") } metadataSources { artifact() } } - ivy("https://downloads.betterthanadventure.net/bta-server/${btaChannel.get()}/") { + ivy("https://downloads.betterthanadventure.net/bta-server/${libs.versions.btaChannel.get()}/") { patternLayout { artifact("/v[revision]/server.jar") } metadataSources { artifact() } } @@ -67,76 +42,72 @@ repositories { metadataSources { artifact() } } } - +lwjgl { + version = libs.versions.lwjgl + implementation(Preset.MINIMAL_OPENGL) +} dependencies { - minecraft("::${btaVersion.get()}") + minecraft("::${libs.versions.bta.get()}") mappings(loom.layered {}) // https://piston-data.mojang.com/v1/objects/43db9b498cb67058d2e12d394e6507722e71bb45/client.jar modRuntimeOnly("objects:client:43db9b498cb67058d2e12d394e6507722e71bb45") - modImplementation("net.fabricmc:fabric-loader:${loaderVersion.get()}") - modImplementation("com.github.Better-than-Adventure:legacy-lwjgl3:${legacyLwjglVersion.get()}") - - implementation(platform("org.lwjgl:lwjgl-bom:${lwjglVersion.get()}")) - implementation("org.slf4j:slf4j-api:${slf4jApiVersion.get()}") - - implementation("com.google.guava:guava:${guavaVersion.get()}") - implementation("com.google.code.gson:gson:${gsonVersion.get()}") - - implementation("org.apache.logging.log4j:log4j-slf4j2-impl:${log4jVersion.get()}") - implementation("org.apache.logging.log4j:log4j-core:${log4jVersion.get()}") - implementation("org.apache.logging.log4j:log4j-api:${log4jVersion.get()}") - implementation("org.apache.logging.log4j:log4j-1.2-api:${log4jVersion.get()}") - - implementation("org.apache.commons:commons-lang3:${commonsLang3Version.get()}") - include("org.apache.commons:commons-lang3:${commonsLang3Version.get()}") - - implementation("org.lwjgl:lwjgl:${lwjglVersion.get()}") - implementation("org.lwjgl:lwjgl-assimp:${lwjglVersion.get()}") - implementation("org.lwjgl:lwjgl-glfw:${lwjglVersion.get()}") - implementation("org.lwjgl:lwjgl-openal:${lwjglVersion.get()}") - implementation("org.lwjgl:lwjgl-opengl:${lwjglVersion.get()}") - implementation("org.lwjgl:lwjgl-stb:${lwjglVersion.get()}") - - runtimeOnly("org.lwjgl:lwjgl::$lwjglNatives") - runtimeOnly("org.lwjgl:lwjgl-assimp::$lwjglNatives") - runtimeOnly("org.lwjgl:lwjgl-glfw::$lwjglNatives") - runtimeOnly("org.lwjgl:lwjgl-openal::$lwjglNatives") - runtimeOnly("org.lwjgl:lwjgl-opengl::$lwjglNatives") - runtimeOnly("org.lwjgl:lwjgl-stb::$lwjglNatives") + modImplementation(libs.loader) + modImplementation(libs.legacyLwjgl) + + implementation(libs.slf4jApi) + implementation(libs.guava) + implementation(libs.log4j.slf4j2.impl) + implementation(libs.log4j.core) + implementation(libs.log4j.api) + implementation(libs.log4j.api12) + implementation(libs.gson) + + implementation(libs.commonsLang3) + include(libs.commonsLang3) +} +java { + toolchain { + languageVersion = javaVersion.map { JavaLanguageVersion.of(it) } + vendor = JvmVendorSpec.ADOPTIUM + } + sourceCompatibility = JavaVersion.toVersion(javaVersion.get()) + targetCompatibility = JavaVersion.toVersion(javaVersion.get()) + withSourcesJar() +} +val licenseFile = run { + val rootLicense = layout.projectDirectory.file("LICENSE") + val parentLicense = layout.projectDirectory.file("../LICENSE") + when { + rootLicense.asFile.exists() -> { + logger.lifecycle("Using LICENSE from project root: {}", rootLicense.asFile) + rootLicense + } + parentLicense.asFile.exists() -> { + logger.lifecycle("Using LICENSE from parent directory: {}", parentLicense.asFile) + parentLicense + } + else -> { + logger.warn("No LICENSE file found in project or parent directory.") + null + } + } } - tasks { withType().configureEach { options.encoding = "UTF-8" - sourceCompatibility = javaVersion.get() - targetCompatibility = javaVersion.get() - if (javaVersion.get().toInt() > 8) options.release = javaVersion.get().toInt() + sourceCompatibility = javaVersion.get().toString() + targetCompatibility = javaVersion.get().toString() + if (javaVersion.get() > 8) options.release = javaVersion } named("updateDaemonJvm") { - languageVersion = JavaLanguageVersion.of(gradleJavaVersion.get().toInt()) + languageVersion = libs.versions.gradleJava.map { JavaLanguageVersion.of(it.toInt()) } vendor = JvmVendorSpec.ADOPTIUM } withType().configureEach { defaultCharacterEncoding = "UTF-8" } withType().configureEach { options.encoding = "UTF-8" } withType().configureEach { defaultCharacterEncoding = "UTF-8" } - named("jar") { - val rootLicense = layout.projectDirectory.file("LICENSE") - val parentLicense = layout.projectDirectory.file("../LICENSE") - val licenseFile = when { - rootLicense.asFile.exists() -> { - logger.lifecycle("Using LICENSE from project root: ${rootLicense.asFile}") - rootLicense - } - parentLicense.asFile.exists() -> { - logger.lifecycle("Using LICENSE from parent directory: ${parentLicense.asFile}") - parentLicense - } - else -> { - logger.warn("No LICENSE file found in project or parent directory.") - null - } - } + withType().configureEach { licenseFile?.let { from(it) { rename { original -> "${original}_${archiveBaseName.get()}" } @@ -145,8 +116,8 @@ tasks { } processResources { val stringModVersion = modVersion.get() - val stringLoaderVersion = loaderVersion.get() - val stringJavaVersion = javaVersion.get() + val stringLoaderVersion = libs.versions.loader.get() + val stringJavaVersion = libs.versions.java.get() inputs.property("modVersion", stringModVersion) inputs.property("loaderVersion", stringLoaderVersion) inputs.property("javaVersion", stringJavaVersion) @@ -161,15 +132,6 @@ tasks { } filesMatching("**/*.mixins.json") { expand(mapOf("java" to stringJavaVersion)) } } - java { - toolchain { - languageVersion = JavaLanguageVersion.of(javaVersion.get()) - vendor = JvmVendorSpec.ADOPTIUM - } - sourceCompatibility = JavaVersion.toVersion(javaVersion.get().toInt()) - targetCompatibility = JavaVersion.toVersion(javaVersion.get().toInt()) - withSourcesJar() - } } // Removes LWJGL2 dependencies configurations.configureEach { exclude(group = "org.lwjgl.lwjgl") } diff --git a/gradle.properties b/gradle.properties index 944ffad59..5d4f444b9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,43 +7,11 @@ org.gradle.warning.mode = all # tasks instead, then this can (and should) be set to true. org.gradle.configuration-cache = false ########################################################################## -# Standard BTA Dependencies -# Check this on https://downloads.betterthanadventure.net/bta-client/ -bta_version = 7.3_04 -# Options are release, prerelease, nightly, and misc. -bta_channel = release -# Check this on https://maven.thesignalumproject.net/#/infrastructure/net/fabricmc/fabric-loader/ -loader_version = 0.17.3-bta.8 -# Check this on https://maven.thesignalumproject.net/#/infrastructure/fabric-loom/fabric-loom.gradle.plugin/ -loom_version = 1.13.0-bta -# The Java version the JDK will be for compiling and running code. -java_version = 8 -# The Java version the JDK will be for running Gradle. -gradle_java_version = 21 -########################################################################## # Mod Properties mod_version = 4.0.0 mod_group = turniplabs mod_name = modmenu-bta ########################################################################## -# Mod Dependencies -# Check this on https://github.com/Better-than-Adventure/legacy-lwjgl3/releases/latest/ -legacy_lwjgl_version = 1.0.6 -########################################################################## -# 3rd Party Dependencies -# Check this on https://central.sonatype.com/artifact/org.slf4j/slf4j-api/ -slf4j_api_version = 2.0.17 -# Check this on https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-api/ -log4j_version = 2.20.0 -# Check this on https://central.sonatype.com/artifact/com.google.guava/guava/ -guava_version = 33.5.0-jre -# Check this on https://central.sonatype.com/artifact/com.google.code.gson/gson/ -gson_version = 2.13.2 -# Check this on https://central.sonatype.com/artifact/org.apache.commons/commons-lang3/ -commons_lang3_version = 3.20.0 -# This should match the version used by the current BTA release. -lwjgl_version = 3.3.3 -########################################################################## # Plugin Dependency # Check this on https://plugins.gradle.org/plugin/org.gradle.toolchains.foojay-resolver-convention/ foojay_resolver_version = 1.0.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..45f474388 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,54 @@ +[versions] +########################################################################## +# Plugins +# Check this on https://maven.thesignalumproject.net/#/infrastructure/fabric-loom/fabric-loom.gradle.plugin/ +loom = "1.13.0-bta" +# Check this on https://plugins.gradle.org/plugin/com.smushytaco.lwjgl3/ +lwjglPlugin = "1.0.0" +########################################################################## +# Java Configuration +# The Java version the JDK will be for compiling and running code. +java = "8" +# The Java version the JDK will be for running Gradle. +gradleJava = "21" +########################################################################## +# Mod Dependencies +# Check this on https://downloads.betterthanadventure.net/bta-client/ +bta = "7.3_04" +# Options are release, prerelease, nightly, and misc. +btaChannel = "release" +# Check this on https://maven.thesignalumproject.net/#/infrastructure/net/fabricmc/fabric-loader/ +loader = "0.17.3-bta.8" +# Check this on https://github.com/Better-than-Adventure/legacy-lwjgl3/releases/latest/ +legacyLwjgl = "1.0.6" +########################################################################## +# Dependencies +# Check this on https://central.sonatype.com/artifact/org.slf4j/slf4j-api/ +slf4jApi = "2.0.17" +# Check this on https://central.sonatype.com/artifact/com.google.guava/guava/ +guava = "33.5.0-jre" +# Check this on https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-api/ +log4j = "2.20.0" +# Check this on https://central.sonatype.com/artifact/com.google.code.gson/gson/ +gson = "2.13.2" +# Check this on https://central.sonatype.com/artifact/org.apache.commons/commons-lang3/ +commonsLang3 = "3.20.0" +# This should match the version used by the current BTA release. +lwjgl = "3.3.3" +########################################################################## + +[libraries] +loader = { group = "net.fabricmc", name = "fabric-loader", version.ref = "loader" } +legacyLwjgl = { group = "com.github.Better-than-Adventure", name = "legacy-lwjgl3", version.ref = "legacyLwjgl" } +slf4jApi = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4jApi" } +guava = { group = "com.google.guava", name = "guava", version.ref = "guava" } +log4j-slf4j2-impl = { group = "org.apache.logging.log4j", name = "log4j-slf4j2-impl", version.ref = "log4j" } +log4j-core = { group = "org.apache.logging.log4j", name = "log4j-core", version.ref = "log4j" } +log4j-api = { group = "org.apache.logging.log4j", name = "log4j-api", version.ref = "log4j" } +log4j-api12 = { group = "org.apache.logging.log4j", name = "log4j-1.2-api", version.ref = "log4j" } +gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } +commonsLang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commonsLang3" } + +[plugins] +loom = { id = "fabric-loom", version.ref = "loom" } +lwjgl = { id = "com.smushytaco.lwjgl3", version.ref = "lwjglPlugin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 255222fab..dbb385b51 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,19 +1,60 @@ val modName = providers.gradleProperty("mod_name") rootProject.name = modName.get() pluginManagement { + fun isRepoHealthy(url: String): Boolean { + var connection: javax.net.ssl.HttpsURLConnection? = null + return try { + connection = java.net.URI(url).toURL().openConnection() as javax.net.ssl.HttpsURLConnection + connection.requestMethod = "HEAD" + connection.connectTimeout = 2000 + connection.readTimeout = 2000 + connection.instanceFollowRedirects = true + connection.connect() + val code = connection.responseCode + code in 200..399 + } catch (_: Exception) { + false + } finally { + connection?.disconnect() + } + } + fun repoUrlWithFallbacks(candidates: List): String { + if (candidates.isEmpty()) { + val badLink = "https://mock.httpstatus.io/500" + logger.error("No repositories have been provided. Defaulting to: {}", badLink) + return badLink + } + val chosenRepository = candidates.firstOrNull { isRepoHealthy(it) } ?: run { + if (candidates.size == 1) { + logger.error("\"{}\" could not be resolved.", candidates.first()) + } else { + logger.error("All {} repositories could not be resolved. Defaulting to: {}", candidates.size, candidates.first()) + } + return candidates.first() + } + logger.lifecycle("Using \"{}\" as the Fabric repository.", chosenRepository) + return chosenRepository + } repositories { - gradlePluginPortal() - maven("https://maven.fabricmc.net/") { name = "Fabric" } + maven( + repoUrlWithFallbacks( + listOf( + "https://maven.fabricmc.net", + "https://maven2.fabricmc.net", + "https://maven3.fabricmc.net" + ) + ) + ) { name = "Fabric" } maven("https://jitpack.io") { name = "Jitpack" } maven("https://maven.thesignalumproject.net/infrastructure") { name = "SignalumMavenInfrastructure" } + mavenCentral() + gradlePluginPortal() } val foojayResolverVersion = providers.gradleProperty("foojay_resolver_version") - val loomVersion = providers.gradleProperty("loom_version") plugins { id("org.gradle.toolchains.foojay-resolver-convention").version(foojayResolverVersion.get()) - id("fabric-loom").version(loomVersion.get()) } } plugins { id("org.gradle.toolchains.foojay-resolver-convention") -} +} \ No newline at end of file From b1497162550964eb463aa3028eb41723b72bd659 Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Sat, 29 Nov 2025 13:01:22 -0800 Subject: [PATCH 13/14] Improved processResources --- build.gradle.kts | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4c82a2164..7445e5ee0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -115,22 +115,14 @@ tasks { } } processResources { - val stringModVersion = modVersion.get() - val stringLoaderVersion = libs.versions.loader.get() - val stringJavaVersion = libs.versions.java.get() - inputs.property("modVersion", stringModVersion) - inputs.property("loaderVersion", stringLoaderVersion) - inputs.property("javaVersion", stringJavaVersion) - filesMatching("fabric.mod.json") { - expand( - mapOf( - "version" to stringModVersion, - "fabricloader" to stringLoaderVersion, - "java" to stringJavaVersion - ) - ) - } - filesMatching("**/*.mixins.json") { expand(mapOf("java" to stringJavaVersion)) } + val resourceMap = mapOf( + "version" to modVersion.get(), + "fabricloader" to libs.versions.loader.get(), + "java" to libs.versions.java.get() + ) + inputs.properties(resourceMap) + filesMatching("fabric.mod.json") { expand(resourceMap) } + filesMatching("**/*.mixins.json") { expand(resourceMap.filterKeys { it == "java" }) } } } // Removes LWJGL2 dependencies @@ -155,4 +147,3 @@ publishing { } } } - From 1a321eaeb4d821031d49c27e00f29214251d5a23 Mon Sep 17 00:00:00 2001 From: Nikan Radan Date: Tue, 2 Dec 2025 20:08:44 -0800 Subject: [PATCH 14/14] Minor Enhancements Accounts for whitespace --- .../github/prospector/modmenu/gui/ModListScreen.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java index c172934ba..88ece1e9a 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java @@ -186,7 +186,7 @@ public void render(Minecraft mc, int mouseX, int mouseY) { public void drawButton(Minecraft mc, int mouseX, int mouseY) { visible = selected != null; Optional link = selected != null ? selected.getMetadata().getContact().get("homepage") : Optional.empty(); - enabled = visible && link.isPresent() && !link.get().isEmpty(); + enabled = visible && link.isPresent() && !link.get().trim().isEmpty(); super.drawButton(mc, mouseX, mouseY); } }; @@ -202,7 +202,7 @@ public void drawButton(Minecraft mc, int mouseX, int mouseY) { public void drawButton(Minecraft mc, int mouseX, int mouseY) { visible = selected != null; Optional link = selected != null ? selected.getMetadata().getContact().get("issues") : Optional.empty(); - enabled = visible && link.isPresent() && !link.get().isEmpty(); + enabled = visible && link.isPresent() && !link.get().trim().isEmpty(); super.drawButton(mc, mouseX, mouseY); } }; @@ -260,7 +260,8 @@ public void render(Minecraft mc, int mouseX, int mouseY) { ) { @Override public void drawButton(Minecraft mc, int mouseX, int mouseY) { - visible = enabled = filterOptionsShown; + visible = filterOptionsShown; + enabled = filterOptionsShown; this.displayString = i18n.translateKeyAndFormat( "modmenu.sorting", ModMenuConfigManager.getConfig().getSorting().getName() @@ -277,7 +278,8 @@ public void drawButton(Minecraft mc, int mouseX, int mouseY) { ) { @Override public void drawButton(Minecraft mc, int mouseX, int mouseY) { - visible = enabled = filterOptionsShown; + visible = filterOptionsShown; + enabled = filterOptionsShown; this.displayString = i18n.translateKeyAndFormat( "modmenu.showLibraries", i18n.translateKey("modmenu.showLibraries."