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..7445e5ee0 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,149 @@ +@file:Suppress("UnstableApiUsage") +import com.smushytaco.lwjgl_gradle.Preset +plugins { + alias(libs.plugins.loom) + alias(libs.plugins.lwjgl) + java + `maven-publish` +} +val modVersion = providers.gradleProperty("mod_version") +val modGroup = providers.gradleProperty("mod_group") +val modName = providers.gradleProperty("mod_name") + +val javaVersion = libs.versions.java.map { it.toInt() } + +base.archivesName = modName +group = modGroup.get() +version = modVersion.get() +loom { + noIntermediateMappings() + 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") + 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/${libs.versions.btaChannel.get()}/") { + patternLayout { artifact("/v[revision]/client.jar") } + metadataSources { artifact() } + } + ivy("https://downloads.betterthanadventure.net/bta-server/${libs.versions.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() } + } +} +lwjgl { + version = libs.versions.lwjgl + implementation(Preset.MINIMAL_OPENGL) +} +dependencies { + minecraft("::${libs.versions.bta.get()}") + mappings(loom.layered {}) + + // https://piston-data.mojang.com/v1/objects/43db9b498cb67058d2e12d394e6507722e71bb45/client.jar + modRuntimeOnly("objects:client:43db9b498cb67058d2e12d394e6507722e71bb45") + 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().toString() + targetCompatibility = javaVersion.get().toString() + if (javaVersion.get() > 8) options.release = javaVersion + } + named("updateDaemonJvm") { + 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" } + withType().configureEach { + licenseFile?.let { + from(it) { + rename { original -> "${original}_${archiveBaseName.get()}" } + } + } + } + processResources { + 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 +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..5d4f444b9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,18 @@ -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 +########################################################################## +# Mod Properties +mod_version = 4.0.0 +mod_group = turniplabs +mod_name = modmenu-bta +########################################################################## +# 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/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/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/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index deedc7fa5..8bdaf60c7 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c1c32a50d..071ff21b9 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.1-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..dbb385b51 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +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 { + 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") + plugins { + id("org.gradle.toolchains.foojay-resolver-convention").version(foojayResolverVersion.get()) + } +} +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") +} \ No newline at end of file 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..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,65 +1,56 @@ package io.github.prospector.modmenu.config; - 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..88ece1e9a 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java @@ -18,492 +18,703 @@ 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; 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.io.IOException; +import java.nio.file.Path; import java.text.NumberFormat; 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 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); + } + } + + 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") + 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(); + 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; + Optional link = selected != null ? selected.getMetadata().getContact().get("homepage") : Optional.empty(); + enabled = visible && link.isPresent() && !link.get().trim().isEmpty(); + 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; + Optional link = selected != null ? selected.getMetadata().getContact().get("issues") : Optional.empty(); + enabled = visible && link.isPresent() && !link.get().trim().isEmpty(); + 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 = filterOptionsShown; + 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 = filterOptionsShown; + 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); + String modsFolder = i18n.translateKey("modmenu.modsFolder"); + this.buttons.add(ButtonUtil.createButton( + MODS_FOLDER_BUTTON_ID, + this.width / 2 - 154, + this.height - 28, + 150, 20, + 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 == null || done.equals("modmenu.done") ? "Done" : 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(ModListScreen::openUrl); + break; + } + case ISSUES_BUTTON_ID: { + ModMetadata metadata = Objects.requireNonNull(selected).getMetadata(); + metadata.getContact().get("issues").ifPresent(ModListScreen::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: { + Path modsFolder = FabricLoader.getInstance().getGameDir().resolve("mods"); + 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 (IOException e) { + ModMenu.LOGGER.error("Failed to open mods folder", 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); } - trimmedName = trimmedName.isEmpty() ? "..." : trimmedName.substring(0, trimmedName.length() - 1) + "..."; } - return trimmedName; } + 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(); - } - - 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); - } + 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; + } + } + + 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); + } + } + + 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..27915a47a 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 = original.call(ModMenu.class, 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/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 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",