diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4ffbd90 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# This is the universal Text Editor Configuration +# for all GTNewHorizons projects +# See: https://editorconfig.org/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{bat,ini}] +end_of_line = crlf + +[*.{dtd,json,info,mcmeta,md,sh,svg,xml,xsd,xsl,yaml,yml}] +indent_size = 2 + +[*.lang] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6f227db --- /dev/null +++ b/.gitattributes @@ -0,0 +1,41 @@ +* text eol=lf + +*.[jJ][aA][rR] binary + +*.[pP][nN][gG] binary +*.[jJ][pP][gG] binary +*.[jJ][pP][eE][gG] binary +*.[gG][iI][fF] binary +*.[tT][iI][fF] binary +*.[tT][iI][fF][fF] binary +*.[iI][cC][oO] binary +*.[sS][vV][gG] text +*.[eE][pP][sS] binary + +*.[kK][aA][rR] binary +*.[mM]4[aA] binary +*.[mM][iI][dD] binary +*.[mM][iI][dD][iI] binary +*.[mM][pP]3 binary +*.[oO][gG][gG] binary +*.[rR][aA] binary + +*.7[zZ] binary +*.[gG][zZ] binary +*.[tT][aA][rR] binary +*.[tT][gG][zZ] binary +*.[zZ][iI][pP] binary + +*.[tT][cC][nN] binary +*.[sS][oO] binary +*.[dD][lL][lL] binary +*.[dD][yY][lL][iI][bB] binary +*.[pP][sS][dD] binary + +*.[pP][aA][tT][cC][hH] -text + +*.[bB][aA][tT] text eol=crlf +*.[cC][mM][dD] text eol=crlf +*.[pP][sS]1 text eol=crlf + +*[aA][uU][tT][oO][gG][eE][nN][eE][rR][aA][tT][eE][dD]* binary \ No newline at end of file diff --git a/.github/scripts/test_no_error_reports b/.github/scripts/test_no_error_reports new file mode 100644 index 0000000..1fcc739 --- /dev/null +++ b/.github/scripts/test_no_error_reports @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# bashsupport disable=BP5006 # Global environment variables +RUNDIR="run" \ + CRASH="crash-reports" \ + SERVERLOG="server.log" + +# enable nullglob to get 0 results when no match rather than the pattern +shopt -s nullglob + +# store matches in array +crash_reports=("$RUNDIR/$CRASH/crash"*.txt) + +# if array not empty there are crash_reports +if [ "${#crash_reports[@]}" -gt 0 ]; then + # get the latest crash_report from array + latest_crash_report="${crash_reports[-1]}" + { + printf 'Latest crash report detected %s:\n' "${latest_crash_report##*/}" + cat "$latest_crash_report" + } >&2 + exit 1 +fi + +if grep --quiet --fixed-strings 'Fatal errors were detected' "$SERVERLOG"; then + { + printf 'Fatal errors detected:\n' + cat server.log + } >&2 + exit 1 +fi + +if grep --quiet --fixed-strings 'The state engine was in incorrect state ERRORED and forced into state SERVER_STOPPED' \ + "$SERVERLOG"; then + { + printf 'Server force stopped:' + cat server.log + } >&2 + exit 1 +fi + +if ! grep --quiet --perl-regexp --only-matching '.+Done \(.+\)\! For help, type "help" or "\?"' "$SERVERLOG"; then + { + printf 'Server did not finish startup:' + cat server.log + } >&2 + exit 1 +fi + +printf 'No crash reports detected' +exit 0 diff --git a/.github/scripts/update_version b/.github/scripts/update_version new file mode 100644 index 0000000..3e5f752 --- /dev/null +++ b/.github/scripts/update_version @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +if ! git diff --name-only HEAD HEAD~1 | grep -qF 'build.gradle'; then + new_version="$(date +%s)" + sed --in-place "s!^//version:.*!//version: $new_version!g" build.gradle + git add build.gradle + git commit -m "[ci skip] update build script version to $new_version" + git push + printf 'Updated buildscript version to %s\n' "$new_version" +else + printf 'Ignored buildscript version update: no changes detected\n' +fi diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000..56a1ad5 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,45 @@ +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Build and test + +on: + pull_request: + branches: [ master, main ] + push: + branches: [ master, main ] + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up JDK 8 + uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'adopt' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup the workspace + run: ./gradlew setupCIWorkspace + + - name: Build the mod + run: ./gradlew build + + - name: Run server for 1.5 minutes + run: | + mkdir run + echo "eula=true" > run/eula.txt + timeout 90 ./gradlew runServer 2>&1 | tee -a server.log || true + + - name: Test no errors reported during server run + run: | + chmod +x .github/scripts/test_no_error_reports + .github/scripts/test_no_error_reports diff --git a/.github/workflows/release-tags.yml b/.github/workflows/release-tags.yml new file mode 100644 index 0000000..c86d888 --- /dev/null +++ b/.github/workflows/release-tags.yml @@ -0,0 +1,51 @@ +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Release tagged build + +on: + push: + tags: + - '*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set release version + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - name: Set up JDK 8 + uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'adopt' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup the workspace + run: ./gradlew setupCIWorkspace + + - name: Build the mod + run: ./gradlew build + + - name: Release under current tag + uses: "marvinpinto/action-automatic-releases@latest" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + automatic_release_tag: "${{ env.RELEASE_VERSION }}" + prerelease: false + title: "${{ env.RELEASE_VERSION }}" + files: build/libs/*.jar + + - name: Publish to Maven + run: ./gradlew publish + env: + MAVEN_USER: ${{ secrets.MAVEN_USER }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48c525b --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +.gradle +.settings +/.idea/ +/.vscode/ +/run/ +/build/ +/eclipse/ +.classpath +.project +/bin/ +/config/ +/crash-reports/ +/logs/ +options.txt +/saves/ +usernamecache.json +banned-ips.json +banned-players.json +eula.txt +ops.json +server.properties +servers.dat +usercache.json +whitelist.json +/out/ +*.iml +*.ipr +*.iws +src/main/resources/mixins.*.json +*.bat +*.DS_Store +!gradlew.bat diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..a6b5f68 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,3 @@ +# Any Github changes require admin approval +/.github/** @GTNewHorizons/admin + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..4e0477b --- /dev/null +++ b/README.MD @@ -0,0 +1,5 @@ +# Modular UI - Rendering mod for Minecraft 1.7.10 + +This is a WIP project for backporting [Modular UI](https://github.com/CleanroomMC/ModularUI) mod from 1.12.2 + +This mod is under LGPL-3 license diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..997b94d --- /dev/null +++ b/build.gradle @@ -0,0 +1,968 @@ +//version: 1661114848 +/* + DO NOT CHANGE THIS FILE! + Also, you may replace this file at any time if there is an update available. + Please check https://github.com/GTNewHorizons/ExampleMod1.7.10/blob/main/build.gradle for updates. + */ + + +import com.github.jengelman.gradle.plugins.shadow.tasks.ConfigureShadowRelocation +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.internal.logging.text.StyledTextOutput.Style +import org.gradle.internal.logging.text.StyledTextOutputFactory + +import java.nio.file.Files +import java.nio.file.Paths +import java.util.concurrent.TimeUnit +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +buildscript { + repositories { + mavenCentral() + + maven { + name 'forge' + url 'https://maven.minecraftforge.net' + } + maven { + // GTNH ForgeGradle Fork + name = "GTNH Maven" + url = "http://jenkins.usrv.eu:8081/nexus/content/groups/public/" + } + maven { + name 'sonatype' + url 'https://oss.sonatype.org/content/repositories/snapshots/' + } + maven { + name 'Scala CI dependencies' + url 'https://repo1.maven.org/maven2/' + } + } + dependencies { + classpath 'net.minecraftforge.gradle:ForgeGradle:1.2.9' + } +} +plugins { + id 'java-library' + id 'idea' + id 'eclipse' + id 'scala' + id 'maven-publish' + id 'org.jetbrains.kotlin.jvm' version '1.5.30' apply false + id 'org.jetbrains.kotlin.kapt' version '1.5.30' apply false + id 'com.google.devtools.ksp' version '1.5.30-1.0.0' apply false + id 'org.ajoberstar.grgit' version '4.1.1' + id 'com.github.johnrengelman.shadow' version '4.0.4' + id 'com.palantir.git-version' version '0.13.0' apply false + id 'de.undercouch.download' version '5.0.1' + id 'com.github.gmazzo.buildconfig' version '3.0.3' apply false + id 'com.diffplug.spotless' version '6.7.2' apply false +} +boolean settingsupdated = verifySettingsGradle() +settingsupdated = verifyGitAttributes() || settingsupdated +if (settingsupdated) + throw new GradleException("Settings has been updated, please re-run task.") + +dependencies { + implementation 'com.diffplug:blowdryer:1.6.0' +} + +apply plugin: 'com.diffplug.blowdryer' + +if (project.file('.git/HEAD').isFile()) { + apply plugin: 'com.palantir.git-version' +} + +def out = services.get(StyledTextOutputFactory).create('an-output') + +apply plugin: 'forge' + +def projectJavaVersion = JavaLanguageVersion.of(8) + +java { + toolchain { + languageVersion.set(projectJavaVersion) + } +} + +idea { + module { + inheritOutputDirs = true + downloadJavadoc = true + downloadSources = true + } +} + +boolean disableSpotless = project.hasProperty("disableSpotless") ? project.disableSpotless.toBoolean() : false + +if (!disableSpotless) { + apply plugin: 'com.diffplug.spotless' + apply from: Blowdryer.file('spotless.gradle') +} + +if (JavaVersion.current() != JavaVersion.VERSION_1_8) { + throw new GradleException("This project requires Java 8, but it's running on " + JavaVersion.current()) +} + +checkPropertyExists("modName") +checkPropertyExists("modId") +checkPropertyExists("modGroup") +checkPropertyExists("autoUpdateBuildScript") +checkPropertyExists("minecraftVersion") +checkPropertyExists("forgeVersion") +checkPropertyExists("replaceGradleTokenInFile") +checkPropertyExists("gradleTokenModId") +checkPropertyExists("gradleTokenModName") +checkPropertyExists("gradleTokenVersion") +checkPropertyExists("gradleTokenGroupName") +checkPropertyExists("apiPackage") +checkPropertyExists("accessTransformersFile") +checkPropertyExists("usesMixins") +checkPropertyExists("mixinPlugin") +checkPropertyExists("mixinsPackage") +checkPropertyExists("coreModClass") +checkPropertyExists("containsMixinsAndOrCoreModOnly") +checkPropertyExists("usesShadowedDependencies") +checkPropertyExists("developmentEnvironmentUserName") + +boolean noPublishedSources = project.hasProperty("noPublishedSources") ? project.noPublishedSources.toBoolean() : false +boolean usesMixinDebug = project.hasProperty('usesMixinDebug') ?: project.usesMixins.toBoolean() +boolean forceEnableMixins = project.hasProperty('forceEnableMixins') ? project.forceEnableMixins.toBoolean() : false +String channel = project.hasProperty('channel') ? project.channel : 'stable' +String mappingsVersion = project.hasProperty('mappingsVersion') ? project.mappingsVersion : '12' +String javaSourceDir = "src/main/java/" +String scalaSourceDir = "src/main/scala/" +String kotlinSourceDir = "src/main/kotlin/" + +String targetPackageJava = javaSourceDir + modGroup.toString().replaceAll("\\.", "/") +String targetPackageScala = scalaSourceDir + modGroup.toString().replaceAll("\\.", "/") +String targetPackageKotlin = kotlinSourceDir + modGroup.toString().replaceAll("\\.", "/") +if (!(getFile(targetPackageJava).exists() || getFile(targetPackageScala).exists() || getFile(targetPackageKotlin).exists())) { + throw new GradleException("Could not resolve \"modGroup\"! Could not find " + targetPackageJava + " or " + targetPackageScala + " or " + targetPackageKotlin) +} + +if (apiPackage) { + targetPackageJava = javaSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + apiPackage.toString().replaceAll("\\.", "/") + targetPackageScala = scalaSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + apiPackage.toString().replaceAll("\\.", "/") + targetPackageKotlin = kotlinSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + apiPackage.toString().replaceAll("\\.", "/") + if (!(getFile(targetPackageJava).exists() || getFile(targetPackageScala).exists() || getFile(targetPackageKotlin).exists())) { + throw new GradleException("Could not resolve \"apiPackage\"! Could not find " + targetPackageJava + " or " + targetPackageScala + " or " + targetPackageKotlin) + } +} + +if (accessTransformersFile) { + String targetFile = "src/main/resources/META-INF/" + accessTransformersFile + if (!getFile(targetFile).exists()) { + throw new GradleException("Could not resolve \"accessTransformersFile\"! Could not find " + targetFile) + } +} + +if (usesMixins.toBoolean()) { + if (mixinsPackage.isEmpty() || mixinPlugin.isEmpty()) { + throw new GradleException("\"mixinPlugin\" requires \"mixinsPackage\" and \"mixinPlugin\" to be set!") + } + + targetPackageJava = javaSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + mixinsPackage.toString().replaceAll("\\.", "/") + targetPackageScala = scalaSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + mixinsPackage.toString().replaceAll("\\.", "/") + targetPackageKotlin = kotlinSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + mixinsPackage.toString().replaceAll("\\.", "/") + if (!(getFile(targetPackageJava).exists() || getFile(targetPackageScala).exists() || getFile(targetPackageKotlin).exists())) { + throw new GradleException("Could not resolve \"mixinsPackage\"! Could not find " + targetPackageJava + " or " + targetPackageScala + " or " + targetPackageKotlin) + } + + String targetFileJava = javaSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + mixinPlugin.toString().replaceAll("\\.", "/") + ".java" + String targetFileScala = scalaSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + mixinPlugin.toString().replaceAll("\\.", "/") + ".scala" + String targetFileScalaJava = scalaSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + mixinPlugin.toString().replaceAll("\\.", "/") + ".java" + String targetFileKotlin = kotlinSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + mixinPlugin.toString().replaceAll("\\.", "/") + ".kt" + if (!(getFile(targetFileJava).exists() || getFile(targetFileScala).exists() || getFile(targetFileScalaJava).exists() || getFile(targetFileKotlin).exists())) { + throw new GradleException("Could not resolve \"mixinPlugin\"! Could not find " + targetFileJava + " or " + targetFileScala + " or " + targetFileScalaJava + " or " + targetFileKotlin) + } +} + +if (coreModClass) { + String targetFileJava = javaSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + coreModClass.toString().replaceAll("\\.", "/") + ".java" + String targetFileScala = scalaSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + coreModClass.toString().replaceAll("\\.", "/") + ".scala" + String targetFileScalaJava = scalaSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + coreModClass.toString().replaceAll("\\.", "/") + ".java" + String targetFileKotlin = kotlinSourceDir + modGroup.toString().replaceAll("\\.", "/") + "/" + coreModClass.toString().replaceAll("\\.", "/") + ".kt" + if (!(getFile(targetFileJava).exists() || getFile(targetFileScala).exists() || getFile(targetFileScalaJava).exists() || getFile(targetFileKotlin).exists())) { + throw new GradleException("Could not resolve \"coreModClass\"! Could not find " + targetFileJava + " or " + targetFileScala + " or " + targetFileScalaJava + " or " + targetFileKotlin) + } +} + +configurations.all { + resolutionStrategy.cacheChangingModulesFor(0, TimeUnit.SECONDS) + + // Make sure GregTech build won't time out + System.setProperty("org.gradle.internal.http.connectionTimeout", 120000 as String) + System.setProperty("org.gradle.internal.http.socketTimeout", 120000 as String) +} + +// Fix Jenkins' Git: chmod a file should not be detected as a change and append a '.dirty' to the version +try { + 'git config core.fileMode false'.execute() +} +catch (Exception ignored) { + out.style(Style.Failure).println("git isn't installed at all") +} + +// Pulls version first from the VERSION env and then git tag +String identifiedVersion +String versionOverride = System.getenv("VERSION") ?: null +try { + identifiedVersion = versionOverride == null ? gitVersion() : versionOverride +} +catch (Exception ignored) { + out.style(Style.Failure).text( + 'This mod must be version controlled by Git AND the repository must provide at least one tag,\n' + + 'or the VERSION override must be set! ').style(Style.SuccessHeader).text('(Do NOT download from GitHub using the ZIP option, instead\n' + + 'clone the repository, see ').style(Style.Info).text('https://gtnh.miraheze.org/wiki/Development').style(Style.SuccessHeader).println(' for details.)' + ) + versionOverride = 'NO-GIT-TAG-SET' + identifiedVersion = versionOverride +} +version = minecraftVersion + '-' + identifiedVersion +ext { + modVersion = identifiedVersion +} + +if (identifiedVersion == versionOverride) { + out.style(Style.Failure).text('Override version to ').style(Style.Identifier).text(modVersion).style(Style.Failure).println('!\7') +} + +group = modGroup +if (project.hasProperty("customArchiveBaseName") && customArchiveBaseName) { + archivesBaseName = customArchiveBaseName +} else { + archivesBaseName = modId +} + +def arguments = [] +def jvmArguments = [] + +if (usesMixins.toBoolean() || forceEnableMixins) { + arguments += [ + "--tweakClass org.spongepowered.asm.launch.MixinTweaker" + ] + if (usesMixinDebug.toBoolean()) { + jvmArguments += [ + "-Dmixin.debug.countInjections=true", + "-Dmixin.debug.verbose=true", + "-Dmixin.debug.export=true" + ] + } +} + +minecraft { + version = minecraftVersion + '-' + forgeVersion + '-' + minecraftVersion + runDir = 'run' + + if (replaceGradleTokenInFile) { + replaceIn replaceGradleTokenInFile + if (gradleTokenModId) { + replace gradleTokenModId, modId + } + if (gradleTokenModName) { + replace gradleTokenModName, modName + } + if (gradleTokenVersion) { + replace gradleTokenVersion, modVersion + } + if (gradleTokenGroupName) { + replace gradleTokenGroupName, modGroup + } + } + + clientIntellijRun { + args(arguments) + jvmArgs(jvmArguments) + + if (developmentEnvironmentUserName) { + args("--username", developmentEnvironmentUserName) + } + } + + serverIntellijRun { + args(arguments) + jvmArgs(jvmArguments) + } +} + +if (file('addon.gradle').exists()) { + apply from: 'addon.gradle' +} + +apply from: 'repositories.gradle' + +configurations { + implementation.extendsFrom(shadowImplementation) // TODO: remove after all uses are refactored + implementation.extendsFrom(shadowCompile) + implementation.extendsFrom(shadeCompile) +} + +repositories { + maven { + name 'Overmind forge repo mirror' + url 'https://gregtech.overminddl1.com/' + } + if (usesMixins.toBoolean() || forceEnableMixins) { + maven { + name 'sponge' + url 'https://repo.spongepowered.org/repository/maven-public' + } + maven { + url 'https://jitpack.io' + } + } +} + +dependencies { + if (usesMixins.toBoolean()) { + annotationProcessor('org.ow2.asm:asm-debug-all:5.0.3') + annotationProcessor('com.google.guava:guava:24.1.1-jre') + annotationProcessor('com.google.code.gson:gson:2.8.6') + annotationProcessor('org.spongepowered:mixin:0.8-SNAPSHOT') + } + if (usesMixins.toBoolean() || forceEnableMixins) { + // using 0.8 to workaround a issue in 0.7 which fails mixin application + compile('com.github.GTNewHorizons:SpongePoweredMixin:0.7.12-GTNH') { + // Mixin includes a lot of dependencies that are too up-to-date + exclude module: 'launchwrapper' + exclude module: 'guava' + exclude module: 'gson' + exclude module: 'commons-io' + exclude module: 'log4j-core' + } + compile('com.github.GTNewHorizons:SpongeMixins:1.5.0') + } +} + +apply from: 'dependencies.gradle' + +def mixingConfigRefMap = 'mixins.' + modId + '.refmap.json' +def refMap = "${tasks.compileJava.temporaryDir}" + File.separator + mixingConfigRefMap +def mixinSrg = "${tasks.reobf.temporaryDir}" + File.separator + "mixins.srg" + +task generateAssets { + if (usesMixins.toBoolean()) { + def mixinConfigFile = getFile("/src/main/resources/mixins." + modId + ".json"); + if (!mixinConfigFile.exists()) { + mixinConfigFile.text = """{ + "required": true, + "minVersion": "0.7.11", + "package": "${modGroup}.${mixinsPackage}", + "plugin": "${modGroup}.${mixinPlugin}", + "refmap": "${mixingConfigRefMap}", + "target": "@env(DEFAULT)", + "compatibilityLevel": "JAVA_8", + "mixins": [], + "client": [], + "server": [] +} +""" + } + } +} + +task relocateShadowJar(type: ConfigureShadowRelocation) { + target = tasks.shadowJar + prefix = modGroup + ".shadow" +} + +shadowJar { + project.configurations.shadeCompile.each { dep -> + from(project.zipTree(dep)) { + exclude 'META-INF', 'META-INF/**' + } + } + + manifest { + attributes(getManifestAttributes()) + } + + minimize() // This will only allow shading for actually used classes + configurations = [ + project.configurations.shadowImplementation, + project.configurations.shadowCompile + ] + dependsOn(relocateShadowJar) +} + +jar { + project.configurations.shadeCompile.each { dep -> + from(project.zipTree(dep)) { + exclude 'META-INF', 'META-INF/**' + } + } + + manifest { + attributes(getManifestAttributes()) + } + + if (usesShadowedDependencies.toBoolean()) { + dependsOn(shadowJar) + enabled = false + } +} + +reobf { + if (usesMixins.toBoolean()) { + addExtraSrgFile mixinSrg + } +} + +afterEvaluate { + if (usesMixins.toBoolean()) { + tasks.compileJava { + options.compilerArgs += [ + "-AreobfSrgFile=${tasks.reobf.srg}", + "-AoutSrgFile=${mixinSrg}", + "-AoutRefMapFile=${refMap}", + // Elan: from what I understand they are just some linter configs so you get some warning on how to properly code + "-XDenableSunApiLintControl", + "-XDignore.symbol.file" + ] + } + } +} + +runClient { + if (developmentEnvironmentUserName) { + arguments += [ + "--username", + developmentEnvironmentUserName + ] + } + + args(arguments) + jvmArgs(jvmArguments) +} + +runServer { + args(arguments) + jvmArgs(jvmArguments) +} + +tasks.withType(JavaExec).configureEach { + javaLauncher.set( + javaToolchains.launcherFor { + languageVersion = projectJavaVersion + } + ) +} + +processResources { + // this will ensure that this task is redone when the versions change. + inputs.property "version", project.version + inputs.property "mcversion", project.minecraft.version + exclude("spotless.gradle") + + // replace stuff in mcmod.info, nothing else + from(sourceSets.main.resources.srcDirs) { + include 'mcmod.info' + + // replace modVersion and minecraftVersion + expand "minecraftVersion": project.minecraft.version, + "modVersion": modVersion, + "modId": modId, + "modName": modName + } + + if (usesMixins.toBoolean()) { + from refMap + } + + // copy everything else that's not the mcmod.info + from(sourceSets.main.resources.srcDirs) { + exclude 'mcmod.info' + exclude 'spotless.gradle' + } +} + +def getManifestAttributes() { + def manifestAttributes = [:] + if (!containsMixinsAndOrCoreModOnly.toBoolean() && (usesMixins.toBoolean() || coreModClass)) { + manifestAttributes += ["FMLCorePluginContainsFMLMod": true] + } + + if (accessTransformersFile) { + manifestAttributes += ["FMLAT": accessTransformersFile.toString()] + } + + if (coreModClass) { + manifestAttributes += ["FMLCorePlugin": modGroup + "." + coreModClass] + } + + if (usesMixins.toBoolean()) { + manifestAttributes += [ + "TweakClass" : "org.spongepowered.asm.launch.MixinTweaker", + "MixinConfigs" : "mixins." + modId + ".json", + "ForceLoadAsMod": !containsMixinsAndOrCoreModOnly.toBoolean() + ] + } + return manifestAttributes +} + +task sourcesJar(type: Jar) { + from(sourceSets.main.allSource) + from(file("$projectDir/LICENSE")) + getArchiveClassifier().set('sources') +} + +task shadowDevJar(type: ShadowJar) { + project.configurations.shadeCompile.each { dep -> + from(project.zipTree(dep)) { + exclude 'META-INF', 'META-INF/**' + } + } + + from sourceSets.main.output + getArchiveClassifier().set("dev") + + manifest { + attributes(getManifestAttributes()) + } + + minimize() // This will only allow shading for actually used classes + configurations = [ + project.configurations.shadowImplementation, + project.configurations.shadowCompile + ] +} + +task relocateShadowDevJar(type: ConfigureShadowRelocation) { + target = tasks.shadowDevJar + prefix = modGroup + ".shadow" +} + +task circularResolverJar(type: Jar) { + dependsOn(relocateShadowDevJar) + dependsOn(shadowDevJar) + enabled = false +} + +task devJar(type: Jar) { + project.configurations.shadeCompile.each { dep -> + from(project.zipTree(dep)) { + exclude 'META-INF', 'META-INF/**' + } + } + + from sourceSets.main.output + getArchiveClassifier().set("dev") + + manifest { + attributes(getManifestAttributes()) + } + + if (usesShadowedDependencies.toBoolean()) { + dependsOn(circularResolverJar) + enabled = false + } +} + +task apiJar(type: Jar) { + from(sourceSets.main.allSource) { + include modGroup.toString().replaceAll("\\.", "/") + "/" + apiPackage.toString().replaceAll("\\.", "/") + '/**' + } + + from(sourceSets.main.output) { + include modGroup.toString().replaceAll("\\.", "/") + "/" + apiPackage.toString().replaceAll("\\.", "/") + '/**' + } + + from(sourceSets.main.resources.srcDirs) { + include("LICENSE") + } + + getArchiveClassifier().set('api') +} + +artifacts { + if (!noPublishedSources) { + archives sourcesJar + } + archives devJar + if (apiPackage) { + archives apiJar + } +} + +// The gradle metadata includes all of the additional deps that we disabled from POM generation (including forgeBin with no groupID), +// and isn't strictly needed with the POM so just disable it. +tasks.withType(GenerateModuleMetadata) { + enabled = false +} + +// workaround variable hiding in pom processing +def projectConfigs = project.configurations + +publishing { + publications { + maven(MavenPublication) { + from components.java + if (usesShadowedDependencies.toBoolean()) { + artifact source: shadowJar, classifier: "" + } + if (!noPublishedSources) { + artifact source: sourcesJar, classifier: "sources" + } + artifact source: usesShadowedDependencies.toBoolean() ? shadowDevJar : devJar, classifier: "dev" + if (apiPackage) { + artifact source: apiJar, classifier: "api" + } + + groupId = System.getenv("ARTIFACT_GROUP_ID") ?: "com.github.GTNewHorizons" + artifactId = System.getenv("ARTIFACT_ID") ?: project.name + // Using the identified version, not project.version as it has the prepended 1.7.10 + version = System.getenv("RELEASE_VERSION") ?: identifiedVersion + + // remove extra garbage from minecraft and minecraftDeps configuration + pom.withXml { + def badArtifacts = [:].withDefault { [] as Set } + for (configuration in [ + projectConfigs.minecraft, + projectConfigs.minecraftDeps + ]) { + for (dependency in configuration.allDependencies) { + badArtifacts[dependency.group == null ? "" : dependency.group] += dependency.name + } + } + // example for specifying extra stuff to ignore + // badArtifacts["org.example.group"] += "artifactName" + + Node pomNode = asNode() + pomNode.dependencies.'*'.findAll() { + badArtifacts[it.groupId.text()].contains(it.artifactId.text()) + }.each() { + it.parent().remove(it) + } + } + } + } + + repositories { + maven { + url = "http://jenkins.usrv.eu:8081/nexus/content/repositories/releases" + credentials { + username = System.getenv("MAVEN_USER") ?: "NONE" + password = System.getenv("MAVEN_PASSWORD") ?: "NONE" + } + } + } +} + +// Updating +task updateBuildScript { + doLast { + if (performBuildScriptUpdate(projectDir.toString())) return + + print("Build script already up-to-date!") + } +} + +if (!project.getGradle().startParameter.isOffline() && isNewBuildScriptVersionAvailable(projectDir.toString())) { + if (autoUpdateBuildScript.toBoolean()) { + performBuildScriptUpdate(projectDir.toString()) + } else { + out.style(Style.SuccessHeader).println("Build script update available! Run 'gradle updateBuildScript'") + } +} + +static URL availableBuildScriptUrl() { + new URL("https://raw.githubusercontent.com/GTNewHorizons/ExampleMod1.7.10/master/build.gradle") +} + +static URL exampleSettingsGradleUrl() { + new URL("https://raw.githubusercontent.com/GTNewHorizons/ExampleMod1.7.10/master/settings.gradle.example") +} + +static URL exampleGitAttributesUrl() { + new URL("https://raw.githubusercontent.com/GTNewHorizons/ExampleMod1.7.10/master/.gitattributes") +} + + +boolean verifyGitAttributes() { + def gitattributesFile = getFile(".gitattributes") + if (!gitattributesFile.exists()) { + println("Downloading default .gitattributes") + exampleGitAttributesUrl().withInputStream { i -> gitattributesFile.withOutputStream { it << i } } + exec { + workingDir '.' + commandLine 'git', 'add', '--renormalize', '.' + } + return true + } + return false +} + +boolean verifySettingsGradle() { + def settingsFile = getFile("settings.gradle") + if (!settingsFile.exists()) { + println("Downloading default settings.gradle") + exampleSettingsGradleUrl().withInputStream { i -> settingsFile.withOutputStream { it << i } } + return true + } + return false +} + +boolean performBuildScriptUpdate(String projectDir) { + if (isNewBuildScriptVersionAvailable(projectDir)) { + def buildscriptFile = getFile("build.gradle") + availableBuildScriptUrl().withInputStream { i -> buildscriptFile.withOutputStream { it << i } } + out.style(Style.Success).print("Build script updated. Please REIMPORT the project or RESTART your IDE!") + boolean settingsupdated = verifySettingsGradle() + settingsupdated = verifyGitAttributes() || settingsupdated + if (settingsupdated) + throw new GradleException("Settings has been updated, please re-run task.") + return true + } + return false +} + +boolean isNewBuildScriptVersionAvailable(String projectDir) { + Map parameters = ["connectTimeout": 2000, "readTimeout": 2000] + + String currentBuildScript = getFile("build.gradle").getText() + String currentBuildScriptHash = getVersionHash(currentBuildScript) + String availableBuildScript = availableBuildScriptUrl().newInputStream(parameters).getText() + String availableBuildScriptHash = getVersionHash(availableBuildScript) + + boolean isUpToDate = currentBuildScriptHash.empty || availableBuildScriptHash.empty || currentBuildScriptHash == availableBuildScriptHash + return !isUpToDate +} + +static String getVersionHash(String buildScriptContent) { + String versionLine = buildScriptContent.find("^//version: [a-z0-9]*") + if (versionLine != null) { + return versionLine.split(": ").last() + } + return "" +} + +configure(updateBuildScript) { + group = 'forgegradle' + description = 'Updates the build script to the latest version' +} + +// Parameter Deobfuscation + +task deobfParams { + doLast { + + String mcpDir = "$project.gradle.gradleUserHomeDir/caches/minecraft/de/oceanlabs/mcp/mcp_$channel/$mappingsVersion" + String mcpZIP = "$mcpDir/mcp_$channel-$mappingsVersion-${minecraftVersion}.zip" + String paramsCSV = "$mcpDir/params.csv" + + download.run { + src "https://maven.minecraftforge.net/de/oceanlabs/mcp/mcp_$channel/$mappingsVersion-$minecraftVersion/mcp_$channel-$mappingsVersion-${minecraftVersion}.zip" + dest mcpZIP + overwrite false + } + + if (!file(paramsCSV).exists()) { + println("Extracting MCP archive ...") + unzip(mcpZIP, mcpDir) + } + + println("Parsing params.csv ...") + Map params = new HashMap<>() + Files.lines(Paths.get(paramsCSV)).forEach { line -> + String[] cells = line.split(",") + if (cells.length > 2 && cells[0].matches("p_i?\\d+_\\d+_")) { + params.put(cells[0], cells[1]) + } + } + + out.style(Style.Success).println("Modified ${replaceParams(file("$projectDir/src/main/java"), params)} files!") + out.style(Style.Failure).println("Don't forget to verify that the code still works as before!\n It could be broken due to duplicate variables existing now\n or parameters taking priority over other variables.") + } +} + +static int replaceParams(File file, Map params) { + int fileCount = 0 + + if (file.isDirectory()) { + for (File f : file.listFiles()) { + fileCount += replaceParams(f, params) + } + return fileCount + } + println("Visiting ${file.getName()} ...") + try { + String content = new String(Files.readAllBytes(file.toPath())) + int hash = content.hashCode() + params.forEach { key, value -> + content = content.replaceAll(key, value) + } + if (hash != content.hashCode()) { + Files.write(file.toPath(), content.getBytes("UTF-8")) + return 1 + } + } catch (Exception e) { + e.printStackTrace() + } + return 0 +} + +// Credit: bitsnaps (https://gist.github.com/bitsnaps/00947f2dce66f4bbdabc67d7e7b33681) +static unzip(String zipFileName, String outputDir) { + byte[] buffer = new byte[16384] + ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFileName)) + ZipEntry zipEntry = zis.getNextEntry() + while (zipEntry != null) { + File newFile = new File(outputDir + File.separator, zipEntry.name) + if (zipEntry.isDirectory()) { + if (!newFile.isDirectory() && !newFile.mkdirs()) { + throw new IOException("Failed to create directory $newFile") + } + } else { + // fix for Windows-created archives + File parent = newFile.parentFile + if (!parent.isDirectory() && !parent.mkdirs()) { + throw new IOException("Failed to create directory $parent") + } + // write file content + FileOutputStream fos = new FileOutputStream(newFile) + int len = 0 + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len) + } + fos.close() + } + zipEntry = zis.getNextEntry() + } + zis.closeEntry() + zis.close() +} + +configure(deobfParams) { + group = 'forgegradle' + description = 'Rename all obfuscated parameter names inherited from Minecraft classes' +} + +// Dependency Deobfuscation + +def deobf(String sourceURL) { + try { + URL url = new URL(sourceURL) + String fileName = url.getFile() + + //get rid of directories: + int lastSlash = fileName.lastIndexOf("/") + if (lastSlash > 0) { + fileName = fileName.substring(lastSlash + 1) + } + //get rid of extension: + if (fileName.endsWith(".jar") || fileName.endsWith(".litemod")) { + fileName = fileName.substring(0, fileName.lastIndexOf(".")) + } + + String hostName = url.getHost() + if (hostName.startsWith("www.")) { + hostName = hostName.substring(4) + } + List parts = Arrays.asList(hostName.split("\\.")) + Collections.reverse(parts) + hostName = String.join(".", parts) + + return deobf(sourceURL, "$hostName/$fileName") + } catch (Exception e) { + return deobf(sourceURL, "deobf/${sourceURL.hashCode()}") + } +} + +// The method above is to be preferred. Use this method if the filename is not at the end of the URL. +def deobf(String sourceURL, String rawFileName) { + String bon2Version = "2.5.1" + String fileName = URLDecoder.decode(rawFileName, "UTF-8") + String cacheDir = "$project.gradle.gradleUserHomeDir/caches" + String bon2Dir = "$cacheDir/forge_gradle/deobf" + String bon2File = "$bon2Dir/BON2-${bon2Version}.jar" + String obfFile = "$cacheDir/modules-2/files-2.1/${fileName}.jar" + String deobfFile = "$cacheDir/modules-2/files-2.1/${fileName}-deobf.jar" + + if (file(deobfFile).exists()) { + return files(deobfFile) + } + + String mappingsVer + String remoteMappings = project.hasProperty('remoteMappings') ? project.remoteMappings : 'https://raw.githubusercontent.com/MinecraftForge/FML/1.7.10/conf/' + if (remoteMappings) { + String id = "${forgeVersion.split("\\.")[3]}-$minecraftVersion" + String mappingsZIP = "$cacheDir/forge_gradle/maven_downloader/de/oceanlabs/mcp/mcp_snapshot_nodoc/$id/mcp_snapshot_nodoc-${id}.zip" + + zipMappings(mappingsZIP, remoteMappings, bon2Dir) + + mappingsVer = "snapshot_$id" + } else { + mappingsVer = "${channel}_$mappingsVersion" + } + + download.run { + src "http://jenkins.usrv.eu:8081/nexus/content/repositories/releases/com/github/parker8283/BON2/$bon2Version-CUSTOM/BON2-$bon2Version-CUSTOM-all.jar" + dest bon2File + quiet true + overwrite false + } + + download.run { + src sourceURL + dest obfFile + quiet true + overwrite false + } + + exec { + commandLine 'java', '-jar', bon2File, '--inputJar', obfFile, '--outputJar', deobfFile, '--mcVer', minecraftVersion, '--mappingsVer', mappingsVer, '--notch' + workingDir bon2Dir + standardOutput = new FileOutputStream("${deobfFile}.log") + } + + return files(deobfFile) +} + +def zipMappings(String zipPath, String url, String bon2Dir) { + File zipFile = new File(zipPath) + if (zipFile.exists()) { + return + } + + String fieldsCache = "$bon2Dir/data/fields.csv" + String methodsCache = "$bon2Dir/data/methods.csv" + + download.run { + src "${url}fields.csv" + dest fieldsCache + quiet true + } + download.run { + src "${url}methods.csv" + dest methodsCache + quiet true + } + + zipFile.getParentFile().mkdirs() + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile)) + + zos.putNextEntry(new ZipEntry("fields.csv")) + Files.copy(Paths.get(fieldsCache), zos) + zos.closeEntry() + + zos.putNextEntry(new ZipEntry("methods.csv")) + Files.copy(Paths.get(methodsCache), zos) + zos.closeEntry() + + zos.close() +} + +// Helper methods + +def checkPropertyExists(String propertyName) { + if (!project.hasProperty(propertyName)) { + throw new GradleException("This project requires a property \"" + propertyName + "\"! Please add it your \"gradle.properties\". You can find all properties and their description here: https://github.com/GTNewHorizons/ExampleMod1.7.10/blob/main/gradle.properties") + } +} + +def getFile(String relativePath) { + return new File(projectDir, relativePath) +} diff --git a/dependencies.gradle b/dependencies.gradle new file mode 100644 index 0000000..3a0f62b --- /dev/null +++ b/dependencies.gradle @@ -0,0 +1,8 @@ +// Add your dependencies here + +dependencies { + compile 'org.jetbrains:annotations:23.0.0' + + compile("com.github.GTNewHorizons:CodeChickenLib:1.1.5.3:dev") + compile("com.github.GTNewHorizons:NotEnoughItems:2.3.1-GTNH:dev") +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..9e048bb --- /dev/null +++ b/gradle.properties @@ -0,0 +1,84 @@ +modName = Modular UI + +# This is a case-sensitive string to identify your mod. Convention is to use lower case. +modId = modularui + +modGroup = com.gtnewhorizons.modularui + +# WHY is there no version field? +# The build script relies on git to provide a version via tags. It is super easy and will enable you to always know the +# code base or your binary. Check out this tutorial: https://blog.mattclemente.com/2017/10/13/versioning-with-git-tags/ + +# Will update your build.gradle automatically whenever an update is available +autoUpdateBuildScript = false + +minecraftVersion = 1.7.10 +forgeVersion = 10.13.4.1614 + +# Specify a MCP channel and mappings version for dependency deobfuscation and the deobfParams task. +channel = stable +mappingsVersion = 12 + +# Define other MCP mappings for dependency deobfuscation +remoteMappings = https://raw.githubusercontent.com/MinecraftForge/FML/1.7.10/conf/ + +# Select a username for testing your mod with breakpoints. You may leave this empty for a random username each time you +# restart Minecraft in development. Choose this dependent on your mod: +# Do you need consistent player progressing (for example Thaumcraft)? -> Select a name +# Do you need to test how your custom blocks interacts with a player that is not the owner? -> leave name empty +developmentEnvironmentUserName = Developer + +# Define a source file of your project with: +# public static final String VERSION = "GRADLETOKEN_VERSION"; +# The string's content will be replaced with your mod's version when compiled. You should use this to specify your mod's +# version in @Mod([...], version = VERSION, [...]) +# Leave these properties empty to skip individual token replacements +replaceGradleTokenInFile = Tags.java +gradleTokenModId = GRADLETOKEN_MODID +gradleTokenModName = GRADLETOKEN_MODNAME +gradleTokenVersion = GRADLETOKEN_VERSION +gradleTokenGroupName = GRADLETOKEN_GROUPNAME + +# In case your mod provides an API for other mods to implement you may declare its package here. Otherwise, you can +# leave this property empty. +# Example value: apiPackage = api + modGroup = com.myname.mymodid -> com.myname.mymodid.api +apiPackage = + +# Specify the configuration file for Forge's access transformers here. It must be placed into /src/main/resources/META-INF/ +# Example value: mymodid_at.cfg +accessTransformersFile = + +# Provides setup for Mixins if enabled. If you don't know what mixins are: Keep it disabled! +usesMixins = true +# Adds some debug arguments like verbose output and export +usesMixinDebug = false +# Specify the location of your implementation of IMixinConfigPlugin. Leave it empty otherwise. +mixinPlugin = mixinplugin.MixinPlugin +# Specify the package that contains all of your Mixins. You may only place Mixins in this package or the build will fail! +mixinsPackage = mixins +# Specify the core mod entry class if you use a core mod. This class must implement IFMLLoadingPlugin! +# This parameter is for legacy compatibility only +# Example value: coreModClass = asm.FMLPlugin + modGroup = com.myname.mymodid -> com.myname.mymodid.asm.FMLPlugin +coreModClass = +# If your project is only a consolidation of mixins or a core mod and does NOT contain a 'normal' mod ( = some class +# that is annotated with @Mod) you want this to be true. When in doubt: leave it on false! +containsMixinsAndOrCoreModOnly = false + +# Enables Mixins even if this mod doesn't use them, useful if one of the dependencies uses mixins. +forceEnableMixins = false + +# If enabled, you may use 'shadowCompile' for dependencies. They will be integrated in your jar. It is your +# responsibility check the licence and request permission for distribution, if required. +usesShadowedDependencies = false + +# Optional parameter to customize the produced artifacts. Use this to preserver artifact naming when migrating older +# projects. New projects should not use this parameter. +#customArchiveBaseName = + +# Optional parameter to prevent the source code from being published +# noPublishedSources = + +# Uncomment this to disable spotless checks +# This should only be uncommented to keep it easier to sync with upstream/other forks. +# That is, if there is no other active fork/upstream, NEVER change this. +disableSpotless = true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..5c2d1cf Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3ab0b72 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..83f2acf --- /dev/null +++ b/gradlew @@ -0,0 +1,188 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# 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")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +: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% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000..09bbb51 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,2 @@ +before_install: + - ./gradlew setupCIWorkspace \ No newline at end of file diff --git a/repositories.gradle b/repositories.gradle new file mode 100644 index 0000000..c884390 --- /dev/null +++ b/repositories.gradle @@ -0,0 +1,5 @@ +// Add any additional repositories for your dependencies here + +repositories { + +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..93c852a --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +plugins { + id 'com.diffplug.blowdryerSetup' version '1.6.0' +} + +apply plugin: 'com.diffplug.blowdryerSetup' + +blowdryerSetup { + github('GTNewHorizons/ExampleMod1.7.10', 'tag', '0.1.5') + //devLocal '.' // Use this when testing config updates locally +} diff --git a/src/main/java/com/gtnewhorizons/modularui/ClientProxy.java b/src/main/java/com/gtnewhorizons/modularui/ClientProxy.java new file mode 100644 index 0000000..220c68e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/ClientProxy.java @@ -0,0 +1,46 @@ +package com.gtnewhorizons.modularui; + + +import codechicken.lib.math.MathHelper; +import com.gtnewhorizons.modularui.common.internal.JsonLoader; +import com.gtnewhorizons.modularui.common.internal.wrapper.ModularGui; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.client.resources.SimpleReloadableResourceManager; +import net.minecraftforge.client.event.GuiScreenEvent; +import org.lwjgl.input.Mouse; + +@SuppressWarnings("unused") +@SideOnly(Side.CLIENT) +public class ClientProxy extends CommonProxy { + + @Override + public void preInit(FMLPreInitializationEvent event) { + super.preInit(event); + } + + public void postInit() { + super.postInit(); + ((SimpleReloadableResourceManager) Minecraft.getMinecraft().getResourceManager()).registerReloadListener(this::onReload); + } + + public void onReload(IResourceManager manager) { + ModularUI.logger.info("Reloading GUIs"); + JsonLoader.loadJson(); + } + + @SubscribeEvent + public void mouseScreenInput(GuiScreenEvent event) { + if (event.gui instanceof ModularGui) { + int w = Mouse.getEventDWheel(); + int wheel = (int)MathHelper.clip(w, -1, 1); + if (wheel != 0) { + ((ModularGui) event.gui).mouseScroll(wheel); + } + } + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/CommonProxy.java b/src/main/java/com/gtnewhorizons/modularui/CommonProxy.java new file mode 100644 index 0000000..72a6084 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/CommonProxy.java @@ -0,0 +1,56 @@ +package com.gtnewhorizons.modularui; + +import com.gtnewhorizons.modularui.config.Config; +import com.gtnewhorizons.modularui.test.TestBlock; +import com.gtnewhorizons.modularui.test.TestTile; +import cpw.mods.fml.client.event.ConfigChangedEvent; +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.registry.GameRegistry; +import net.minecraft.block.Block; +import net.minecraft.block.material.Material; +import net.minecraft.creativetab.CreativeTabs; +import net.minecraftforge.common.MinecraftForge; + +public class CommonProxy { + + public static Block testBlock; + + public void preInit(FMLPreInitializationEvent event) { + + testBlock = new TestBlock(Material.rock) + .setBlockName("testBlock") + .setCreativeTab(CreativeTabs.tabBlock) + .setBlockTextureName("stone"); + GameRegistry.registerBlock(testBlock, "testBlock"); + GameRegistry.registerTileEntity(TestTile.class, "TestTileEntity"); + + Config.init(event.getSuggestedConfigurationFile()); + + FMLCommonHandler.instance().bus().register(this); + MinecraftForge.EVENT_BUS.register(this); + } + + public void postInit() { + } + +// @SubscribeEvent +// public static void registerBlocks(RegistryEvent.Register event) { +// IForgeRegistry registry = event.getRegistry(); +// registry.register(testBlock); +// } +// +// @SubscribeEvent +// public static void registerItems(RegistryEvent.Register event) { +// IForgeRegistry registry = event.getRegistry(); +// registry.register(testItemBlock); +// } + + @SubscribeEvent + public void onConfigChange(ConfigChangedEvent.OnConfigChangedEvent event) { + if (event.modID.equals(ModularUI.MODID)) { + Config.syncConfig(); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/ModularUI.java b/src/main/java/com/gtnewhorizons/modularui/ModularUI.java new file mode 100644 index 0000000..a696f26 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/ModularUI.java @@ -0,0 +1,78 @@ +package com.gtnewhorizons.modularui; + +import com.gtnewhorizons.modularui.common.internal.JsonLoader; +import com.gtnewhorizons.modularui.common.internal.network.NetworkHandler; +import com.gtnewhorizons.modularui.common.internal.wrapper.ModularGui; +import com.gtnewhorizons.modularui.common.internal.wrapper.ModularUIContainer; +import com.gtnewhorizons.modularui.api.UIInfos; +import com.gtnewhorizons.modularui.api.screen.ModularUIContext; +import com.gtnewhorizons.modularui.api.screen.ModularWindow; +import com.gtnewhorizons.modularui.api.screen.UIBuildContext; +import com.gtnewhorizons.modularui.common.widget.WidgetJsonRegistry; +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.Mod; +import cpw.mods.fml.common.SidedProxy; +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.entity.player.EntityPlayer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.function.Function; + +@Mod( + modid = ModularUI.MODID, + version = Tags.VERSION, + name = Tags.MODNAME, + acceptedMinecraftVersions = "[1.7.10]", + dependencies = ModularUI.DEPENDENCIES, + guiFactory = ModularUI.GUI_FACTORY +) +public class ModularUI { + + public static final String MODID = "modularui"; + public static final String DEPENDENCIES = "required-after:CodeChickenLib; required-after:NotEnoughItems;"; + public static final String GUI_FACTORY = Tags.GROUPNAME + ".config.GuiFactory"; + + public static final Logger logger = LogManager.getLogger(Tags.MODID); + + @Mod.Instance(ModularUI.MODID) + public static ModularUI INSTANCE; + + @SidedProxy(modId = MODID, clientSide = Tags.GROUPNAME + ".ClientProxy", serverSide = Tags.GROUPNAME + ".CommonProxy") + public static CommonProxy proxy; + + @Mod.EventHandler + public void preInit(FMLPreInitializationEvent event) { + proxy.preInit(event); + NetworkHandler.init(); + UIInfos.init(); + WidgetJsonRegistry.init(); + } + + @Mod.EventHandler + public void init(FMLInitializationEvent event) { + if (FMLCommonHandler.instance().getSide() == Side.SERVER) { + JsonLoader.loadJson(); + } + } + + @Mod.EventHandler + public void onPostInit(FMLPostInitializationEvent event) { + proxy.postInit(); + } + + public static ModularUIContainer createContainer(EntityPlayer player, Function windowCreator) { + UIBuildContext buildContext = new UIBuildContext(player); + ModularWindow window = windowCreator.apply(buildContext); + return new ModularUIContainer(new ModularUIContext(buildContext), window); + } + + @SideOnly(Side.CLIENT) + public static ModularGui createGuiScreen(EntityPlayer player, Function windowCreator) { + return new ModularGui(createContainer(player, windowCreator)); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/Tags.java b/src/main/java/com/gtnewhorizons/modularui/Tags.java new file mode 100644 index 0000000..07daa8d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/Tags.java @@ -0,0 +1,12 @@ +package com.gtnewhorizons.modularui; + +// Use this class for Strings only. Do not import any classes here. It will lead to issues with Mixins if in use! + +public class Tags { + + // GRADLETOKEN_* will be replaced by your configuration values at build time + public static final String MODID = "GRADLETOKEN_MODID"; + public static final String MODNAME = "GRADLETOKEN_MODNAME"; + public static final String VERSION = "GRADLETOKEN_VERSION"; + public static final String GROUPNAME = "GRADLETOKEN_GROUPNAME"; +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/GlStateManager.java b/src/main/java/com/gtnewhorizons/modularui/api/GlStateManager.java new file mode 100644 index 0000000..3d96de6 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/GlStateManager.java @@ -0,0 +1,1278 @@ +package com.gtnewhorizons.modularui.api; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.renderer.OpenGlHelper; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL14; +import org.lwjgl.util.vector.Quaternion; + +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +/** + * Copied from forge 1.12.2-14.23.5.2847 net.minecraft.client.renderer.GlStateManager + */ +@SideOnly(Side.CLIENT) +public class GlStateManager { + private static final FloatBuffer BUF_FLOAT_16 = BufferUtils.createFloatBuffer(16); + private static final FloatBuffer BUF_FLOAT_4 = BufferUtils.createFloatBuffer(4); + private static final AlphaState alphaState = new AlphaState(); + private static final BooleanState lightingState = new BooleanState(2896); + private static final BooleanState[] lightState = new BooleanState[8]; + private static final ColorMaterialState colorMaterialState; + private static final BlendState blendState; + private static final DepthState depthState; + private static final FogState fogState; + private static final CullState cullState; + private static final PolygonOffsetState polygonOffsetState; + private static final ColorLogicState colorLogicState; + private static final TexGenState texGenState; + private static final ClearState clearState; + private static final StencilState stencilState; + private static final BooleanState normalizeState; + private static int activeTextureUnit; + private static final TextureState[] textureState; + private static int activeShadeModel; + private static final BooleanState rescaleNormalState; + private static final ColorMask colorMaskState; + private static final Color colorState; + + /** + * Do not use (see MinecraftForge issue #1637) + */ + public static void pushAttrib() + { + GL11.glPushAttrib(8256); + } + + /** + * Do not use (see MinecraftForge issue #1637) + */ + public static void popAttrib() + { + GL11.glPopAttrib(); + } + + public static void disableAlpha() + { + alphaState.alphaTest.setDisabled(); + } + + public static void enableAlpha() + { + alphaState.alphaTest.setEnabled(); + } + + public static void alphaFunc(int func, float ref) + { + if (func != alphaState.func || ref != alphaState.ref) + { + alphaState.func = func; + alphaState.ref = ref; + GL11.glAlphaFunc(func, ref); + } + } + + public static void enableLighting() + { + lightingState.setEnabled(); + } + + public static void disableLighting() + { + lightingState.setDisabled(); + } + + public static void enableLight(int light) + { + lightState[light].setEnabled(); + } + + public static void disableLight(int light) + { + lightState[light].setDisabled(); + } + + public static void enableColorMaterial() + { + colorMaterialState.colorMaterial.setEnabled(); + } + + public static void disableColorMaterial() + { + colorMaterialState.colorMaterial.setDisabled(); + } + + public static void colorMaterial(int face, int mode) + { + if (face != colorMaterialState.face || mode != colorMaterialState.mode) + { + colorMaterialState.face = face; + colorMaterialState.mode = mode; + GL11.glColorMaterial(face, mode); + } + } + + public static void glLight(int light, int pname, FloatBuffer params) + { + GL11.glLight(light, pname, params); + } + + public static void glLightModel(int pname, FloatBuffer params) + { + GL11.glLightModel(pname, params); + } + + public static void glNormal3f(float nx, float ny, float nz) + { + GL11.glNormal3f(nx, ny, nz); + } + + public static void disableDepth() + { + depthState.depthTest.setDisabled(); + } + + public static void enableDepth() + { + depthState.depthTest.setEnabled(); + } + + public static void depthFunc(int depthFunc) + { + if (depthFunc != depthState.depthFunc) + { + depthState.depthFunc = depthFunc; + GL11.glDepthFunc(depthFunc); + } + } + + public static void depthMask(boolean flagIn) + { + if (flagIn != depthState.maskEnabled) + { + depthState.maskEnabled = flagIn; + GL11.glDepthMask(flagIn); + } + } + + public static void disableBlend() + { + blendState.blend.setDisabled(); + } + + public static void enableBlend() + { + blendState.blend.setEnabled(); + } + + public static void blendFunc(SourceFactor srcFactor, DestFactor dstFactor) + { + blendFunc(srcFactor.factor, dstFactor.factor); + } + + public static void blendFunc(int srcFactor, int dstFactor) + { + if (srcFactor != blendState.srcFactor || dstFactor != blendState.dstFactor) + { + blendState.srcFactor = srcFactor; + blendState.dstFactor = dstFactor; + GL11.glBlendFunc(srcFactor, dstFactor); + } + } + + public static void tryBlendFuncSeparate(SourceFactor srcFactor, DestFactor dstFactor, SourceFactor srcFactorAlpha, DestFactor dstFactorAlpha) + { + tryBlendFuncSeparate(srcFactor.factor, dstFactor.factor, srcFactorAlpha.factor, dstFactorAlpha.factor); + } + + public static void tryBlendFuncSeparate(int srcFactor, int dstFactor, int srcFactorAlpha, int dstFactorAlpha) + { + if (srcFactor != blendState.srcFactor || dstFactor != blendState.dstFactor || srcFactorAlpha != blendState.srcFactorAlpha || dstFactorAlpha != blendState.dstFactorAlpha) + { + blendState.srcFactor = srcFactor; + blendState.dstFactor = dstFactor; + blendState.srcFactorAlpha = srcFactorAlpha; + blendState.dstFactorAlpha = dstFactorAlpha; + OpenGlHelper.glBlendFunc(srcFactor, dstFactor, srcFactorAlpha, dstFactorAlpha); + } + } + + public static void glBlendEquation(int blendEquation) + { + GL14.glBlendEquation(blendEquation); + } + + public static void enableOutlineMode(int color) + { + BUF_FLOAT_4.put(0, (float)(color >> 16 & 255) / 255.0F); + BUF_FLOAT_4.put(1, (float)(color >> 8 & 255) / 255.0F); + BUF_FLOAT_4.put(2, (float)(color >> 0 & 255) / 255.0F); + BUF_FLOAT_4.put(3, (float)(color >> 24 & 255) / 255.0F); + glTexEnv(8960, 8705, BUF_FLOAT_4); + glTexEnvi(8960, 8704, 34160); + glTexEnvi(8960, 34161, 7681); + glTexEnvi(8960, 34176, 34166); + glTexEnvi(8960, 34192, 768); + glTexEnvi(8960, 34162, 7681); + glTexEnvi(8960, 34184, 5890); + glTexEnvi(8960, 34200, 770); + } + + public static void disableOutlineMode() + { + glTexEnvi(8960, 8704, 8448); + glTexEnvi(8960, 34161, 8448); + glTexEnvi(8960, 34162, 8448); + glTexEnvi(8960, 34176, 5890); + glTexEnvi(8960, 34184, 5890); + glTexEnvi(8960, 34192, 768); + glTexEnvi(8960, 34200, 770); + } + + public static void enableFog() + { + fogState.fog.setEnabled(); + } + + public static void disableFog() + { + fogState.fog.setDisabled(); + } + + public static void setFog(FogMode fogMode) + { + setFog(fogMode.capabilityId); + } + + private static void setFog(int param) + { + if (param != fogState.mode) + { + fogState.mode = param; + GL11.glFogi(GL11.GL_FOG_MODE, param); + } + } + + public static void setFogDensity(float param) + { + if (param != fogState.density) + { + fogState.density = param; + GL11.glFogf(GL11.GL_FOG_DENSITY, param); + } + } + + public static void setFogStart(float param) + { + if (param != fogState.start) + { + fogState.start = param; + GL11.glFogf(GL11.GL_FOG_START, param); + } + } + + public static void setFogEnd(float param) + { + if (param != fogState.end) + { + fogState.end = param; + GL11.glFogf(GL11.GL_FOG_END, param); + } + } + + public static void glFog(int pname, FloatBuffer param) + { + GL11.glFog(pname, param); + } + + public static void glFogi(int pname, int param) + { + GL11.glFogi(pname, param); + } + + public static void enableCull() + { + cullState.cullFace.setEnabled(); + } + + public static void disableCull() + { + cullState.cullFace.setDisabled(); + } + + public static void cullFace(CullFace cullFace) + { + cullFace(cullFace.mode); + } + + private static void cullFace(int mode) + { + if (mode != cullState.mode) + { + cullState.mode = mode; + GL11.glCullFace(mode); + } + } + + public static void glPolygonMode(int face, int mode) + { + GL11.glPolygonMode(face, mode); + } + + public static void enablePolygonOffset() + { + polygonOffsetState.polygonOffsetFill.setEnabled(); + } + + public static void disablePolygonOffset() + { + polygonOffsetState.polygonOffsetFill.setDisabled(); + } + + public static void doPolygonOffset(float factor, float units) + { + if (factor != polygonOffsetState.factor || units != polygonOffsetState.units) + { + polygonOffsetState.factor = factor; + polygonOffsetState.units = units; + GL11.glPolygonOffset(factor, units); + } + } + + public static void enableColorLogic() + { + colorLogicState.colorLogicOp.setEnabled(); + } + + public static void disableColorLogic() + { + colorLogicState.colorLogicOp.setDisabled(); + } + + public static void colorLogicOp(LogicOp logicOperation) + { + colorLogicOp(logicOperation.opcode); + } + + public static void colorLogicOp(int opcode) + { + if (opcode != colorLogicState.opcode) + { + colorLogicState.opcode = opcode; + GL11.glLogicOp(opcode); + } + } + + public static void enableTexGenCoord(TexGen texGen) + { + texGenCoord(texGen).textureGen.setEnabled(); + } + + public static void disableTexGenCoord(TexGen texGen) + { + texGenCoord(texGen).textureGen.setDisabled(); + } + + public static void texGen(TexGen texGen, int param) + { + TexGenCoord glstatemanager$texgencoord = texGenCoord(texGen); + + if (param != glstatemanager$texgencoord.param) + { + glstatemanager$texgencoord.param = param; + GL11.glTexGeni(glstatemanager$texgencoord.coord, GL11.GL_TEXTURE_GEN_MODE, param); + } + } + + public static void texGen(TexGen texGen, int pname, FloatBuffer params) + { + GL11.glTexGen(texGenCoord(texGen).coord, pname, params); + } + + private static TexGenCoord texGenCoord(TexGen texGen) + { + switch (texGen) + { + case S: + return texGenState.s; + case T: + return texGenState.t; + case R: + return texGenState.r; + case Q: + return texGenState.q; + default: + return texGenState.s; + } + } + + public static void setActiveTexture(int texture) + { + if (activeTextureUnit != texture - OpenGlHelper.defaultTexUnit) + { + activeTextureUnit = texture - OpenGlHelper.defaultTexUnit; + OpenGlHelper.setActiveTexture(texture); + } + } + + public static void enableTexture2D() + { + textureState[activeTextureUnit].texture2DState.setEnabled(); + } + + public static void disableTexture2D() + { + textureState[activeTextureUnit].texture2DState.setDisabled(); + } + + public static void glTexEnv(int target, int parameterName, FloatBuffer parameters) + { + GL11.glTexEnv(target, parameterName, parameters); + } + + public static void glTexEnvi(int target, int parameterName, int parameter) + { + GL11.glTexEnvi(target, parameterName, parameter); + } + + public static void glTexEnvf(int target, int parameterName, float parameter) + { + GL11.glTexEnvf(target, parameterName, parameter); + } + + public static void glTexParameterf(int target, int parameterName, float parameter) + { + GL11.glTexParameterf(target, parameterName, parameter); + } + + public static void glTexParameteri(int target, int parameterName, int parameter) + { + GL11.glTexParameteri(target, parameterName, parameter); + } + + public static int glGetTexLevelParameteri(int target, int level, int parameterName) + { + return GL11.glGetTexLevelParameteri(target, level, parameterName); + } + + public static int generateTexture() + { + return GL11.glGenTextures(); + } + + public static void deleteTexture(int texture) + { + GL11.glDeleteTextures(texture); + + for (TextureState glstatemanager$texturestate : textureState) + { + if (glstatemanager$texturestate.textureName == texture) + { + glstatemanager$texturestate.textureName = -1; + } + } + } + + public static void bindTexture(int texture) + { + if (texture != textureState[activeTextureUnit].textureName) + { + textureState[activeTextureUnit].textureName = texture; + GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture); + } + } + + public static void glTexImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, @Nullable IntBuffer pixels) + { + GL11.glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixels); + } + + public static void glTexSubImage2D(int target, int level, int xOffset, int yOffset, int width, int height, int format, int type, IntBuffer pixels) + { + GL11.glTexSubImage2D(target, level, xOffset, yOffset, width, height, format, type, pixels); + } + + public static void glCopyTexSubImage2D(int target, int level, int xOffset, int yOffset, int x, int y, int width, int height) + { + GL11.glCopyTexSubImage2D(target, level, xOffset, yOffset, x, y, width, height); + } + + public static void glGetTexImage(int target, int level, int format, int type, IntBuffer pixels) + { + GL11.glGetTexImage(target, level, format, type, pixels); + } + + public static void enableNormalize() + { + normalizeState.setEnabled(); + } + + public static void disableNormalize() + { + normalizeState.setDisabled(); + } + + public static void shadeModel(int mode) + { + if (mode != activeShadeModel) + { + activeShadeModel = mode; + GL11.glShadeModel(mode); + } + } + + public static void enableRescaleNormal() + { + rescaleNormalState.setEnabled(); + } + + public static void disableRescaleNormal() + { + rescaleNormalState.setDisabled(); + } + + public static void viewport(int x, int y, int width, int height) + { + GL11.glViewport(x, y, width, height); + } + + public static void colorMask(boolean red, boolean green, boolean blue, boolean alpha) + { + if (red != colorMaskState.red || green != colorMaskState.green || blue != colorMaskState.blue || alpha != colorMaskState.alpha) + { + colorMaskState.red = red; + colorMaskState.green = green; + colorMaskState.blue = blue; + colorMaskState.alpha = alpha; + GL11.glColorMask(red, green, blue, alpha); + } + } + + public static void clearDepth(double depth) + { + if (depth != clearState.depth) + { + clearState.depth = depth; + GL11.glClearDepth(depth); + } + } + + public static void clearColor(float red, float green, float blue, float alpha) + { + if (red != clearState.color.red || green != clearState.color.green || blue != clearState.color.blue || alpha != clearState.color.alpha) + { + clearState.color.red = red; + clearState.color.green = green; + clearState.color.blue = blue; + clearState.color.alpha = alpha; + GL11.glClearColor(red, green, blue, alpha); + } + } + + public static void clear(int mask) + { + GL11.glClear(mask); + } + + public static void matrixMode(int mode) + { + GL11.glMatrixMode(mode); + } + + public static void loadIdentity() + { + GL11.glLoadIdentity(); + } + + public static void pushMatrix() + { + GL11.glPushMatrix(); + } + + public static void popMatrix() + { + GL11.glPopMatrix(); + } + + public static void getFloat(int pname, FloatBuffer params) + { + GL11.glGetFloat(pname, params); + } + + public static void ortho(double left, double right, double bottom, double top, double zNear, double zFar) + { + GL11.glOrtho(left, right, bottom, top, zNear, zFar); + } + + public static void rotate(float angle, float x, float y, float z) + { + GL11.glRotatef(angle, x, y, z); + } + + public static void scale(float x, float y, float z) + { + GL11.glScalef(x, y, z); + } + + public static void scale(double x, double y, double z) + { + GL11.glScaled(x, y, z); + } + + public static void translate(float x, float y, float z) + { + GL11.glTranslatef(x, y, z); + } + + public static void translate(double x, double y, double z) + { + GL11.glTranslated(x, y, z); + } + + public static void multMatrix(FloatBuffer matrix) + { + GL11.glMultMatrix(matrix); + } + + public static void rotate(Quaternion quaternionIn) + { + multMatrix(quatToGlMatrix(BUF_FLOAT_16, quaternionIn)); + } + + public static FloatBuffer quatToGlMatrix(FloatBuffer buffer, Quaternion quaternionIn) + { + buffer.clear(); + float f = quaternionIn.x * quaternionIn.x; + float f1 = quaternionIn.x * quaternionIn.y; + float f2 = quaternionIn.x * quaternionIn.z; + float f3 = quaternionIn.x * quaternionIn.w; + float f4 = quaternionIn.y * quaternionIn.y; + float f5 = quaternionIn.y * quaternionIn.z; + float f6 = quaternionIn.y * quaternionIn.w; + float f7 = quaternionIn.z * quaternionIn.z; + float f8 = quaternionIn.z * quaternionIn.w; + buffer.put(1.0F - 2.0F * (f4 + f7)); + buffer.put(2.0F * (f1 + f8)); + buffer.put(2.0F * (f2 - f6)); + buffer.put(0.0F); + buffer.put(2.0F * (f1 - f8)); + buffer.put(1.0F - 2.0F * (f + f7)); + buffer.put(2.0F * (f5 + f3)); + buffer.put(0.0F); + buffer.put(2.0F * (f2 + f6)); + buffer.put(2.0F * (f5 - f3)); + buffer.put(1.0F - 2.0F * (f + f4)); + buffer.put(0.0F); + buffer.put(0.0F); + buffer.put(0.0F); + buffer.put(0.0F); + buffer.put(1.0F); + buffer.rewind(); + return buffer; + } + + public static void color(float colorRed, float colorGreen, float colorBlue, float colorAlpha) + { + if (colorRed != colorState.red || colorGreen != colorState.green || colorBlue != colorState.blue || colorAlpha != colorState.alpha) + { + colorState.red = colorRed; + colorState.green = colorGreen; + colorState.blue = colorBlue; + colorState.alpha = colorAlpha; + GL11.glColor4f(colorRed, colorGreen, colorBlue, colorAlpha); + } + } + + public static void color(float colorRed, float colorGreen, float colorBlue) + { + color(colorRed, colorGreen, colorBlue, 1.0F); + } + + public static void glTexCoord2f(float sCoord, float tCoord) + { + GL11.glTexCoord2f(sCoord, tCoord); + } + + public static void glVertex3f(float x, float y, float z) + { + GL11.glVertex3f(x, y, z); + } + + public static void resetColor() + { + colorState.red = -1.0F; + colorState.green = -1.0F; + colorState.blue = -1.0F; + colorState.alpha = -1.0F; + } + + public static void glNormalPointer(int type, int stride, ByteBuffer buffer) + { + GL11.glNormalPointer(type, stride, buffer); + } + + public static void glTexCoordPointer(int size, int type, int stride, int buffer_offset) + { + GL11.glTexCoordPointer(size, type, stride, (long)buffer_offset); + } + + public static void glTexCoordPointer(int size, int type, int stride, ByteBuffer buffer) + { + GL11.glTexCoordPointer(size, type, stride, buffer); + } + + public static void glVertexPointer(int size, int type, int stride, int buffer_offset) + { + GL11.glVertexPointer(size, type, stride, (long)buffer_offset); + } + + public static void glVertexPointer(int size, int type, int stride, ByteBuffer buffer) + { + GL11.glVertexPointer(size, type, stride, buffer); + } + + public static void glColorPointer(int size, int type, int stride, int buffer_offset) + { + GL11.glColorPointer(size, type, stride, (long)buffer_offset); + } + + public static void glColorPointer(int size, int type, int stride, ByteBuffer buffer) + { + GL11.glColorPointer(size, type, stride, buffer); + } + + public static void glDisableClientState(int cap) + { + GL11.glDisableClientState(cap); + } + + public static void glEnableClientState(int cap) + { + GL11.glEnableClientState(cap); + } + + public static void glBegin(int mode) + { + GL11.glBegin(mode); + } + + public static void glEnd() + { + GL11.glEnd(); + } + + public static void glDrawArrays(int mode, int first, int count) + { + GL11.glDrawArrays(mode, first, count); + } + + public static void glLineWidth(float width) + { + GL11.glLineWidth(width); + } + + public static void callList(int list) + { + GL11.glCallList(list); + } + + public static void glDeleteLists(int list, int range) + { + GL11.glDeleteLists(list, range); + } + + public static void glNewList(int list, int mode) + { + GL11.glNewList(list, mode); + } + + public static void glEndList() + { + GL11.glEndList(); + } + + public static int glGenLists(int range) + { + return GL11.glGenLists(range); + } + + public static void glPixelStorei(int parameterName, int param) + { + GL11.glPixelStorei(parameterName, param); + } + + public static void glReadPixels(int x, int y, int width, int height, int format, int type, IntBuffer pixels) + { + GL11.glReadPixels(x, y, width, height, format, type, pixels); + } + + public static int glGetError() + { + return GL11.glGetError(); + } + + public static String glGetString(int name) + { + return GL11.glGetString(name); + } + + public static void glGetInteger(int parameterName, IntBuffer parameters) + { + GL11.glGetInteger(parameterName, parameters); + } + + public static int glGetInteger(int parameterName) + { + return GL11.glGetInteger(parameterName); + } + + static + { + for (int i = 0; i < 8; ++i) + { + lightState[i] = new BooleanState(16384 + i); + } + + colorMaterialState = new ColorMaterialState(); + blendState = new BlendState(); + depthState = new DepthState(); + fogState = new FogState(); + cullState = new CullState(); + polygonOffsetState = new PolygonOffsetState(); + colorLogicState = new ColorLogicState(); + texGenState = new TexGenState(); + clearState = new ClearState(); + stencilState = new StencilState(); + normalizeState = new BooleanState(2977); + textureState = new TextureState[8]; + + for (int j = 0; j < 8; ++j) + { + textureState[j] = new TextureState(); + } + + activeShadeModel = 7425; + rescaleNormalState = new BooleanState(32826); + colorMaskState = new ColorMask(); + colorState = new Color(); + } + + @SideOnly(Side.CLIENT) + static class AlphaState + { + public BooleanState alphaTest; + public int func; + public float ref; + + private AlphaState() + { + this.alphaTest = new BooleanState(3008); + this.func = 519; + this.ref = -1.0F; + } + } + + @SideOnly(Side.CLIENT) + static class BlendState + { + public BooleanState blend; + public int srcFactor; + public int dstFactor; + public int srcFactorAlpha; + public int dstFactorAlpha; + + private BlendState() + { + this.blend = new BooleanState(3042); + this.srcFactor = 1; + this.dstFactor = 0; + this.srcFactorAlpha = 1; + this.dstFactorAlpha = 0; + } + } + + @SideOnly(Side.CLIENT) + static class BooleanState + { + private final int capability; + private boolean currentState; + + public BooleanState(int capabilityIn) + { + this.capability = capabilityIn; + } + + public void setDisabled() + { + this.setState(false); + } + + public void setEnabled() + { + this.setState(true); + } + + public void setState(boolean state) + { + if (state != this.currentState) + { + this.currentState = state; + + if (state) + { + GL11.glEnable(this.capability); + } + else + { + GL11.glDisable(this.capability); + } + } + } + } + + @SideOnly(Side.CLIENT) + static class ClearState + { + public double depth; + public Color color; + + private ClearState() + { + this.depth = 1.0D; + this.color = new Color(0.0F, 0.0F, 0.0F, 0.0F); + } + } + + @SideOnly(Side.CLIENT) + static class Color + { + public float red; + public float green; + public float blue; + public float alpha; + + public Color() + { + this(1.0F, 1.0F, 1.0F, 1.0F); + } + + public Color(float redIn, float greenIn, float blueIn, float alphaIn) + { + this.red = 1.0F; + this.green = 1.0F; + this.blue = 1.0F; + this.alpha = 1.0F; + this.red = redIn; + this.green = greenIn; + this.blue = blueIn; + this.alpha = alphaIn; + } + } + + @SideOnly(Side.CLIENT) + static class ColorLogicState + { + public BooleanState colorLogicOp; + public int opcode; + + private ColorLogicState() + { + this.colorLogicOp = new BooleanState(3058); + this.opcode = 5379; + } + } + + @SideOnly(Side.CLIENT) + static class ColorMask + { + public boolean red; + public boolean green; + public boolean blue; + public boolean alpha; + + private ColorMask() + { + this.red = true; + this.green = true; + this.blue = true; + this.alpha = true; + } + } + + @SideOnly(Side.CLIENT) + static class ColorMaterialState + { + public BooleanState colorMaterial; + public int face; + public int mode; + + private ColorMaterialState() + { + this.colorMaterial = new BooleanState(2903); + this.face = 1032; + this.mode = 5634; + } + } + + @SideOnly(Side.CLIENT) + public static enum CullFace + { + FRONT(1028), + BACK(1029), + FRONT_AND_BACK(1032); + + public final int mode; + + private CullFace(int modeIn) + { + this.mode = modeIn; + } + } + + @SideOnly(Side.CLIENT) + static class CullState + { + public BooleanState cullFace; + public int mode; + + private CullState() + { + this.cullFace = new BooleanState(2884); + this.mode = 1029; + } + } + + @SideOnly(Side.CLIENT) + static class DepthState + { + public BooleanState depthTest; + public boolean maskEnabled; + public int depthFunc; + + private DepthState() + { + this.depthTest = new BooleanState(2929); + this.maskEnabled = true; + this.depthFunc = 513; + } + } + + @SideOnly(Side.CLIENT) + public static enum DestFactor + { + CONSTANT_ALPHA(32771), + CONSTANT_COLOR(32769), + DST_ALPHA(772), + DST_COLOR(774), + ONE(1), + ONE_MINUS_CONSTANT_ALPHA(32772), + ONE_MINUS_CONSTANT_COLOR(32770), + ONE_MINUS_DST_ALPHA(773), + ONE_MINUS_DST_COLOR(775), + ONE_MINUS_SRC_ALPHA(771), + ONE_MINUS_SRC_COLOR(769), + SRC_ALPHA(770), + SRC_COLOR(768), + ZERO(0); + + public final int factor; + + private DestFactor(int factorIn) + { + this.factor = factorIn; + } + } + + @SideOnly(Side.CLIENT) + public static enum FogMode + { + LINEAR(9729), + EXP(2048), + EXP2(2049); + + /** The capability ID of this {@link FogMode} */ + public final int capabilityId; + + private FogMode(int capabilityIn) + { + this.capabilityId = capabilityIn; + } + } + + @SideOnly(Side.CLIENT) + static class FogState + { + public BooleanState fog; + public int mode; + public float density; + public float start; + public float end; + + private FogState() + { + this.fog = new BooleanState(2912); + this.mode = 2048; + this.density = 1.0F; + this.end = 1.0F; + } + } + + @SideOnly(Side.CLIENT) + public static enum LogicOp + { + AND(5377), + AND_INVERTED(5380), + AND_REVERSE(5378), + CLEAR(5376), + COPY(5379), + COPY_INVERTED(5388), + EQUIV(5385), + INVERT(5386), + NAND(5390), + NOOP(5381), + NOR(5384), + OR(5383), + OR_INVERTED(5389), + OR_REVERSE(5387), + SET(5391), + XOR(5382); + + public final int opcode; + + private LogicOp(int opcodeIn) + { + this.opcode = opcodeIn; + } + } + + @SideOnly(Side.CLIENT) + static class PolygonOffsetState + { + public BooleanState polygonOffsetFill; + public BooleanState polygonOffsetLine; + public float factor; + public float units; + + private PolygonOffsetState() + { + this.polygonOffsetFill = new BooleanState(32823); + this.polygonOffsetLine = new BooleanState(10754); + } + } + + @SideOnly(Side.CLIENT) + public static enum SourceFactor + { + CONSTANT_ALPHA(32771), + CONSTANT_COLOR(32769), + DST_ALPHA(772), + DST_COLOR(774), + ONE(1), + ONE_MINUS_CONSTANT_ALPHA(32772), + ONE_MINUS_CONSTANT_COLOR(32770), + ONE_MINUS_DST_ALPHA(773), + ONE_MINUS_DST_COLOR(775), + ONE_MINUS_SRC_ALPHA(771), + ONE_MINUS_SRC_COLOR(769), + SRC_ALPHA(770), + SRC_ALPHA_SATURATE(776), + SRC_COLOR(768), + ZERO(0); + + public final int factor; + + private SourceFactor(int factorIn) + { + this.factor = factorIn; + } + } + + @SideOnly(Side.CLIENT) + static class StencilFunc + { + public int func; + public int mask; + + private StencilFunc() + { + this.func = 519; + this.mask = -1; + } + } + + @SideOnly(Side.CLIENT) + static class StencilState + { + public StencilFunc func; + public int mask; + public int fail; + public int zfail; + public int zpass; + + private StencilState() + { + this.func = new StencilFunc(); + this.mask = -1; + this.fail = 7680; + this.zfail = 7680; + this.zpass = 7680; + } + } + + @SideOnly(Side.CLIENT) + public static enum TexGen + { + S, + T, + R, + Q; + } + + @SideOnly(Side.CLIENT) + static class TexGenCoord + { + public BooleanState textureGen; + public int coord; + public int param = -1; + + public TexGenCoord(int coordIn, int capabilityIn) + { + this.coord = coordIn; + this.textureGen = new BooleanState(capabilityIn); + } + } + + @SideOnly(Side.CLIENT) + static class TexGenState + { + public TexGenCoord s; + public TexGenCoord t; + public TexGenCoord r; + public TexGenCoord q; + + private TexGenState() + { + this.s = new TexGenCoord(8192, 3168); + this.t = new TexGenCoord(8193, 3169); + this.r = new TexGenCoord(8194, 3170); + this.q = new TexGenCoord(8195, 3171); + } + } + + @SideOnly(Side.CLIENT) + static class TextureState + { + public BooleanState texture2DState; + public int textureName; + + private TextureState() + { + this.texture2DState = new BooleanState(3553); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/KeyBindAPI.java b/src/main/java/com/gtnewhorizons/modularui/api/KeyBindAPI.java new file mode 100644 index 0000000..5c0c978 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/KeyBindAPI.java @@ -0,0 +1,60 @@ +package com.gtnewhorizons.modularui.api; + +import net.minecraft.client.settings.KeyBinding; + +import java.util.*; + +public class KeyBindAPI { + + private static final Set forceCheckKey = new HashSet<>(); + private static final Map> compatibiliyMap = new HashMap<>(); + + /** + * By default key binds can only used in specific GUIs. This forces the key bond to trigger regardless of that restriction. + * + * @param keyBinding key bind to force always to trigger + */ + public static void forceCheckKeyBind(KeyBinding keyBinding) { + if (keyBinding == null) { + throw new NullPointerException(); + } + forceCheckKey.add(keyBinding); + } + + /** + * Returns if the key bind should be forced to trigger + * + * @param keyBinding key bind to check + * @return if the key bind should be forced to trigger + */ + public static boolean doForceCheckKeyBind(KeyBinding keyBinding) { + return keyBinding != null && forceCheckKey.contains(keyBinding); + } + + /** + * This forces 2 key binds to always be compatible even if they have assigned the same key. + * Conflicts must be handled manually! + */ + public static void setCompatible(KeyBinding keyBinding1, KeyBinding keyBinding2) { + if (keyBinding1 == keyBinding2 || keyBinding1 == null || keyBinding2 == null) { + throw new IllegalArgumentException(); + } + compatibiliyMap.computeIfAbsent(keyBinding1, key -> new HashSet<>()).add(keyBinding2); + compatibiliyMap.computeIfAbsent(keyBinding2, key -> new HashSet<>()).add(keyBinding1); + } + + /** + * @return if the given key binds are forced to be compatible + */ + public static boolean areCompatible(KeyBinding keyBinding1, KeyBinding keyBinding2) { + return compatibiliyMap.getOrDefault(keyBinding1, Collections.emptySet()).contains(keyBinding2); + } + + /** + * @return all forced compatible key binds for the given key bind + */ + public static Collection getCompatibles(KeyBinding keyBinding) { + return compatibiliyMap.getOrDefault(keyBinding, Collections.emptySet()); + } + +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/ModularUITextures.java b/src/main/java/com/gtnewhorizons/modularui/api/ModularUITextures.java new file mode 100644 index 0000000..9402e23 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/ModularUITextures.java @@ -0,0 +1,29 @@ +package com.gtnewhorizons.modularui.api; + +import com.gtnewhorizons.modularui.ModularUI; +import com.gtnewhorizons.modularui.api.drawable.AdaptableUITexture; +import com.gtnewhorizons.modularui.api.drawable.UITexture; + +public class ModularUITextures { + + public static final UITexture ICON_INFO = UITexture.fullImage(ModularUI.MODID, "gui/widgets/information"); + public static final UITexture VANILLA_BACKGROUND = AdaptableUITexture.of(ModularUI.MODID, "gui/background/vanilla_background", 195, 136, 4); + public static final AdaptableUITexture BASE_BUTTON = AdaptableUITexture.of(ModularUI.MODID, "gui/widgets/base_button", 18, 18, 1); + public static final AdaptableUITexture ITEM_SLOT = AdaptableUITexture.of(ModularUI.MODID, "gui/slot/item", 18, 18, 1); + public static final AdaptableUITexture FLUID_SLOT = AdaptableUITexture.of(ModularUI.MODID, "gui/slot/fluid", 18, 18, 1); + + public static final UITexture ARROW_LEFT = UITexture.fullImage(ModularUI.MODID, "gui/icons/arrow_left"); + public static final UITexture ARROW_RIGHT = UITexture.fullImage(ModularUI.MODID, "gui/icons/arrow_right"); + public static final UITexture ARROW_UP = UITexture.fullImage(ModularUI.MODID, "gui/icons/arrow_up"); + public static final UITexture ARROW_DOWN = UITexture.fullImage(ModularUI.MODID, "gui/icons/arrow_down"); + public static final UITexture CROSS = UITexture.fullImage(ModularUI.MODID, "gui/icons/cross"); + + public static final UITexture VANILLA_TAB_TOP = UITexture.fullImage(ModularUI.MODID, "gui/tab/tabs_top"); + public static final UITexture VANILLA_TAB_BOTTOM = UITexture.fullImage(ModularUI.MODID, "gui/tab/tabs_bottom"); + public static final UITexture VANILLA_TAB_LEFT = UITexture.fullImage(ModularUI.MODID, "gui/tab/tabs_left"); + public static final UITexture VANILLA_TAB_RIGHT = UITexture.fullImage(ModularUI.MODID, "gui/tab/tabs_right"); + + public static final UITexture VANILLA_TAB_TOP_START = VANILLA_TAB_TOP.getSubArea(0f, 0f, 1 / 3f, 1f); + public static final UITexture VANILLA_TAB_TOP_MIDDLE = VANILLA_TAB_TOP.getSubArea(1 / 3f, 0f, 2 / 3f, 1f); + public static final UITexture VANILLA_TAB_TOP_END = VANILLA_TAB_TOP.getSubArea(2 / 3f, 0f, 1f, 1f); +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/NumberFormat.java b/src/main/java/com/gtnewhorizons/modularui/api/NumberFormat.java new file mode 100644 index 0000000..97423cc --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/NumberFormat.java @@ -0,0 +1,75 @@ +package com.gtnewhorizons.modularui.api; + +import org.jetbrains.annotations.NotNull; + +import java.text.DecimalFormat; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; + +public class NumberFormat { + + private static final NavigableMap suffixesByPower = new TreeMap<>(); + private static final java.text.NumberFormat[] NUMBER_FORMAT = { + new DecimalFormat("0."), + new DecimalFormat("0.#"), + new DecimalFormat("0.##"), + new DecimalFormat("0.###"), + new DecimalFormat("0.####"), + new DecimalFormat("0.#####"), + new DecimalFormat("0.######"), + new DecimalFormat("0.#######"), + new DecimalFormat("0.########"), + new DecimalFormat("0.#########"), + }; + + static { + suffixesByPower.put(0.000_000_000_000_000_001D, "a"); + suffixesByPower.put(0.000_000_000_000_001D, "f"); + suffixesByPower.put(0.000_000_000_001D, "p"); + suffixesByPower.put(0.000_000_001D, "n"); + suffixesByPower.put(0.000_001D, "u"); + suffixesByPower.put(0.001D, "m"); + suffixesByPower.put(1_000D, "k"); + suffixesByPower.put(1_000_000D, "M"); + suffixesByPower.put(1_000_000_000D, "G"); + suffixesByPower.put(1_000_000000_000D, "T"); + suffixesByPower.put(1_000_000000_000_000D, "P"); + suffixesByPower.put(1_000_000000_000_000_000D, "E"); + } + + @NotNull + public static String format(double value, int precision) { + //Double.MIN_VALUE == -Double.MIN_VALUE so we need an adjustment here + if (value == Double.MIN_VALUE) return format(Double.MIN_VALUE + 1, precision); + if (value == 0) return "0"; + if (value < 0) return '-' + format(-value, precision); + double divideBy; + String suffix; + if (value < pow(10, precision)) { + divideBy = 1; + suffix = ""; + } else { + Map.Entry e = suffixesByPower.floorEntry(value); + divideBy = e.getKey(); + suffix = e.getValue(); + } + + double truncated = value / (divideBy / 10); //the number part of the output times 10 + boolean hasDecimal = truncated < 100 && (truncated / 10D) != (truncated / 10); + return hasDecimal ? NUMBER_FORMAT[precision].format(truncated / 10D) + suffix : NUMBER_FORMAT[precision].format(truncated / 10) + suffix; + } + + @NotNull + public static String format(double value) { + return format(value, 3); + } + + private static int pow(int num, int e) { + int result = num; + for (int i = 0; i < e; i++) { + result *= num; + } + return result; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/UIInfos.java b/src/main/java/com/gtnewhorizons/modularui/api/UIInfos.java new file mode 100644 index 0000000..ec7229d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/UIInfos.java @@ -0,0 +1,57 @@ +package com.gtnewhorizons.modularui.api; + +import com.gtnewhorizons.modularui.ModularUI; +import com.gtnewhorizons.modularui.common.builder.UIBuilder; +import com.gtnewhorizons.modularui.common.builder.UIInfo; +import com.gtnewhorizons.modularui.common.internal.network.NetworkUtils; +import com.gtnewhorizons.modularui.common.internal.wrapper.ModularGui; +import com.gtnewhorizons.modularui.common.internal.wrapper.ModularUIContainer; + +import com.gtnewhorizons.modularui.api.screen.ITileWithModularUI; +import com.gtnewhorizons.modularui.api.screen.ModularUIContext; +import com.gtnewhorizons.modularui.api.screen.ModularWindow; +import com.gtnewhorizons.modularui.api.screen.UIBuildContext; +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.tileentity.TileEntity; + +import java.util.function.Function; + +public class UIInfos { + + public static void init() { + } + + public static final UIInfo TILE_MODULAR_UI = UIBuilder.of() + .gui(((player, world, x, y, z) -> { + if (!world.isRemote) return null; + TileEntity te = world.getTileEntity(x, y, z); + if (te instanceof ITileWithModularUI) { + return ModularUI.createGuiScreen(player, ((ITileWithModularUI) te)::createWindow); + } + return null; + })) + .container((player, world, x, y, z) -> { + TileEntity te = world.getTileEntity(x, y, z); + if (te instanceof ITileWithModularUI) { + return ModularUI.createContainer(player, ((ITileWithModularUI) te)::createWindow); + } + return null; + }) + .build(); + + @SideOnly(Side.CLIENT) + public static void openClientUI(EntityPlayer player, Function uiCreator) { + if (!NetworkUtils.isClient(player)) { + ModularUI.logger.info("Tried opening client ui on server!"); + return; + } + UIBuildContext buildContext = new UIBuildContext(player); + ModularWindow window = uiCreator.apply(buildContext); + GuiScreen screen = new ModularGui(new ModularUIContainer(new ModularUIContext(buildContext, true), window)); + FMLCommonHandler.instance().showGuiScreen(screen); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/Widget.java b/src/main/java/com/gtnewhorizons/modularui/api/Widget.java new file mode 100644 index 0000000..6ea5f6d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/Widget.java @@ -0,0 +1,15 @@ +package com.gtnewhorizons.modularui.api; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.texture.TextureManager; + +public class Widget { + + public void drawForeGround(int mouseX, int mouseY, FontRenderer fontRenderer){ + + } + + public void drawBackGround(int mouseX, int mouseY, TextureManager textureManager, int WindowStartX, int windwStartY){ + + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/animation/Eases.java b/src/main/java/com/gtnewhorizons/modularui/api/animation/Eases.java new file mode 100644 index 0000000..674fbf2 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/animation/Eases.java @@ -0,0 +1,25 @@ +package com.gtnewhorizons.modularui.api.animation; + +public enum Eases implements IEase { + EaseLinear(input -> input), + EaseQuadIn(input -> input * input), + EaseQuadInOut(input -> { + if ((input /= 0.5f) < 1) { + return 0.5f * input * input; + } + return -0.5f * ((--input) * (input - 2) - 1); + }), + EaseQuadOut(input -> -input * (input - 2)); + + + IEase ease; + + Eases(IEase ease) { + this.ease = ease; + } + + @Override + public float interpolate(float t) { + return ease.interpolate(t); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/animation/IEase.java b/src/main/java/com/gtnewhorizons/modularui/api/animation/IEase.java new file mode 100644 index 0000000..d1edcf6 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/animation/IEase.java @@ -0,0 +1,6 @@ +package com.gtnewhorizons.modularui.api.animation; + +@FunctionalInterface +public interface IEase { + float interpolate(float t); +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/animation/Interpolator.java b/src/main/java/com/gtnewhorizons/modularui/api/animation/Interpolator.java new file mode 100644 index 0000000..4751729 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/animation/Interpolator.java @@ -0,0 +1,87 @@ +package com.gtnewhorizons.modularui.api.animation; + +import java.util.function.Consumer; + +public class Interpolator { + + private final float from; + private final float to; + private final int duration; + private final IEase ease; + private final Consumer interpolate; + private Consumer callback; + + private int runs = 0; + private int progress = 0; + + public Interpolator(float from, float to, int duration, IEase ease, Consumer interpolate) { + this(from, to, duration, ease, interpolate, null); + } + + public Interpolator(float from, float to, int duration, IEase ease, Consumer interpolate, Consumer callback) { + this.from = from; + this.to = to; + this.duration = duration; + this.ease = ease; + this.interpolate = interpolate; + this.callback = callback; + } + + public Interpolator getReversed(int duration, IEase ease) { + return new Interpolator(to, from, duration, ease, interpolate, callback); + } + + public void setCallback(Consumer callback) { + this.callback = callback; + } + + public void stop() { + runs = 0; + } + + public Interpolator forward() { + progress = 0; + runs = 1; + return this; + } + + public void backwards() { + progress = duration; + runs = -1; + } + + public boolean isAtStart() { + return progress == 0; + } + + public boolean isAtEnd() { + return progress >= duration; + } + + public boolean isRunning() { + return runs != 0 && progress > 0 && progress < duration; + } + + public void update(float partialTicks) { + if (runs != 0) { + if (runs == -1 && progress <= 0) { + progress = 0; + if (callback != null) { + callback.accept(ease.interpolate(progress * 1.0f / duration) * (to - from) + from); + } + stop(); + return; + } else if (runs == 1 && progress >= duration) { + progress = duration; + if (callback != null) { + callback.accept(ease.interpolate(progress * 1.0f / duration) * (to - from) + from); + } + stop(); + return; + } else { + interpolate.accept(ease.interpolate(progress * 1.0f / duration) * (to - from) + from); + } + progress += partialTicks * 50 * runs; + } + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/drawable/AdaptableUITexture.java b/src/main/java/com/gtnewhorizons/modularui/api/drawable/AdaptableUITexture.java new file mode 100644 index 0000000..e8aa4ca --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/drawable/AdaptableUITexture.java @@ -0,0 +1,68 @@ +package com.gtnewhorizons.modularui.api.drawable; + +import net.minecraft.util.ResourceLocation; + +public class AdaptableUITexture extends UITexture { + + private final int imageWidth, imageHeight, borderWidthU, borderWidthV; + + public AdaptableUITexture(ResourceLocation location, float u0, float v0, float u1, float v1, int imageWidth, int imageHeight, int borderWidthU, int borderWidthV) { + super(location, u0, v0, u1, v1); + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + this.borderWidthU = borderWidthU; + this.borderWidthV = borderWidthV; + } + + public AdaptableUITexture(ResourceLocation location, int imageWidth, int imageHeight, int borderWidthU, int borderWidthV) { + this(location, 0, 0, 1, 1, imageWidth, imageHeight, borderWidthU, borderWidthV); + } + + public static AdaptableUITexture of(ResourceLocation location, int imageWidth, int imageHeight, int borderWidthU, int borderWidthV) { + return new AdaptableUITexture(location, imageWidth, imageHeight, borderWidthU, borderWidthV); + } + + public static AdaptableUITexture of(ResourceLocation location, int imageWidth, int imageHeight, int borderWidthPixel) { + return new AdaptableUITexture(location, imageWidth, imageHeight, borderWidthPixel, borderWidthPixel); + } + + public static AdaptableUITexture of(String location, int imageWidth, int imageHeight, int borderWidthPixel) { + return new AdaptableUITexture(new ResourceLocation(location), imageWidth, imageHeight, borderWidthPixel, borderWidthPixel); + } + + public static AdaptableUITexture of(String mod, String location, int imageWidth, int imageHeight, int borderWidthPixel) { + return new AdaptableUITexture(new ResourceLocation(mod, location), imageWidth, imageHeight, borderWidthPixel, borderWidthPixel); + } + + @Override + public AdaptableUITexture getSubArea(float uStart, float vStart, float uEnd, float vEnd) { + return new AdaptableUITexture(location, calcU(uStart), calcV(vStart), calcU(uEnd), calcV(vEnd), imageWidth, imageHeight, borderWidthU, borderWidthV); + } + + @Override + public AdaptableUITexture exposeToJson() { + return (AdaptableUITexture) super.exposeToJson(); + } + + @Override + public void draw(float x, float y, float width, float height) { + if (width == imageWidth && height == imageHeight) { + super.draw(x, y, width, height); + return; + } + float borderU = borderWidthU * 1f / imageWidth; + float borderV = borderWidthV * 1f / imageHeight; + // draw corners + draw(location, x, y, borderWidthU, borderWidthV, u0, v0, borderU, borderV); // x0 y0 + draw(location, x + width - borderWidthU, y, borderWidthU, borderWidthV, u1 - borderU, v0, u1, borderV); // x1 y0 + draw(location, x, y + height - borderWidthV, borderWidthU, borderWidthV, u0, v1 - borderV, borderU, v1); // x0 y1 + draw(location, x + width - borderWidthU, y + height - borderWidthV, borderWidthU, borderWidthV, u1 - borderU, v1 - borderV, u1, v1); // x1 y1 + // draw edges + draw(location, x + borderWidthU, y, width - borderWidthU * 2, borderWidthV, borderU, v0, u1 - borderU, borderV); // top + draw(location, x + borderWidthU, y + height - borderWidthV, width - borderWidthU * 2, borderWidthV, borderU, v1 - borderV, u1 - borderU, v1); // bottom + draw(location, x, y + borderWidthV, borderWidthU, height - borderWidthV * 2, u0, borderV, borderU, v1 - borderV); // left + draw(location, x + width - borderWidthU, y + borderWidthV, borderWidthU, height - borderWidthV * 2, u1 - borderU, borderV, u1, v1 - borderV); // left + // draw body + draw(location, x + borderWidthU, y + borderWidthV, width - borderWidthU * 2, height - borderWidthV * 2, borderU, borderV, u1 - borderU, v1 - borderV); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/drawable/GuiHelper.java b/src/main/java/com/gtnewhorizons/modularui/api/drawable/GuiHelper.java new file mode 100644 index 0000000..50758e2 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/drawable/GuiHelper.java @@ -0,0 +1,318 @@ +package com.gtnewhorizons.modularui.api.drawable; + +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.math.Alignment; +import com.gtnewhorizons.modularui.api.math.Color; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.util.IIcon; +import net.minecraftforge.client.GuiIngameForge; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.FluidStack; +import org.apache.commons.lang3.tuple.Pair; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; + +import java.util.List; +import java.util.Stack; +import java.util.stream.Collectors; + +@SideOnly(Side.CLIENT) +public class GuiHelper { + + //==== Screen helpers ==== + + public static boolean hasScreen() { + return Minecraft.getMinecraft().currentScreen != null; + } + + public static GuiScreen getActiveScreen() { + return Minecraft.getMinecraft().currentScreen; + } + + /** + * @return the scaled screen size. (0;0) if no screen is open. + */ + public static Size getScreenSize() { + GuiScreen screen = Minecraft.getMinecraft().currentScreen; + if (screen != null) { + return new Size(screen.width, screen.height); + } + return Size.ZERO; + } + + /** + * @return the current mouse pos. (0;0) if no screen is open. + */ + public static Pos2d getCurrentMousePos() { + GuiScreen screen = Minecraft.getMinecraft().currentScreen; + if (screen != null) { + int x = Mouse.getEventX() * screen.width / Minecraft.getMinecraft().displayWidth; + int y = screen.height - Mouse.getEventY() * screen.height / Minecraft.getMinecraft().displayHeight - 1; + return new Pos2d(x, y); + } + return Pos2d.ZERO; + } + + + //==== Tooltip helpers ==== + + public static void drawHoveringText(List textLines, Pos2d mousePos, Size screenSize, int maxWidth, float scale, boolean forceShadow, Alignment alignment) { + if (textLines.isEmpty()) { + return; + } + List lines = textLines.stream().map(line -> line.getFormatted()).collect(Collectors.toList()); + drawHoveringTextFormatted(lines, mousePos, screenSize, maxWidth, scale, forceShadow, alignment); + } + + public static void drawHoveringTextFormatted(List lines, Pos2d mousePos, Size screenSize, int maxWidth) { + drawHoveringTextFormatted(lines, mousePos, screenSize, maxWidth, 1f, false, Alignment.TopLeft); + } + + public static void drawHoveringTextFormatted(List lines, Pos2d mousePos, Size screenSize, int maxWidth, float scale, boolean forceShadow, Alignment alignment) { + if (lines.isEmpty()) { + return; + } + if (maxWidth < 0) { + maxWidth = Integer.MAX_VALUE; + } +// RenderTooltipEvent.Pre event = new RenderTooltipEvent.Pre(ItemStack.EMPTY, lines, mousePos.x, mousePos.y, screenSize.width, screenSize.height, maxWidth, TextRenderer.getFontRenderer()); +// if (MinecraftForge.EVENT_BUS.post(event)) { +// return; +// } +// lines = event.getLines(); +// mousePos = new Pos2d(event.x(), event.getY()); +// screenSize = new Size(event.getScreenWidth(), event.getScreenHeight()); +// maxWidth = event.getMaxWidth(); + + int maxTextWidth = maxWidth; + + boolean mouseOnRightSide = false; + int screenSpaceRight = screenSize.width - mousePos.x - 16; + if (mousePos.x > screenSize.width / 2f) { + mouseOnRightSide = true; + } + if (maxTextWidth > screenSpaceRight) { + maxTextWidth = screenSpaceRight; + } + boolean putOnLeft = false; + int tooltipY = mousePos.y - 12; + int tooltipX = mousePos.x + 12; + TextRenderer renderer = new TextRenderer(); + renderer.setPos(mousePos); + renderer.setAlignment(Alignment.TopLeft, maxTextWidth); + renderer.setScale(scale); + renderer.setShadow(forceShadow); + renderer.setSimulate(true); + List> measuredLines = renderer.measureLines(lines); + if (mouseOnRightSide && measuredLines.size() > lines.size()) { + putOnLeft = true; + maxTextWidth = Math.min(maxWidth, mousePos.x - 16); + } + + renderer.setAlignment(Alignment.TopLeft, maxTextWidth); + measuredLines = renderer.measureLines(lines); + renderer.drawMeasuredLines(measuredLines); + int tooltipTextWidth = (int) renderer.lastWidth; + int tooltipHeight = (int) renderer.lastHeight; + + if (mouseOnRightSide && putOnLeft) { + tooltipX += -24 - tooltipTextWidth; + } + + GlStateManager.disableRescaleNormal(); + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableLighting(); + GlStateManager.disableDepth(); + + int color = 0xFFFFFF; + + final int zLevel = 300; + int backgroundColor = 0xF0100010; + int borderColorStart = 0x505000FF; + int borderColorEnd = (borderColorStart & 0xFEFEFE) >> 1 | borderColorStart & 0xFF000000; +// RenderTooltipEvent.Color colorEvent = new RenderTooltipEvent.Color(ItemStack.EMPTY, lines, tooltipX, tooltipY, TextRenderer.getFontRenderer(), backgroundColor, borderColorStart, borderColorEnd); +// MinecraftForge.EVENT_BUS.post(colorEvent); +// backgroundColor = colorEvent.getBackground(); +// borderColorStart = colorEvent.getBorderStart(); +// borderColorEnd = colorEvent.getBorderEnd(); + drawGradientRect(zLevel, tooltipX - 3, tooltipY - 4, tooltipX + tooltipTextWidth + 3, tooltipY - 3, backgroundColor, backgroundColor); + drawGradientRect(zLevel, tooltipX - 3, tooltipY + tooltipHeight + 3, tooltipX + tooltipTextWidth + 3, tooltipY + tooltipHeight + 4, backgroundColor, backgroundColor); + drawGradientRect(zLevel, tooltipX - 3, tooltipY - 3, tooltipX + tooltipTextWidth + 3, tooltipY + tooltipHeight + 3, backgroundColor, backgroundColor); + drawGradientRect(zLevel, tooltipX - 4, tooltipY - 3, tooltipX - 3, tooltipY + tooltipHeight + 3, backgroundColor, backgroundColor); + drawGradientRect(zLevel, tooltipX + tooltipTextWidth + 3, tooltipY - 3, tooltipX + tooltipTextWidth + 4, tooltipY + tooltipHeight + 3, backgroundColor, backgroundColor); + drawGradientRect(zLevel, tooltipX - 3, tooltipY - 3 + 1, tooltipX - 3 + 1, tooltipY + tooltipHeight + 3 - 1, borderColorStart, borderColorEnd); + drawGradientRect(zLevel, tooltipX + tooltipTextWidth + 2, tooltipY - 3 + 1, tooltipX + tooltipTextWidth + 3, tooltipY + tooltipHeight + 3 - 1, borderColorStart, borderColorEnd); + drawGradientRect(zLevel, tooltipX - 3, tooltipY - 3, tooltipX + tooltipTextWidth + 3, tooltipY - 3 + 1, borderColorStart, borderColorStart); + drawGradientRect(zLevel, tooltipX - 3, tooltipY + tooltipHeight + 2, tooltipX + tooltipTextWidth + 3, tooltipY + tooltipHeight + 3, borderColorEnd, borderColorEnd); + +// MinecraftForge.EVENT_BUS.post(new RenderTooltipEvent.PostBackground(ItemStack.EMPTY, lines, tooltipX, tooltipY, TextRenderer.getFontRenderer(), tooltipTextWidth, tooltipHeight)); + + renderer.setSimulate(false); + renderer.setPos(tooltipX, tooltipY); + renderer.setAlignment(alignment, maxTextWidth); + renderer.setColor(color); + renderer.drawMeasuredLines(measuredLines); + +// MinecraftForge.EVENT_BUS.post(new RenderTooltipEvent.PostText(ItemStack.EMPTY, lines, tooltipX, tooltipY, TextRenderer.getFontRenderer(), tooltipTextWidth, tooltipHeight)); + + GlStateManager.enableLighting(); + GlStateManager.enableDepth(); + RenderHelper.enableStandardItemLighting(); + GlStateManager.enableRescaleNormal(); + } + + //==== Draw helpers ==== + + public static void drawGradientRect(float zLevel, float left, float top, float right, float bottom, int startColor, int endColor) { + float startAlpha = (float) (startColor >> 24 & 255) / 255.0F; + float startRed = (float) (startColor >> 16 & 255) / 255.0F; + float startGreen = (float) (startColor >> 8 & 255) / 255.0F; + float startBlue = (float) (startColor & 255) / 255.0F; + float endAlpha = (float) (endColor >> 24 & 255) / 255.0F; + float endRed = (float) (endColor >> 16 & 255) / 255.0F; + float endGreen = (float) (endColor >> 8 & 255) / 255.0F; + float endBlue = (float) (endColor & 255) / 255.0F; + + GlStateManager.disableTexture2D(); + GlStateManager.enableBlend(); + GlStateManager.disableAlpha(); + GlStateManager.tryBlendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO); + GlStateManager.shadeModel(GL11.GL_SMOOTH); + + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawing(GL11.GL_QUADS); + tessellator.setColorRGBA_F(startRed, startGreen, startBlue, startAlpha); + tessellator.addVertex(right, top, zLevel); + tessellator.setColorRGBA_F(startRed, startGreen, startBlue, startAlpha); + tessellator.addVertex(left, top, zLevel); + tessellator.setColorRGBA_F(endRed, endGreen, endBlue, endAlpha); + tessellator.addVertex(left, bottom, zLevel); + tessellator.setColorRGBA_F(endRed, endGreen, endBlue, endAlpha); + tessellator.addVertex(right, bottom, zLevel); + tessellator.draw(); + + GlStateManager.shadeModel(GL11.GL_FLAT); + GlStateManager.disableBlend(); + GlStateManager.enableAlpha(); + GlStateManager.enableTexture2D(); + } + + public static void drawFluidTexture(FluidStack content, float x0, float y0, float width, float height, float z) { + if (content == null) { + return; + } + Fluid fluid = content.getFluid(); + IIcon fluidStill = fluid.getIcon(content); + int fluidColor = fluid.getColor(content); + GlStateManager.enableBlend(); + Minecraft.getMinecraft().renderEngine.bindTexture(TextureMap.locationBlocksTexture); + + float u0 = fluidStill.getMinU(), u1 = fluidStill.getMaxU(), v0 = fluidStill.getMinV(), v1 = fluidStill.getMaxV(); + float x1 = x0 + width, y1 = y0 + height; + float r = Color.getRedF(fluidColor), g = Color.getGreenF(fluidColor), b = Color.getBlueF(fluidColor), a = Color.getAlphaF(fluidColor); + + Tessellator tessellator = Tessellator.instance; +// tessellator.startDrawing(7, DefaultVertexFormats.POSITION_TEX_COLOR); + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_F(r, g, b, a); + tessellator.setTextureUV(u0, v1); + tessellator.addVertex(x0, y1, z); + tessellator.setTextureUV(u1, v1); + tessellator.addVertex(x1, y1, z); + tessellator.setTextureUV(u1, v0); + tessellator.addVertex(x1, y0, z); + tessellator.setTextureUV(u0, v0); + tessellator.addVertex(x0, y0, z); + tessellator.draw(); + GlStateManager.disableBlend(); + } + + + //==== Scissor helpers ==== + + private static final Stack scissorFrameStack = new Stack<>(); + + public static void useScissor(int x, int y, int width, int height, Runnable codeBlock) { + pushScissorFrame(x, y, width, height); + try { + codeBlock.run(); + } finally { + popScissorFrame(); + } + } + + private static int[] peekFirstScissorOrFullScreen() { + int[] currentTopFrame = scissorFrameStack.isEmpty() ? null : scissorFrameStack.peek(); + if (currentTopFrame == null) { + Minecraft minecraft = Minecraft.getMinecraft(); + return new int[]{0, 0, minecraft.displayWidth, minecraft.displayHeight}; + } + return currentTopFrame; + } + + public static void pushScissorFrame(int x, int y, int width, int height) { + int[] parentScissor = peekFirstScissorOrFullScreen(); + int parentX = parentScissor[0]; + int parentY = parentScissor[1]; + int parentWidth = parentScissor[2]; + int parentHeight = parentScissor[3]; + + boolean pushedFrame = false; + if (x <= parentX + parentWidth && y <= parentY + parentHeight) { + int newX = Math.max(x, parentX); + int newY = Math.max(y, parentY); + int newWidth = width - (newX - x); + int newHeight = height - (newY - y); + if (newWidth > 0 && newHeight > 0) { + int maxWidth = parentWidth - (x - parentX); + int maxHeight = parentHeight - (y - parentY); + newWidth = Math.min(maxWidth, newWidth); + newHeight = Math.min(maxHeight, newHeight); + applyScissor(newX, newY, newWidth, newHeight); + //finally, push applied scissor on top of scissor stack + if (scissorFrameStack.isEmpty()) { + GL11.glEnable(GL11.GL_SCISSOR_TEST); + } + scissorFrameStack.push(new int[]{newX, newY, newWidth, newHeight}); + pushedFrame = true; + } + } + if (!pushedFrame) { + if (scissorFrameStack.isEmpty()) { + GL11.glEnable(GL11.GL_SCISSOR_TEST); + } + scissorFrameStack.push(new int[]{parentX, parentY, parentWidth, parentHeight}); + } + } + + public static void popScissorFrame() { + scissorFrameStack.pop(); + int[] parentScissor = peekFirstScissorOrFullScreen(); + int parentX = parentScissor[0]; + int parentY = parentScissor[1]; + int parentWidth = parentScissor[2]; + int parentHeight = parentScissor[3]; + applyScissor(parentX, parentY, parentWidth, parentHeight); + if (scissorFrameStack.isEmpty()) { + GL11.glDisable(GL11.GL_SCISSOR_TEST); + } + } + + //applies scissor with gui-space coordinates and sizes + private static void applyScissor(int x, int y, int w, int h) { + //translate upper-left to bottom-left + ScaledResolution r = ((GuiIngameForge) Minecraft.getMinecraft().ingameGUI).getResolution(); + int s = r == null ? 1 : r.getScaleFactor(); + int translatedY = r == null ? 0 : (r.getScaledHeight() - y - h); + GL11.glScissor(x * s, translatedY * s, w * s, h * s); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/drawable/IDrawable.java b/src/main/java/com/gtnewhorizons/modularui/api/drawable/IDrawable.java new file mode 100644 index 0000000..a238415 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/drawable/IDrawable.java @@ -0,0 +1,120 @@ +package com.gtnewhorizons.modularui.api.drawable; + +import com.google.gson.JsonObject; +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.math.Color; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.common.internal.JsonHelper; +import com.gtnewhorizons.modularui.common.widget.DrawableWidget; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@FunctionalInterface +public interface IDrawable { + + /** + * Empty drawable + */ + IDrawable EMPTY = (x, y, width, height, partialTicks) -> { + }; + + /** + * Called ever frame + * + * @param x x position + * @param y y position + * @param width width of the drawable + * @param height height of the drawable + * @param partialTicks ticks since last render + */ + @SideOnly(Side.CLIENT) + void draw(float x, float y, float width, float height, float partialTicks); + + @SideOnly(Side.CLIENT) + default void draw(Pos2d pos, Size size, float partialTicks) { + draw(pos.x, pos.y, size.width, size.height, partialTicks); + } + + default void tick() { + } + + @SideOnly(Side.CLIENT) + default void applyThemeColor(int color) { + GlStateManager.color(Color.getRedF(color), Color.getGreenF(color), Color.getBlueF(color), Color.getAlphaF(color)); + } + + @SideOnly(Side.CLIENT) + default void applyThemeColor() { + applyThemeColor(Color.WHITE.normal); + } + + /** + * @return a drawable that can be used in guis as a widget + */ + default DrawableWidget asWidget() { + return new DrawableWidget().setDrawable(this); + } + + /** + * This drawable with an offset pos. + * Useful if the background of a widget should be larger than the widget itself. + * + * @param offsetX offset in x + * @param offsetY offset in y + * @param widthOffset offset width (added to the width passed in {@link #draw(float, float, float, float, float)}) + * @param heightOffset offset height (added to the height passed in {@link #draw(float, float, float, float, float)}) + * @return this drawable with offset + */ + default IDrawable withOffset(float offsetX, float offsetY, float widthOffset, float heightOffset) { + return new OffsetDrawable(this, offsetX, offsetY, widthOffset, heightOffset); + } + + default IDrawable withOffset(float offsetX, float offsetY) { + return new OffsetDrawable(this, offsetX, offsetY); + } + + /** + * This drawable with a fixed size. + * + * @param fixedHeight fixed width (ignores width passed in {@link #draw(float, float, float, float, float)}) + * @param fixedWidth fixed height (ignores height passed in {@link #draw(float, float, float, float, float)}) + * @param offsetX offset in x + * @param offsetY offset in y + * @return this drawable with offset + */ + default IDrawable withFixedSize(float fixedWidth, float fixedHeight, float offsetX, float offsetY) { + return new SizedDrawable(this, fixedWidth, fixedHeight, offsetX, offsetY); + } + + default IDrawable withFixedSize(float fixedWidth, float fixedHeight) { + return new SizedDrawable(this, fixedWidth, fixedHeight); + } + + static final Map> JSON_DRAWABLE_MAP = new HashMap<>(); + + static IDrawable ofJson(JsonObject json) { + IDrawable drawable = EMPTY; + if (json.has("type")) { + Function function = JSON_DRAWABLE_MAP.get(json.get("type").getAsString()); + if (function != null) { + drawable = function.apply(json); + } + } + Pos2d offset = JsonHelper.getElement(json, Pos2d.ZERO, Pos2d::ofJson, "offset"); + Size offsetSize = JsonHelper.getElement(json, Size.ZERO, Size::ofJson, "offsetSize"); + Size fixedSize = JsonHelper.getElement(json, Size.ZERO, Size::ofJson, "fixedSize"); + if (!fixedSize.isZero()) { + return drawable.withFixedSize(fixedSize.width, fixedSize.height, offset.x, offset.y); + } + if (!offset.isZero() || !offsetSize.isZero()) { + return drawable.withOffset(offset.x, offset.y, offsetSize.width, offsetSize.height); + } + + return drawable; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/drawable/ItemDrawable.java b/src/main/java/com/gtnewhorizons/modularui/api/drawable/ItemDrawable.java new file mode 100644 index 0000000..61a523b --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/drawable/ItemDrawable.java @@ -0,0 +1,52 @@ +package com.gtnewhorizons.modularui.api.drawable; + +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.Widget; +import com.gtnewhorizons.modularui.common.widget.DrawableWidget; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.entity.RenderItem; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +/** + * Draws item. Can also be used for {@link Widget} + */ +public class ItemDrawable implements IDrawable { + + private ItemStack item = null; + + private static final RenderItem itemRenderer = new RenderItem(); + + public ItemDrawable(@NotNull ItemStack item) { + this.item = item; + } + + @Override + public void applyThemeColor(int color) { + } + + @Override + public void draw(float x, float y, float width, float height, float partialTicks) { + if (item.getItem() == null) return; + GlStateManager.pushMatrix(); + RenderHelper.enableGUIStandardItemLighting(); + GlStateManager.enableDepth(); + GlStateManager.scale(width / 16, height / 16, 1); + itemRenderer.renderItemAndEffectIntoGUI(item.getItem().getFontRenderer(item), Minecraft.getMinecraft().getTextureManager(), item, (int) x, (int) y); + GlStateManager.disableDepth(); + RenderHelper.enableStandardItemLighting(); + GlStateManager.disableLighting(); + GlStateManager.popMatrix(); + } + + @Override + public DrawableWidget asWidget() { + return (DrawableWidget) IDrawable.super.asWidget().setSize(16, 16); + } + + public ItemDrawable setItem(@NotNull ItemStack item) { + this.item = item; + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/drawable/OffsetDrawable.java b/src/main/java/com/gtnewhorizons/modularui/api/drawable/OffsetDrawable.java new file mode 100644 index 0000000..3f8e4d6 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/drawable/OffsetDrawable.java @@ -0,0 +1,28 @@ +package com.gtnewhorizons.modularui.api.drawable; + +/** + * Draws a {@link IDrawable} to a offset pos with a offset size + */ +public class OffsetDrawable implements IDrawable { + + private final IDrawable drawable; + private final float offsetX, offsetY; + private final float widthOffset, heightOffset; + + public OffsetDrawable(IDrawable drawable, float offsetX, float offsetY, float widthOffset, float heightOffset) { + this.drawable = drawable; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.widthOffset = widthOffset; + this.heightOffset = heightOffset; + } + + public OffsetDrawable(IDrawable drawable, float offsetX, float offsetY) { + this(drawable, offsetX, offsetY, 0, 0); + } + + @Override + public void draw(float x, float y, float width, float height, float partialTicks) { + drawable.draw(x + offsetX, y + offsetY, width + widthOffset, height + heightOffset, partialTicks); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/drawable/SizedDrawable.java b/src/main/java/com/gtnewhorizons/modularui/api/drawable/SizedDrawable.java new file mode 100644 index 0000000..3a9b4a4 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/drawable/SizedDrawable.java @@ -0,0 +1,28 @@ +package com.gtnewhorizons.modularui.api.drawable; + +/** + * Draws a {@link IDrawable} with a fixed size to a offset position + */ +public class SizedDrawable implements IDrawable { + + private final IDrawable drawable; + private final float fixedWidth, fixedHeight; + private final float offsetX, offsetY; + + public SizedDrawable(IDrawable drawable, float fixedWidth, float fixedHeight, float offsetX, float offsetY) { + this.drawable = drawable; + this.fixedWidth = fixedWidth; + this.fixedHeight = fixedHeight; + this.offsetX = offsetX; + this.offsetY = offsetY; + } + + public SizedDrawable(IDrawable drawable, float fixedWidth, float fixedHeight) { + this(drawable, fixedWidth, fixedHeight, 0, 0); + } + + @Override + public void draw(float x, float y, float width, float height, float partialTicks) { + drawable.draw(x + offsetX, y + offsetY, fixedWidth, fixedHeight, partialTicks); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/drawable/Text.java b/src/main/java/com/gtnewhorizons/modularui/api/drawable/Text.java new file mode 100644 index 0000000..0a0b8ca --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/drawable/Text.java @@ -0,0 +1,139 @@ +package com.gtnewhorizons.modularui.api.drawable; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.gtnewhorizons.modularui.api.math.Alignment; +import com.gtnewhorizons.modularui.api.math.Color; +import com.gtnewhorizons.modularui.common.internal.JsonHelper; +import com.gtnewhorizons.modularui.common.internal.Theme; +import cpw.mods.fml.common.FMLCommonHandler; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; + +import javax.annotation.Nullable; +import java.util.Objects; +import java.util.function.Supplier; + +public class Text implements IDrawable { + + public static final Text EMPTY = new Text(""); + + private static final TextRenderer renderer = new TextRenderer(); + private final String text; + private String formatting = ""; + @Nullable + private Supplier localisationData; + private int color; + private boolean shadow = false; + + public Text(String text) { + this.text = Objects.requireNonNull(text, "String in Text can't be null!"); + this.color = Theme.INSTANCE.getText(); + } + + public static Text localised(String key, Object... data) { + return new Text(key).localise(data); + } + + public static Text of(ChatComponentText textComponent) { + return new Text(textComponent.getFormattedText()); + } + + public Text color(int color) { + this.color = Color.withAlpha(color, 255); + return this; + } + + public Text format(EnumChatFormatting color) { + this.formatting = color.toString() + this.formatting; + return this; + } + + public Text shadow(boolean shadow) { + this.shadow = shadow; + return this; + } + + public Text localise(Supplier localisationData) { + this.localisationData = localisationData; + return this; + } + + public Text localise(Object... localisationData) { + localise(() -> localisationData); + return this; + } + + public Text shadow() { + return shadow(true); + } + + public int getColor() { + return color; + } + + public boolean hasColor() { + return Color.getAlpha(color) > 0; + } + + public boolean hasShadow() { + return shadow; + } + + public String getRawText() { + return text; + } + + @Override + public void applyThemeColor(int color) { + renderer.setColor(hasColor() ? this.color : Theme.INSTANCE.getText()); + } + + @Override + public void draw(float x, float y, float width, float height, float partialTicks) { + renderer.setPos((int) (x + 0.5), (int) (y + 0.5)); + renderer.setShadow(shadow); + renderer.setAlignment(Alignment.Center, width, height); + renderer.draw(getFormatted()); + } + + public String getFormatted() { + String text = getRawText(); + if (localisationData != null && FMLCommonHandler.instance().getSide().isClient()) { + text = I18n.format(text, localisationData.get()).replaceAll("\\\\n", "\n"); + } + if (!this.formatting.isEmpty()) { + text = formatting + text; + } + return text; + } + + public static String getFormatted(Text... texts) { + StringBuilder builder = new StringBuilder(); + for (Text text : texts) { + builder.append(text.getFormatted()); + } + return builder.toString(); + } + + public static Text ofJson(JsonElement json) { + if (json.isJsonObject()) { + JsonObject jsonObject = json.getAsJsonObject(); + Text text = new Text(JsonHelper.getString(jsonObject, "E:404", "text")); + text.shadow(JsonHelper.getBoolean(jsonObject, false, "shadow")); + Integer color = JsonHelper.getElement(jsonObject, null, Color::ofJson, "color"); + if (color != null) { + text.color(color); + } + if (JsonHelper.getBoolean(jsonObject, false, "localise")) { + text.localise(); + } + return text; + } + if (!json.isJsonArray()) { + return new Text(json.getAsString()); + } + return new Text(""); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/drawable/TextRenderer.java b/src/main/java/com/gtnewhorizons/modularui/api/drawable/TextRenderer.java new file mode 100644 index 0000000..826c4ab --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/drawable/TextRenderer.java @@ -0,0 +1,183 @@ +package com.gtnewhorizons.modularui.api.drawable; + +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.math.Alignment; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.common.internal.Theme; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class TextRenderer { + + protected float maxWidth = -1, maxHeight = -1; + protected int x = 0, y = 0; + protected Alignment alignment = Alignment.TopLeft; + protected float scale = 1f; + protected boolean shadow = false; + protected int color = Theme.INSTANCE.getText(); + protected boolean simulate; + protected float lastWidth = 0, lastHeight = 0; + + public void setAlignment(Alignment alignment, float maxWidth) { + setAlignment(alignment, maxWidth, -1); + } + + public void setAlignment(Alignment alignment, float maxWidth, float maxHeight) { + this.alignment = alignment; + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + } + + public void setShadow(boolean shadow) { + this.shadow = shadow; + } + + public void setScale(float scale) { + this.scale = scale; + } + + public void setPos(int x, int y) { + this.x = x; + this.y = y; + } + + public void setPos(Pos2d pos) { + setPos(pos.x, pos.y); + } + + public void setColor(int color) { + this.color = color; + } + + public void setSimulate(boolean simulate) { + this.simulate = simulate; + } + + public void draw(String text) { + draw(Collections.singletonList(text)); + } + + public void draw(List lines) { + drawMeasuredLines(measureLines(lines)); + } + + protected void drawMeasuredLines(List> measuredLines) { + float maxW = 0; + int y0 = getStartY(measuredLines.size()); + for (Pair measuredLine : measuredLines) { + int x0 = getStartX(measuredLine.getRight()); + maxW = Math.max(draw(measuredLine.getLeft(), x0, y0), maxW); + y0 += getFontRenderer().FONT_HEIGHT * scale; + } + this.lastWidth = maxWidth > 0 ? Math.min(maxW, maxWidth) : maxW; + this.lastHeight = measuredLines.size() * getFontHeight(); + this.lastWidth = Math.max(0, this.lastWidth - scale); + this.lastHeight = Math.max(0, this.lastHeight - scale); + } + + public List> measureLines(List lines) { + List> measuredLines = new ArrayList<>(); + for (String line : lines) { + for (String subLine : wrapLine(line)) { + float width = getFontRenderer().getStringWidth(subLine) * scale; + measuredLines.add(Pair.of(subLine, width)); + } + } + return measuredLines; + } + + public List wrapLine(String line) { + return maxWidth > 0 ? getFontRenderer().listFormattedStringToWidth(line, (int) (maxWidth / scale)) : Collections.singletonList(line); + } + + public boolean wouldFit(List text) { + if (maxHeight > 0 && maxHeight < text.size() * getFontHeight() - scale) { + return false; + } + if (maxWidth > 0) { + for (String line : text) { + if (maxWidth < getFontRenderer().getStringWidth(line)) { + return false; + } + } + } + return true; + } + + public int getMaxWidth(List lines) { + if (lines.isEmpty()) { + return 0; + } + List> measuredLines = measureLines(lines); + float w = 0; + for (Pair measuredLine : measuredLines) { + w = Math.max(w, measuredLine.getRight()); + } + return (int) Math.ceil(w); + } + + protected int getStartY(int lines) { + if (alignment.y >= 0 && maxHeight > 0) { + float height = lines * getFontHeight() - scale; + if (alignment.y > 0) { + return (int) (y + maxHeight - height); + } else { + return (int) (y + (maxHeight - height) / 2f); + } + } + return y; + } + + protected int getStartX(float lineWidth) { + if (maxWidth > 0 && alignment.x >= 0) { + if (alignment.x > 0) { + return (int) (x + maxWidth - lineWidth); + } else { + return (int) (x + (maxWidth - lineWidth) / 2f); + } + } + return x; + } + + protected float draw(String text, float x, float y) { + if (simulate) { + return getFontRenderer().getStringWidth(text); + } + GlStateManager.disableBlend(); + GlStateManager.pushMatrix(); + GlStateManager.scale(scale, scale, 0f); + int width = getFontRenderer().drawString(text, (int)(x / scale), (int)(y / scale), color, shadow); + GlStateManager.popMatrix(); + GlStateManager.enableBlend(); + return width * scale; + } + + public float getFontHeight() { + return getFontRenderer().FONT_HEIGHT * scale; + } + + public float getLastHeight() { + return lastHeight; + } + + public float getLastWidth() { + return lastWidth; + } + + public Size getLastSize() { + return new Size(lastWidth, lastHeight); + } + + @SideOnly(Side.CLIENT) + public static FontRenderer getFontRenderer() { + return Minecraft.getMinecraft().fontRenderer; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/drawable/UITexture.java b/src/main/java/com/gtnewhorizons/modularui/api/drawable/UITexture.java new file mode 100644 index 0000000..2c85be8 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/drawable/UITexture.java @@ -0,0 +1,161 @@ +package com.gtnewhorizons.modularui.api.drawable; + +import com.google.gson.JsonObject; +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.api.math.GuiArea; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.common.internal.JsonHelper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL11; + +import java.util.HashMap; +import java.util.Map; + +public class UITexture implements IDrawable { + + public static final Map JSON_TEXTURES = new HashMap<>(); + + public static final UITexture DEFAULT = fullImage("gui/options_background"); + + public final ResourceLocation location; + public final float u0, v0, u1, v1; + + /** + * Creates a drawable texture + * + * @param location location of the texture + * @param u0 x offset of the image (0-1) + * @param v0 y offset of the image (0-1) + * @param u1 x end offset of the image (0-1) + * @param v1 y end offset of the image (0-1) + */ + public UITexture(ResourceLocation location, float u0, float v0, float u1, float v1) { + if (!location.getResourcePath().endsWith(".png")) { + location = new ResourceLocation(location.getResourceDomain(), location.getResourcePath() + ".png"); + } + if (!location.getResourcePath().startsWith("textures/")) { + location = new ResourceLocation(location.getResourceDomain(), "textures/" + location.getResourcePath()); + } + this.location = location; + this.u0 = u0; + this.v0 = v0; + this.u1 = u1; + this.v1 = v1; + } + + public static UITexture fullImage(ResourceLocation location) { + return new UITexture(location, 0, 0, 1, 1); + } + + public static UITexture fullImage(String location) { + return fullImage(new ResourceLocation(location)); + } + + public static UITexture fullImage(String mod, String location) { + return fullImage(new ResourceLocation(mod, location)); + } + + public static UITexture partly(ResourceLocation location, int imageWidth, int imageHeight, int u0, int v0, int u1, int v1) { + return new UITexture(location, u0 / (float) imageWidth, v0 / (float) imageHeight, u1 / (float) imageWidth, v1 / (float) imageHeight); + } + + public static UITexture partly(String location, int imageWidth, int imageHeight, int u0, int v0, int u1, int v1) { + return partly(new ResourceLocation(location), imageWidth, imageHeight, u0, v0, u1, v1); + } + + public static UITexture partly(String domain, String location, int imageWidth, int imageHeight, int u0, int v0, int u1, int v1) { + return partly(new ResourceLocation(domain, location), imageWidth, imageHeight, u0, v0, u1, v1); + } + + public UITexture getSubArea(GuiArea bounds) { + return getSubArea(bounds.x0, bounds.y0, bounds.x1, bounds.y1); + } + + /** + * Returns a texture with a sub area relative to this area texture + * + * @param uStart x offset of the image (0-1) + * @param vStart y offset of the image (0-1) + * @param uEnd x end offset of the image (0-1) + * @param vEnd y end offset of the image (0-1) + * @return relative sub area + */ + public UITexture getSubArea(float uStart, float vStart, float uEnd, float vEnd) { + return new UITexture(location, calcU(uStart), calcV(vStart), calcU(uEnd), calcV(vEnd)); + } + + public UITexture exposeToJson() { + if (JSON_TEXTURES.containsKey(location)) { + UITexture texture = JSON_TEXTURES.get(location); + ModularUI.logger.error("{} '{}' is already exposed to json with uv {}, {}, {}, {}!", texture.getClass().getSimpleName(), location, texture.u0, texture.v0, texture.u1, texture.v1); + } else { + JSON_TEXTURES.put(location, this); + } + return this; + } + + public ResourceLocation getLocation() { + return location; + } + + protected final float calcU(float uNew) { + return (u1 - u0) * uNew + u0; + } + + protected final float calcV(float vNew) { + return (v1 - v0) * vNew + v0; + } + + @Override + public void draw(float x, float y, float width, float height, float partialTicks) { + draw(x, y, width, height); + } + + public void draw(float x, float y, float width, float height) { + draw(location, x, y, width, height, u0, v0, u1, v1); + } + + public void drawSubArea(float x, float y, float width, float height, float uStart, float vStart, float uEnd, float vEnd) { + draw(location, x, y, width, height, calcU(uStart), calcV(vStart), calcU(uEnd), calcV(vEnd)); + } + + public static void draw(ResourceLocation location, float x0, float y0, float width, float height, float u0, float v0, float u1, float v1) { + float x1 = x0 + width, y1 = y0 + height; + Minecraft.getMinecraft().renderEngine.bindTexture(location); + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_ALPHA_TEST); + tessellator.setTextureUV(u0, v1); + tessellator.addVertex(x0, y1, 0.0f); + tessellator.setTextureUV(u1, v1); + tessellator.addVertex(x1, y1, 0.0f); + tessellator.setTextureUV(u1, v0); + tessellator.addVertex(x1, y0, 0.0f); + tessellator.setTextureUV(u0, v0); + tessellator.addVertex(x0, y0, 0.0f); + tessellator.draw(); + GL11.glEnable(GL11.GL_LIGHTING); + } + + public static UITexture ofJson(JsonObject json) { + if (!json.has("src")) { + return DEFAULT; + } + ResourceLocation rl = new ResourceLocation(json.get("src").getAsString()); + if (JSON_TEXTURES.containsKey(rl)) { + return JSON_TEXTURES.get(rl); + } + float u0 = JsonHelper.getFloat(json, 0, "u", "u0"), v0 = JsonHelper.getFloat(json, 0, "v", "v0"); + float u1 = JsonHelper.getFloat(json, 1, "u1"), v1 = JsonHelper.getFloat(json, 1, "v1"); + Size imageSize = JsonHelper.getElement(json, Size.ZERO, Size::ofJson, "imageSize"); + int borderWidth = JsonHelper.getInt(json, -1, "borderWidth"); + if (imageSize.width > 0 && imageSize.height > 0 && borderWidth >= 0) { + return AdaptableUITexture.of(rl, imageSize.width, imageSize.height, borderWidth).getSubArea(u0, v0, u1, v1); + } + return new UITexture(rl, u0, v0, u1, v1); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/drawable/shapes/Circle.java b/src/main/java/com/gtnewhorizons/modularui/api/drawable/shapes/Circle.java new file mode 100644 index 0000000..d9a6f93 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/drawable/shapes/Circle.java @@ -0,0 +1,78 @@ +package com.gtnewhorizons.modularui.api.drawable.shapes; + +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.math.Color; +import net.minecraft.client.renderer.Tessellator; +import org.jetbrains.annotations.Contract; +import org.lwjgl.opengl.GL11; + +public class Circle implements IDrawable { + + public static final double PI2 = Math.PI * 2; + + private int colorInner, colorOuter, segments; + + public Circle() { + this.colorInner = 0; + this.colorOuter = 0; + this.segments = 40; + } + + @Contract("_ -> this") + public Circle setColorInner(int colorInner) { + this.colorInner = colorInner; + return this; + } + + public Circle setColorOuter(int colorOuter) { + this.colorOuter = colorOuter; + return this; + } + + public Circle setColor(int inner, int outer) { + this.colorInner = inner; + this.colorOuter = outer; + return this; + } + + public Circle setSegments(int segments) { + this.segments = segments; + return this; + } + + @Override + public void applyThemeColor(int color) { + if (colorInner == 0 && colorOuter == 0) { + IDrawable.super.applyThemeColor(color == 0 ? 0xFFFFFFFF : color); + } + } + + @Override + public void draw(float x0, float y0, float width, float height, float partialTicks) { + GlStateManager.disableTexture2D(); + GlStateManager.enableBlend(); + GlStateManager.enableAlpha(); + GlStateManager.tryBlendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO); + GlStateManager.shadeModel(7425); + Tessellator tessellator = Tessellator.instance; + float x_2 = x0 + width / 2, y_2 = y0 + height / 2; + tessellator.startDrawing(GL11.GL_TRIANGLE_FAN); + tessellator.setColorRGBA(Color.getRed(colorInner), Color.getGreen(colorInner), Color.getBlue(colorInner), Color.getAlpha(colorInner)); + tessellator.addVertex(x_2, y_2, 0.0f); + float incr = (float) (PI2 / segments); + for (int i = 0; i <= segments; i++) { + float angle = incr * i; + float x = (float) (Math.sin(angle) * (width / 2) + x_2); + float y = (float) (Math.cos(angle) * (height / 2) + y_2); + tessellator.startDrawing(GL11.GL_TRIANGLE_FAN); + tessellator.setColorRGBA(Color.getRed(colorOuter), Color.getGreen(colorOuter), Color.getBlue(colorOuter), Color.getAlpha(colorOuter)); + tessellator.addVertex(x, y, 0.0f); + } + tessellator.draw(); + GlStateManager.shadeModel(7424); + GlStateManager.disableBlend(); + GlStateManager.enableAlpha(); + GlStateManager.enableTexture2D(); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/drawable/shapes/Rectangle.java b/src/main/java/com/gtnewhorizons/modularui/api/drawable/shapes/Rectangle.java new file mode 100644 index 0000000..ad492e8 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/drawable/shapes/Rectangle.java @@ -0,0 +1,130 @@ +package com.gtnewhorizons.modularui.api.drawable.shapes; + +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.math.Color; +import net.minecraft.client.renderer.Tessellator; +import org.lwjgl.opengl.GL11; + +public class Rectangle implements IDrawable { + + public static final double PI_2 = Math.PI / 2; + + private int cornerRadius, colorTL, colorTR, colorBL, colorBR, cornerSegments; + + public Rectangle() { + this.cornerRadius = 0; + this.colorTL = 0; + this.colorTR = 0; + this.colorBL = 0; + this.colorBR = 0; + this.cornerSegments = 6; + } + + public Rectangle setCornerRadius(int cornerRadius) { + this.cornerRadius = Math.max(0, cornerRadius); + return this; + } + + public Rectangle setColor(int colorTL, int colorTR, int colorBL, int colorBR) { + this.colorTL = colorTL; + this.colorTR = colorTR; + this.colorBL = colorBL; + this.colorBR = colorBR; + return this; + } + + public Rectangle setVerticalGradient(int colorTop, int colorBottom) { + return setColor(colorTop, colorTop, colorBottom, colorBottom); + } + + public Rectangle setHorizontalGradient(int colorLeft, int colorRight) { + return setColor(colorLeft, colorRight, colorLeft, colorRight); + } + + public Rectangle setColor(int color) { + return setColor(color, color, color, color); + } + + public Rectangle setCornerSegments(int cornerSegments) { + this.cornerSegments = cornerSegments; + return this; + } + + @Override + public void applyThemeColor(int color) { + if (colorTL == 0 && colorBL == 0 && colorBR == 0 && colorTR == 0) { + IDrawable.super.applyThemeColor(color == 0 ? 0xFFFFFFFF : color); + } + } + + @Override + public void draw(float x0, float y0, float width, float height, float partialTicks) { + GlStateManager.disableTexture2D(); + GlStateManager.enableBlend(); + GlStateManager.enableAlpha(); + GlStateManager.tryBlendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO); + GlStateManager.shadeModel(7425); + Tessellator tessellator = Tessellator.instance; + float x1 = x0 + width, y1 = y0 + height; + if (this.cornerRadius == 0) { + tessellator.startDrawingQuads(); + + tessellator.setColorRGBA_F(Color.getRed(colorTL), Color.getGreen(colorTL), Color.getBlue(colorTL), Color.getAlpha(colorTL)); + tessellator.addVertex(x0, y0, 0.0f); + tessellator.setColorRGBA_F(Color.getRed(colorBL), Color.getGreen(colorBL), Color.getBlue(colorBL), Color.getAlpha(colorBL)); + tessellator.addVertex(x0, y1, 0.0f); + tessellator.setColorRGBA_F(Color.getRed(colorBR), Color.getGreen(colorBR), Color.getBlue(colorBR), Color.getAlpha(colorBR)); + tessellator.addVertex(x1, y1, 0.0f); + tessellator.setColorRGBA_F(Color.getRed(colorTR), Color.getGreen(colorTR), Color.getBlue(colorTR), Color.getAlpha(colorTR)); + tessellator.addVertex(x1, y0, 0.0f); + } else { + tessellator.startDrawing(GL11.GL_TRIANGLE_FAN); + int color = Color.average(colorBL, colorBR, colorTR, colorTL); + tessellator.setColorRGBA_F(Color.getRed(color), Color.getGreen(color), Color.getBlue(color), Color.getAlpha(color)); + tessellator.addVertex(x0 + width / 2, y0 + height / 2, 0.0f); + tessellator.setColorRGBA_F(Color.getRed(colorTL), Color.getGreen(colorTL), Color.getBlue(colorTL), Color.getAlpha(colorTL)); + tessellator.addVertex(x0, y0 + cornerRadius, 0.0f); + tessellator.setColorRGBA_F(Color.getRed(colorBL), Color.getGreen(colorBL), Color.getBlue(colorBL), Color.getAlpha(colorBL)); + tessellator.addVertex(x0, y1 - cornerRadius, 0.0f); + int n = cornerSegments; + for (int i = 1; i <= n; i++) { + float x = (float) (x0 + cornerRadius - Math.cos(PI_2 / n * i) * cornerRadius); + float y = (float) (y1 - cornerRadius + Math.sin(PI_2 / n * i) * cornerRadius); + tessellator.setColorRGBA_F(Color.getRed(colorBL), Color.getGreen(colorBL), Color.getBlue(colorBL), Color.getAlpha(colorBL)); + tessellator.addVertex(x, y, 0.0f); + } + tessellator.setColorRGBA_F(Color.getRed(colorBR), Color.getGreen(colorBR), Color.getBlue(colorBR), Color.getAlpha(colorBR)); + tessellator.addVertex(x1 - cornerRadius, y1, 0.0f); + for (int i = 1; i <= n; i++) { + float x = (float) (x1 - cornerRadius + Math.sin(PI_2 / n * i) * cornerRadius); + float y = (float) (y1 - cornerRadius + Math.cos(PI_2 / n * i) * cornerRadius); + tessellator.setColorRGBA_F(Color.getRed(colorBR), Color.getGreen(colorBR), Color.getBlue(colorBR), Color.getAlpha(colorBR)); + tessellator.addVertex(x, y, 0.0f); + } + tessellator.setColorRGBA_F(Color.getRed(colorTR), Color.getGreen(colorTR), Color.getBlue(colorTR), Color.getAlpha(colorTR)); + tessellator.addVertex(x1, y0 + cornerRadius, 0.0f); + for (int i = 1; i <= n; i++) { + float x = (float) (x1 - cornerRadius + Math.cos(PI_2 / n * i) * cornerRadius); + float y = (float) (y0 + cornerRadius - Math.sin(PI_2 / n * i) * cornerRadius); + tessellator.setColorRGBA_F(Color.getRed(colorTR), Color.getGreen(colorTR), Color.getBlue(colorTR), Color.getAlpha(colorTR)); + tessellator.addVertex(x, y, 0.0f); + } + tessellator.setColorRGBA_F(Color.getRed(colorTL), Color.getGreen(colorTL), Color.getBlue(colorTL), Color.getAlpha(colorTL)); + tessellator.addVertex(x0 + cornerRadius, y0, 0.0f); + for (int i = 1; i <= n; i++) { + float x = (float) (x0 + cornerRadius - Math.sin(PI_2 / n * i) * cornerRadius); + float y = (float) (y0 + cornerRadius - Math.cos(PI_2 / n * i) * cornerRadius); + tessellator.setColorRGBA_F(Color.getRed(colorTL), Color.getGreen(colorTL), Color.getBlue(colorTL), Color.getAlpha(colorTL)); + tessellator.addVertex(x, y, 0.0f); + } + tessellator.setColorRGBA_F(Color.getRed(colorTL), Color.getGreen(colorTL), Color.getBlue(colorTL), Color.getAlpha(colorTL)); + tessellator.addVertex(x0, y0 + cornerRadius, 0.0f); + } + tessellator.draw(); + GlStateManager.shadeModel(7424); + GlStateManager.disableBlend(); + GlStateManager.enableAlpha(); + GlStateManager.enableTexture2D(); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/forge/CraftingHelper.java b/src/main/java/com/gtnewhorizons/modularui/api/forge/CraftingHelper.java new file mode 100644 index 0000000..4a07723 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/forge/CraftingHelper.java @@ -0,0 +1,117 @@ +package com.gtnewhorizons.modularui.api.forge; + + +import com.gtnewhorizons.modularui.ModularUI; +import cpw.mods.fml.common.ModContainer; +import net.minecraft.item.crafting.CraftingManager; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class CraftingHelper { + + private static final boolean DEBUG_LOAD_MINECRAFT = false; + + public static boolean findFiles(ModContainer mod, String base, Function preprocessor, BiFunction processor, + boolean defaultUnfoundRoot, boolean visitAllFiles) { + + File source = mod.getSource(); + + if ("minecraft".equals(mod.getModId())) + { + if (!DEBUG_LOAD_MINECRAFT) + return true; + + try + { + URI tmp = CraftingManager.class.getResource("/assets/.mcassetsroot").toURI(); + source = new File(tmp.resolve("..").getPath()); + } + catch (URISyntaxException e) + { + ModularUI.logger.error("Error finding Minecraft jar: ", e); + return false; + } + } + + FileSystem fs = null; + boolean success = true; + + try + { + Path root = null; + + if (source.isFile()) + { + try + { + fs = FileSystems.newFileSystem(source.toPath(), null); + root = fs.getPath("/" + base); + } + catch (IOException e) + { + ModularUI.logger.error("Error loading FileSystem from jar: ", e); + return false; + } + } + else if (source.isDirectory()) + { + root = source.toPath().resolve(base); + } + + if (root == null || !Files.exists(root)) + return defaultUnfoundRoot; + + if (preprocessor != null) + { + Boolean cont = preprocessor.apply(root); + if (cont == null || !cont.booleanValue()) + return false; + } + + if (processor != null) + { + Iterator itr = null; + try + { + itr = Files.walk(root).iterator(); + } + catch (IOException e) + { + ModularUI.logger.error("Error iterating filesystem for: {}", mod.getModId(), e); + return false; + } + + while (itr != null && itr.hasNext()) + { + Boolean cont = processor.apply(root, itr.next()); + + if (visitAllFiles) + { + success &= cont != null && cont; + } + else if (cont == null || !cont) + { + return false; + } + } + } + } + finally + { + IOUtils.closeQuietly(fs); + } + + return success; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/forge/IItemHandler.java b/src/main/java/com/gtnewhorizons/modularui/api/forge/IItemHandler.java new file mode 100644 index 0000000..a105c86 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/forge/IItemHandler.java @@ -0,0 +1,24 @@ +package com.gtnewhorizons.modularui.api.forge; +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +import javax.annotation.Nonnull; +import net.minecraft.item.ItemStack; + +public interface IItemHandler { + int getSlots(); + + ItemStack getStackInSlot(int var1); + + ItemStack insertItem(int var1, ItemStack var2, boolean var3); + + ItemStack extractItem(int var1, int var2, boolean var3); + + int getSlotLimit(int var1); + + default boolean isItemValid(int slot, ItemStack stack) { + return true; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/forge/IItemHandlerModifiable.java b/src/main/java/com/gtnewhorizons/modularui/api/forge/IItemHandlerModifiable.java new file mode 100644 index 0000000..e31a835 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/forge/IItemHandlerModifiable.java @@ -0,0 +1,12 @@ +package com.gtnewhorizons.modularui.api.forge; +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +import net.minecraft.item.ItemStack; + +public interface IItemHandlerModifiable extends IItemHandler { + void setStackInSlot(int var1, ItemStack var2); +} + diff --git a/src/main/java/com/gtnewhorizons/modularui/api/forge/INBTSerializable.java b/src/main/java/com/gtnewhorizons/modularui/api/forge/INBTSerializable.java new file mode 100644 index 0000000..eaf95cd --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/forge/INBTSerializable.java @@ -0,0 +1,14 @@ +package com.gtnewhorizons.modularui.api.forge; + +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +import net.minecraft.nbt.NBTBase; + +public interface INBTSerializable { + T serializeNBT(); + + void deserializeNBT(T var1); +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/forge/InvWrapper.java b/src/main/java/com/gtnewhorizons/modularui/api/forge/InvWrapper.java new file mode 100644 index 0000000..09bad32 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/forge/InvWrapper.java @@ -0,0 +1,190 @@ +package com.gtnewhorizons.modularui.api.forge; + +import net.minecraft.inventory.IInventory; +import net.minecraft.item.ItemStack; + +public class InvWrapper implements IItemHandlerModifiable { + private final IInventory inv; + + public InvWrapper(IInventory inv) + { + this.inv = inv; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + InvWrapper that = (InvWrapper) o; + + return getInv().equals(that.getInv()); + + } + + @Override + public int hashCode() + { + return getInv().hashCode(); + } + + @Override + public int getSlots() + { + return getInv().getSizeInventory(); + } + + @Override + public ItemStack getStackInSlot(int slot) + { + return getInv().getStackInSlot(slot); + } + + @Override + public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) + { + if (stack == null) + return null; + + ItemStack stackInSlot = getInv().getStackInSlot(slot); + + int m; + if (stackInSlot != null) + { + if (stackInSlot.stackSize >= Math.min(stackInSlot.getMaxStackSize(), getSlotLimit(slot))) + return stack; + + if (!ItemHandlerHelper.canItemStacksStack(stack, stackInSlot)) + return stack; + + if (!getInv().isItemValidForSlot(slot, stack)) + return stack; + + m = Math.min(stack.getMaxStackSize(), getSlotLimit(slot)) - stackInSlot.stackSize; + + if (stack.stackSize <= m) + { + if (!simulate) + { + ItemStack copy = stack.copy(); + copy.stackSize += stackInSlot.stackSize; + getInv().setInventorySlotContents(slot, copy); + getInv().markDirty(); + } + + return null; + } + else + { + // copy the stack to not modify the original one + stack = stack.copy(); + if (!simulate) + { + ItemStack copy = stack.splitStack(m); + copy.stackSize += stackInSlot.stackSize; + getInv().setInventorySlotContents(slot, copy); + getInv().markDirty(); + return stack; + } + else + { + stack.stackSize -= m; + return stack; + } + } + } + else + { + if (!getInv().isItemValidForSlot(slot, stack)) + return stack; + + m = Math.min(stack.getMaxStackSize(), getSlotLimit(slot)); + if (m < stack.stackSize) + { + // copy the stack to not modify the original one + stack = stack.copy(); + if (!simulate) + { + getInv().setInventorySlotContents(slot, stack.splitStack(m)); + getInv().markDirty(); + return stack; + } + else + { + stack.stackSize -= m; + return stack; + } + } + else + { + if (!simulate) + { + getInv().setInventorySlotContents(slot, stack); + getInv().markDirty(); + } + return null; + } + } + + } + + @Override + public ItemStack extractItem(int slot, int amount, boolean simulate) + { + if (amount == 0) + return null; + + ItemStack stackInSlot = getInv().getStackInSlot(slot); + + if (stackInSlot == null) + return null; + + if (simulate) + { + if (stackInSlot.stackSize < amount) + { + return stackInSlot.copy(); + } + else + { + ItemStack copy = stackInSlot.copy(); + copy.stackSize = amount; + return copy; + } + } + else + { + int m = Math.min(stackInSlot.stackSize, amount); + + ItemStack decrStackSize = getInv().decrStackSize(slot, m); + getInv().markDirty(); + return decrStackSize; + } + } + + @Override + public void setStackInSlot(int slot, ItemStack stack) + { + getInv().setInventorySlotContents(slot, stack); + } + + @Override + public int getSlotLimit(int slot) + { + return getInv().getInventoryStackLimit(); + } + + @Override + public boolean isItemValid(int slot, ItemStack stack) + { + return getInv().isItemValidForSlot(slot, stack); + } + + public IInventory getInv() + { + return inv; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/forge/ItemHandlerHelper.java b/src/main/java/com/gtnewhorizons/modularui/api/forge/ItemHandlerHelper.java new file mode 100644 index 0000000..47574f7 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/forge/ItemHandlerHelper.java @@ -0,0 +1,158 @@ +package com.gtnewhorizons.modularui.api.forge; +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +import javax.annotation.Nullable; + +import net.minecraft.entity.item.EntityItem; +import net.minecraft.entity.player.EntityPlayer; +//import net.minecraft.init.SoundEvents; +import net.minecraft.item.ItemStack; +import net.minecraft.util.MathHelper; +import net.minecraft.world.World; + +public class ItemHandlerHelper { + + public ItemHandlerHelper() { + } + + public static ItemStack insertItem(IItemHandler dest, ItemStack stack, boolean simulate) { + if (dest != null && stack != null) { + for(int i = 0; i < dest.getSlots(); ++i) { + stack = dest.insertItem(i, stack, simulate); + if (stack == null) { + return null; + } + } + + return stack; + } else { + return stack; + } + } + + public static boolean canItemStacksStack(ItemStack a, ItemStack b) { + if (a != null && a.isItemEqual(b) && a.hasTagCompound() == b.hasTagCompound()) { + return (!a.hasTagCompound() || a.getTagCompound().equals(b.getTagCompound())); + } else { + return false; + } + } + + public static boolean canItemStacksStackRelaxed(ItemStack a, ItemStack b) { + if (a != null && b != null && a.getItem() == b.getItem()) { + if (!a.isStackable()) { + return false; + } else if (a.getHasSubtypes() && a.getItemDamage() != b.getItemDamage()) { + return false; + } else if (a.hasTagCompound() != b.hasTagCompound()) { + return false; + } else { + return (!a.hasTagCompound() || a.getTagCompound().equals(b.getTagCompound())); + } + } else { + return false; + } + } + + public static ItemStack copyStackWithSize(ItemStack itemStack, int size) { + if (size == 0) { + return null; + } else { + ItemStack copy = itemStack.copy(); + copy.stackSize = size; + return copy; + } + } + + public static ItemStack insertItemStacked(IItemHandler inventory, ItemStack stack, boolean simulate) { + if (inventory != null && stack != null) { + if (!stack.isStackable()) { + return insertItem(inventory, stack, simulate); + } else { + int sizeInventory = inventory.getSlots(); + + int i; + for(i = 0; i < sizeInventory; ++i) { + ItemStack slot = inventory.getStackInSlot(i); + if (canItemStacksStackRelaxed(slot, stack)) { + stack = inventory.insertItem(i, stack, simulate); + if (stack == null) { + break; + } + } + } + + if (stack != null) { + for(i = 0; i < sizeInventory; ++i) { + if (inventory.getStackInSlot(i) == null) { + stack = inventory.insertItem(i, stack, simulate); + if (stack == null) { + break; + } + } + } + } + + return stack; + } + } else { + return stack; + } + } + + public static void giveItemToPlayer(EntityPlayer player, ItemStack stack) { + giveItemToPlayer(player, stack, -1); + } + + public static void giveItemToPlayer(EntityPlayer player, ItemStack stack, int preferredSlot) { + if (stack != null) { + IItemHandler inventory = new PlayerMainInvWrapper(player.inventory); + World world = player.worldObj; + ItemStack remainder = stack; + if (preferredSlot >= 0 && preferredSlot < inventory.getSlots()) { + remainder = inventory.insertItem(preferredSlot, stack, false); + } + + if (remainder != null) { + remainder = insertItemStacked(inventory, remainder, false); + } + + if (remainder == null || remainder.stackSize != stack.stackSize) { + world.playSoundAtEntity(player, "random.pop", 0.2F, ((world.rand.nextFloat() - world.rand.nextFloat()) * 0.7F + 1.0F) * 2.0F); + } + + if (remainder != null && !world.isRemote) { + EntityItem entityitem = new EntityItem(world, player.posX, player.posY + 0.5D, player.posZ, remainder); + entityitem.delayBeforeCanPickup = 40; + entityitem.motionX = 0.0D; + entityitem.motionZ = 0.0D; + world.spawnEntityInWorld(entityitem); + } + + } + } + + public static int calcRedstoneFromInventory(@Nullable IItemHandler inv) { + if (inv == null) { + return 0; + } else { + int itemsFound = 0; + float proportion = 0.0F; + + for(int j = 0; j < inv.getSlots(); ++j) { + ItemStack itemstack = inv.getStackInSlot(j); + if (itemstack != null) { + proportion += (float)itemstack.stackSize / (float)Math.min(inv.getSlotLimit(j), itemstack.getMaxStackSize()); + ++itemsFound; + } + } + + proportion /= (float)inv.getSlots(); + return MathHelper.floor_float(proportion * 14.0F) + (itemsFound > 0 ? 1 : 0); + } + } +} + diff --git a/src/main/java/com/gtnewhorizons/modularui/api/forge/ItemStackHandler.java b/src/main/java/com/gtnewhorizons/modularui/api/forge/ItemStackHandler.java new file mode 100644 index 0000000..1c0d992 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/forge/ItemStackHandler.java @@ -0,0 +1,181 @@ +package com.gtnewhorizons.modularui.api.forge; +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; + +import java.util.Arrays; +import java.util.List; + +public class ItemStackHandler implements IItemHandler, IItemHandlerModifiable, INBTSerializable { + protected List stacks; + + public ItemStackHandler() { + this(1); + } + + public ItemStackHandler(int size) { + ItemStack[] stacks = new ItemStack[size]; + Arrays.fill(stacks, null); + this.stacks = Arrays.asList(stacks); + } + + public ItemStackHandler(List stacks) { + this.stacks = stacks; + } + + public void setSize(int size) + { + ItemStack[] stacks = new ItemStack[size]; + Arrays.fill(stacks, null); + this.stacks = Arrays.asList(stacks); + } + + @Override + public void setStackInSlot(int slot, ItemStack stack) { + this.validateSlotIndex(slot); + this.stacks.set(slot, stack); + this.onContentsChanged(slot); + } + + @Override + public int getSlots() { + return this.stacks.size(); + } + + @Override + public ItemStack getStackInSlot(int slot) { + this.validateSlotIndex(slot); + return (ItemStack)this.stacks.get(slot); + } + + @Override + public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) { + if (stack == null) { + return null; + } else { + this.validateSlotIndex(slot); + ItemStack existing = (ItemStack)this.stacks.get(slot); + int limit = this.getStackLimit(slot, stack); + if (existing != null) { + if (!ItemHandlerHelper.canItemStacksStack(stack, existing)) { + return stack; + } + + limit -= existing.stackSize; + } + + if (limit <= 0) { + return stack; + } else { + boolean reachedLimit = stack.stackSize > limit; + if (!simulate) { + if (existing == null) { + this.stacks.set(slot, reachedLimit ? ItemHandlerHelper.copyStackWithSize(stack, limit) : stack); + } else { + existing.stackSize += reachedLimit ? limit : stack.stackSize; + } + + this.onContentsChanged(slot); + } + + return reachedLimit ? ItemHandlerHelper.copyStackWithSize(stack, stack.stackSize - limit) : null; + } + } + } + + @Override + public ItemStack extractItem(int slot, int amount, boolean simulate) { + if (amount == 0) { + return null; + } else { + this.validateSlotIndex(slot); + ItemStack existing = (ItemStack)this.stacks.get(slot); + if (existing == null) { + return null; + } else { + int toExtract = Math.min(amount, existing.getMaxStackSize()); + if (existing.stackSize <= toExtract) { + if (!simulate) { + this.stacks.set(slot, null); + this.onContentsChanged(slot); + } + + return existing; + } else { + if (!simulate) { + this.stacks.set(slot, ItemHandlerHelper.copyStackWithSize(existing, existing.stackSize - toExtract)); + this.onContentsChanged(slot); + } + + return ItemHandlerHelper.copyStackWithSize(existing, toExtract); + } + } + } + } + + @Override + public int getSlotLimit(int slot) { + return 64; + } + + protected int getStackLimit(int slot, ItemStack stack) { + return Math.min(this.getSlotLimit(slot), stack.getMaxStackSize()); + } + + @Override + public boolean isItemValid(int slot, ItemStack stack) { + return true; + } + + @Override + public NBTTagCompound serializeNBT() { + NBTTagList nbtTagList = new NBTTagList(); + + for(int i = 0; i < this.stacks.size(); ++i) { + if ((ItemStack)this.stacks.get(i) != null) { + NBTTagCompound itemTag = new NBTTagCompound(); + itemTag.setInteger("Slot", i); + ((ItemStack)this.stacks.get(i)).writeToNBT(itemTag); + nbtTagList.appendTag(itemTag); + } + } + + NBTTagCompound nbt = new NBTTagCompound(); + nbt.setTag("Items", nbtTagList); + nbt.setInteger("Size", this.stacks.size()); + return nbt; + } + + @Override + public void deserializeNBT(NBTTagCompound nbt) { + this.setSize(nbt.hasKey("Size", 3) ? nbt.getInteger("Size") : this.stacks.size()); + NBTTagList tagList = nbt.getTagList("Items", 10); + + for(int i = 0; i < tagList.tagCount(); ++i) { + NBTTagCompound itemTags = tagList.getCompoundTagAt(i); + int slot = itemTags.getInteger("Slot"); + if (slot >= 0 && slot < this.stacks.size()) { +// this.stacks.set(slot, new ItemStack(itemTags)); + } + } + + this.onLoad(); + } + + protected void validateSlotIndex(int slot) { + if (slot < 0 || slot >= this.stacks.size()) { + throw new RuntimeException("Slot " + slot + " not in valid range - [0," + this.stacks.size() + ")"); + } + } + + protected void onLoad() { + } + + protected void onContentsChanged(int slot) { + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/forge/PlayerMainInvWrapper.java b/src/main/java/com/gtnewhorizons/modularui/api/forge/PlayerMainInvWrapper.java new file mode 100644 index 0000000..055339d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/forge/PlayerMainInvWrapper.java @@ -0,0 +1,46 @@ +package com.gtnewhorizons.modularui.api.forge; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.item.ItemStack; + +/** + * Exposes the player inventory WITHOUT the armor inventory as IItemHandler. + * Also takes core of inserting/extracting having the same logic as picking up items. + */ +public class PlayerMainInvWrapper extends RangedWrapper { + private final InventoryPlayer inventoryPlayer; + + public PlayerMainInvWrapper(InventoryPlayer inv) + { + super(new InvWrapper(inv), 0, inv.mainInventory.length); + inventoryPlayer = inv; + } + + @Override + public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) + { + ItemStack rest = super.insertItem(slot, stack, simulate); + if (rest.stackSize != stack.stackSize) + { + // the stack in the slot changed, animate it + ItemStack inSlot = getStackInSlot(slot); + if(inSlot != null) + { + if (getInventoryPlayer().player.worldObj.isRemote) + { + inSlot.animationsToGo = 5; + } + else if(getInventoryPlayer().player instanceof EntityPlayerMP) { + getInventoryPlayer().player.openContainer.detectAndSendChanges(); + } + } + } + return rest; + } + + public InventoryPlayer getInventoryPlayer() + { + return inventoryPlayer; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/forge/RangedWrapper.java b/src/main/java/com/gtnewhorizons/modularui/api/forge/RangedWrapper.java new file mode 100644 index 0000000..c49cc7f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/forge/RangedWrapper.java @@ -0,0 +1,98 @@ +package com.gtnewhorizons.modularui.api.forge; + +import com.google.common.base.Preconditions; +import net.minecraft.item.ItemStack; + +/** + * A wrapper that composes another IItemHandlerModifiable, exposing only a range of the composed slots. + * Shifting of slot indices is handled automatically for you. + */ +public class RangedWrapper implements IItemHandlerModifiable { + + private final IItemHandlerModifiable compose; + private final int minSlot; + private final int maxSlot; + + public RangedWrapper(IItemHandlerModifiable compose, int minSlot, int maxSlotExclusive) + { + Preconditions.checkArgument(maxSlotExclusive > minSlot, "Max slot must be greater than min slot"); + this.compose = compose; + this.minSlot = minSlot; + this.maxSlot = maxSlotExclusive; + } + + @Override + public int getSlots() + { + return maxSlot - minSlot; + } + + @Override + public ItemStack getStackInSlot(int slot) + { + if (checkSlot(slot)) + { + return compose.getStackInSlot(slot + minSlot); + } + + return null; + } + + @Override + public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) + { + if (checkSlot(slot)) + { + return compose.insertItem(slot + minSlot, stack, simulate); + } + + return stack; + } + + @Override + public ItemStack extractItem(int slot, int amount, boolean simulate) + { + if (checkSlot(slot)) + { + return compose.extractItem(slot + minSlot, amount, simulate); + } + + return null; + } + + @Override + public void setStackInSlot(int slot, ItemStack stack) + { + if (checkSlot(slot)) + { + compose.setStackInSlot(slot + minSlot, stack); + } + } + + @Override + public int getSlotLimit(int slot) + { + if (checkSlot(slot)) + { + return compose.getSlotLimit(slot + minSlot); + } + + return 0; + } + + @Override + public boolean isItemValid(int slot, ItemStack stack) + { + if (checkSlot(slot)) + { + return compose.isItemValid(slot + minSlot, stack); + } + + return false; + } + + private boolean checkSlot(int localSlot) + { + return localSlot + minSlot < maxSlot; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/forge/SlotItemHandler.java b/src/main/java/com/gtnewhorizons/modularui/api/forge/SlotItemHandler.java new file mode 100644 index 0000000..ba447dc --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/forge/SlotItemHandler.java @@ -0,0 +1,98 @@ +package com.gtnewhorizons.modularui.api.forge; +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +import javax.annotation.Nonnull; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.InventoryBasic; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; + +public class SlotItemHandler extends Slot { + private static IInventory emptyInventory = new InventoryBasic("[Null]", true, 0); + private final IItemHandler itemHandler; + private final int index; + + public SlotItemHandler(IItemHandler itemHandler, int index, int xPosition, int yPosition) { + super(emptyInventory, index, xPosition, yPosition); + this.itemHandler = itemHandler; + this.index = index; + } + + public boolean isItemValid(ItemStack stack) { + if (stack!=null && this.itemHandler.isItemValid(this.index, stack)) { + IItemHandler handler = this.getItemHandler(); + ItemStack remainder; + if (handler instanceof IItemHandlerModifiable) { + IItemHandlerModifiable handlerModifiable = (IItemHandlerModifiable)handler; + ItemStack currentStack = handlerModifiable.getStackInSlot(this.index); + handlerModifiable.setStackInSlot(this.index, null); + remainder = handlerModifiable.insertItem(this.index, stack, true); + handlerModifiable.setStackInSlot(this.index, currentStack); + } else { + remainder = handler.insertItem(this.index, stack, true); + } + + return remainder.stackSize < stack.stackSize; + } else { + return false; + } + } + + public ItemStack getStack() { + return this.getItemHandler().getStackInSlot(this.index); + } + + public void putStack(ItemStack stack) { + ((IItemHandlerModifiable)this.getItemHandler()).setStackInSlot(this.index, stack); + this.onSlotChanged(); + } + + public void onSlotChange(ItemStack p_75220_1_, ItemStack p_75220_2_) { + } + + public int getSlotStackLimit() { + return this.itemHandler.getSlotLimit(this.index); + } + + public int getItemStackLimit(ItemStack stack) { + ItemStack maxAdd = stack.copy(); + int maxInput = stack.getMaxStackSize(); + maxAdd.stackSize = maxInput; + IItemHandler handler = this.getItemHandler(); + ItemStack currentStack = handler.getStackInSlot(this.index); + if (handler instanceof IItemHandlerModifiable) { + IItemHandlerModifiable handlerModifiable = (IItemHandlerModifiable)handler; + handlerModifiable.setStackInSlot(this.index, null); + ItemStack remainder = handlerModifiable.insertItem(this.index, maxAdd, true); + handlerModifiable.setStackInSlot(this.index, currentStack); + return maxInput - remainder.stackSize; + } else { + ItemStack remainder = handler.insertItem(this.index, maxAdd, true); + int current = currentStack.stackSize; + int added = maxInput - remainder.stackSize; + return current + added; + } + } + + public boolean canTakeStack(EntityPlayer playerIn) { + return this.getItemHandler().extractItem(this.index, 1, true) != null; + } + + @Nonnull + public ItemStack decrStackSize(int amount) { + return this.getItemHandler().extractItem(this.index, amount, false); + } + + public IItemHandler getItemHandler() { + return this.itemHandler; + } + + public boolean isSameInventory(Slot other) { + return other instanceof SlotItemHandler && ((SlotItemHandler)other).getItemHandler() == this.itemHandler; + } +} + diff --git a/src/main/java/com/gtnewhorizons/modularui/api/math/Alignment.java b/src/main/java/com/gtnewhorizons/modularui/api/math/Alignment.java new file mode 100644 index 0000000..8147586 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/math/Alignment.java @@ -0,0 +1,37 @@ +package com.gtnewhorizons.modularui.api.math; + +public class Alignment { + + public final int x, y; + + public static final Alignment TopLeft = new Alignment(-1, -1); + public static final Alignment TopCenter = new Alignment(0, -1); + public static final Alignment TopRight = new Alignment(1, -1); + public static final Alignment CenterLeft = new Alignment(-1, 0); + public static final Alignment Center = new Alignment(0, 0); + public static final Alignment CenterRight = new Alignment(1, 0); + public static final Alignment BottomLeft = new Alignment(-1, 1); + public static final Alignment BottomCenter = new Alignment(0, 1); + public static final Alignment BottomRight = new Alignment(1, 1); + + public static final Alignment[] ALL = { + TopLeft, TopCenter, TopRight, + CenterLeft, Center, CenterRight, + BottomLeft, BottomCenter, BottomRight + }; + + public static final Alignment[] CORNERS = { + TopLeft, TopRight, + BottomLeft, BottomRight + }; + + public Alignment(int x, int y) { + this.x = x; + this.y = y; + } + + public Pos2d getAlignedPos(Size parent, Size child) { + float x = (this.x + 1) * 1f / 2, y = (this.y + 1) * 1f / 2; + return new Pos2d(parent.width * x - child.width * x, parent.height * y - child.height * y); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/math/Color.java b/src/main/java/com/gtnewhorizons/modularui/api/math/Color.java new file mode 100644 index 0000000..1ff6c95 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/math/Color.java @@ -0,0 +1,464 @@ +package com.gtnewhorizons.modularui.api.math; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.common.internal.JsonHelper; +import org.jetbrains.annotations.NotNull; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.util.MathHelper; + +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.Iterator; + +public class Color implements Iterable { + + /** + * Creates a color int. All values should be 0 - 255 + */ + public static int rgb(int red, int green, int blue) { + return argb(red, green, blue, 255); + } + + /** + * Creates a color int. All values should be 0 - 255 + */ + public static int argb(int red, int green, int blue, int alpha) { + return ((alpha & 0xFF) << 24) | ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | ((blue & 0xFF)); + } + + /** + * Creates a color int. All values should be 0 - 1 + */ + public static int argb(float red, float green, float blue, float alpha) { + return argb((int) (red * 255), (int) (green * 255), (int) (blue * 255), (int) (alpha * 255)); + } + + /** + * Creates a color int. All values should be 0 - 255 + */ + public static int rgba(int red, int green, int blue, int alpha) { + return ((red & 0xFF) << 24) | ((green & 0xFF) << 16) | ((blue & 0xFF) << 8) | (alpha & 0xFF); + } + + /** + * Creates a color int. All values should be 0 - 1 + */ + public static int rgba(float red, float green, float blue, float alpha) { + return rgba((int) (red * 255), (int) (green * 255), (int) (blue * 255), (int) (alpha * 255)); + } + + /** + * Creates a color int. All values should be 0 - 1 + */ + public static int rgb(float red, float green, float blue) { + return argb(red, green, blue, 1f); + } + + /** + * Converts a HSV color to rgba + * + * @param hue value from 0 to 360 + * @param saturation value from 0 to 1 + * @param value value from 0 to 1 + * @param alpha value from 0 to 1 + * @return the color + */ + public static int ofHSV(int hue, float saturation, float value, float alpha) { + hue = Math.max(0, Math.min(hue, 360)); + + float c = value * saturation; + float x = c * (1 - (((hue / 60f) % 2) - 1)); + x = Math.max(x, -x); + float m = value - c; + float r, g, b; + if (hue < 60) { + r = c; + g = x; + b = 0; + } else if (hue < 120) { + r = x; + g = c; + b = 0; + } else if (hue < 180) { + r = 0; + g = c; + b = x; + } else if (hue < 240) { + r = 0; + g = x; + b = c; + } else if (hue < 300) { + r = x; + g = 0; + b = c; + } else { + r = c; + g = 0; + b = x; + } + return argb(r + m, g + m, b + m, alpha); + } + + public static int withRed(int argb, int red) { + argb &= ~(0xFF << 16); + return argb | red << 16; + } + + public static int withGreen(int argb, int green) { + argb &= ~(0xFF << 8); + return argb | green << 8; + } + + public static int withBlue(int argb, int blue) { + argb &= ~0xFF; + return argb | blue; + } + + public static int withAlpha(int argb, int alpha) { + argb &= ~(0xFF << 24); + return argb | alpha << 24; + } + + public static int withRed(int argb, float red) { + return withRed(argb, (int) (red * 255)); + } + + public static int withGreen(int argb, float green) { + return withGreen(argb, (int) (green * 255)); + } + + public static int withBlue(int argb, float blue) { + return withBlue(argb, (int) (blue * 255)); + } + + public static int withAlpha(int argb, float alpha) { + return withAlpha(argb, (int) (alpha * 255)); + } + + /** + * @return the red value + */ + public static int getRed(int argb) { + return argb >> 16 & 255; + } + + /** + * @return the green value + */ + public static int getGreen(int argb) { + return argb >> 8 & 255; + } + + /** + * @return the blue value + */ + public static int getBlue(int argb) { + return argb & 255; + } + + /** + * @return the alpha value + */ + public static int getAlpha(int argb) { + return argb >> 24 & 255; + } + + /** + * @return the red value + */ + public static float getRedF(int argb) { + return getRed(argb) / 255f; + } + + /** + * @return the green value + */ + public static float getGreenF(int argb) { + return getGreen(argb) / 255f; + } + + /** + * @return the blue value + */ + public static float getBlueF(int argb) { + return getBlue(argb) / 255f; + } + + /** + * @return the alpha value + */ + public static float getAlphaF(int argb) { + return getAlpha(argb) / 255f; + } + + /** + * @return rgba as an array [red, green, blue, alpha] + */ + public static int[] getValues(int argb) { + return new int[]{getRed(argb), getGreen(argb), getBlue(argb), getAlpha(argb)}; + } + + public static int rgbaToArgb(int rgba) { + return Color.argb(getAlpha(rgba), getRed(rgba), getGreen(rgba), getBlue(rgba)); + } + + public static int argbToRgba(int argb) { + return Color.rgba(getRed(argb), getGreen(argb), getBlue(argb), getAlpha(argb)); + } + + public static int invert(int argb) { + return Color.argb(255 - getRed(argb), 255 - getGreen(argb), 255 - getBlue(argb), getAlpha(argb)); + } + + public static int average(int... colors) { + int r = 0, g = 0, b = 0, a = 0; + for (int color : colors) { + r += getRed(color); + g += getGreen(color); + b += getBlue(color); + a += getAlpha(color); + } + return argb(r / colors.length, g / colors.length, b / colors.length, a / colors.length); + } + + public static int interpolate(int color1, int color2, double value) { + value = MathHelper.clamp_double(value, 0, 1); + int r = (int) ((Color.getRed(color2) - Color.getRed(color1)) * value + Color.getRed(color1)); + int g = (int) ((Color.getGreen(color2) - Color.getGreen(color1)) * value + Color.getGreen(color1)); + int b = (int) ((Color.getBlue(color2) - Color.getBlue(color1)) * value + Color.getBlue(color1)); + int a = (int) ((Color.getAlpha(color2) - Color.getAlpha(color1)) * value + Color.getAlpha(color1)); + return Color.argb(r, g, b, a); + } + + /** + * A helper method to apply a color to rendering. + * If the alpha is 0 and any other value is not null, the alpha will be set to max. + * + * @param color argb color + */ + @SideOnly(Side.CLIENT) + public static void setGlColor(int color) { + if (color == 0) { + GlStateManager.color(0, 0, 0, 0); + return; + } + float a = getAlphaF(color); + float r = getRedF(color); + float g = getGreenF(color); + float b = getBlueF(color); + if (a == 0) a = 1f; + GlStateManager.color(r, g, b, a); + } + + @Nullable + public static Integer ofJson(JsonElement jsonElement) { + if (jsonElement.isJsonPrimitive()) { + return jsonElement.getAsInt(); + } + if (jsonElement.isJsonArray()) { + return null; + } + if (jsonElement.isJsonObject()) { + JsonObject json = jsonElement.getAsJsonObject(); + int red = JsonHelper.getInt(json, 255, "r", "red"); + int green = JsonHelper.getInt(json, 255, "g", "green"); + int blue = JsonHelper.getInt(json, 255, "b", "blue"); + int alpha = JsonHelper.getInt(json, 255, "a", "alpha"); + return Color.argb(red, green, blue, alpha); + } + String string = jsonElement.getAsString(); + if (string.startsWith("#")) { + string = string.substring(1); + } else if (string.startsWith("0x")) { + string = string.substring(2); + } + try { + return Integer.parseInt(string, 16); + } catch (NumberFormatException e) { + ModularUI.logger.error("Error parsing json color {}", jsonElement); + } + return null; + } + + public static final Color WHITE = new Color(0xFFFFFF, new int[]{}, + 0xF7F7F7, + 0xEFEFEF, + 0xE7E7E7, + 0xDFDFDF, + 0xD7D7D7, + 0xCFCFCF, + 0xC7C7C7, + 0xBFBFBF); + + public static final Color BLACK = new Color(0x000000, new int[]{ + 0x080808, + 0x101010, + 0x181818, + 0x202020, + 0x282828, + 0x303030, + 0x383838, + 0x404040 + }); + + // from here on color values are taken from https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/colors.dart + + public static final Color RED = new Color(0xF44336, + new int[]{0xEF5350, 0xE57373, 0xEF9A9A, 0xFFCDD2, 0xFFEBEE}, + 0xE53935, 0xD32F2F, 0xC62828, 0xB71C1C); + + public static final Color RED_ACCENT = new Color(0xFF5252, new int[]{0xFF8A80}, 0xFF1744, 0xD50000); + + public static final Color PINK = new Color(0xE91E63, + new int[]{0xEC407A, 0xF06292, 0xF48FB1, 0xF8BBD0, 0xFCE4EC}, + 0xD81B60, 0xC2185B, 0xAD1457, 0x880E4F); + + public static final Color PINK_ACCENT = new Color(0xFF4081, new int[]{0xFF80AB}, 0xF50057, 0xC51162); + + public static final Color PURPLE = new Color(0x9C27B0, + new int[]{0xAB47BC, 0xBA68C8, 0xCE93D8, 0xE1BEE7, 0xF3E5F5}, + 0x8E24AA, 0x7B1FA2, 0x6A1B9A, 0x4A148C); + + public static final Color PURPLE_ACCENT = new Color(0xE040FB, new int[]{0xEA80FC}, 0xD500F9, 0xAA00FF); + + public static final Color DEEP_PURPLE = new Color(0x673AB7, + new int[]{0x7E57C2, 0x9575CD, 0xB39DDB, 0xD1C4E9, 0xEDE7F6}, + 0x5E35B1, 0x512DA8, 0x4527A0, 0x311B92); + + public static final Color DEEP_PURPLE_ACCENT = new Color(0x7C4DFF, new int[]{0xB388FF}, 0x651FFF, 0x651FFF); + + public static final Color INDIGO = new Color(0x3F51B5, + new int[]{0x5C6BC0, 0x7986CB, 0x9FA8DA, 0xC5CAE9, 0xE8EAF6}, + 0x3949AB, 0x303F9F, 0x283593, 0x1A237E); + + public static final Color INDIGO_ACCENT = new Color(0x536DFE, new int[]{0x8C9EFF}, 0x3D5AFE, 0x304FFE); + + public static final Color BLUE = new Color(0x2196F3, + new int[]{0x42A5F5, 0x64B5F6, 0x90CAF9, 0xBBDEFB, 0xE3F2FD}, + 0x1E88E5, 0x1976D2, 0x1565C0, 0x0D47A1); + + public static final Color BLUE_ACCENT = new Color(0x448AFF, new int[]{0x82B1FF}, 0x2979FF, 0x2962FF); + + public static final Color LIGHT_BLUE = new Color(0x03A9F4, + new int[]{0x29B6F6, 0x4FC3F7, 0x81D4FA, 0xB3E5FC, 0xE1F5FE}, + 0x039BE5, 0x0288D1, 0x0277BD, 0x01579B); + + public static final Color LIGHT_BLUE_ACCENT = new Color(0x40C4FF, new int[]{0x80D8FF}, 0x00B0FF, 0x0091EA); + + public static final Color CYAN = new Color(0x00BCD4, + new int[]{0x26C6DA, 0x4DD0E1, 0x80DEEA, 0xB2EBF2, 0xE0F7FA}, + 0x00ACC1, 0x0097A7, 0x00838F, 0x006064); + + public static final Color CYAN_ACCENT = new Color(0x18FFFF, new int[]{0x84FFFF}, 0x00E5FF, 0x00B8D4); + + public static final Color TEAL = new Color(0x009688, + new int[]{0x26A69A, 0x4DB6AC, 0x80CBC4, 0xB2DFDB, 0xE0F2F1}, + 0x00897B, 0x00796B, 0x00695C, 0x004D40); + + public static final Color TEAL_ACCENT = new Color(0x64FFDA, new int[]{0xA7FFEB}, 0x1DE9B6, 0x00BFA5); + + public static final Color GREEN = new Color(0x4CAF50, + new int[]{0x66BB6A, 0x81C784, 0xA5D6A7, 0xC8E6C9, 0xE8F5E9}, + 0x43A047, 0x388E3C, 0x2E7D32, 0x1B5E20); + + public static final Color GREEN_ACCENT = new Color(0x69F0AE, new int[]{0xB9F6CA}, 0x00E676, 0x00C853); + + public static final Color LIGHT_GREEN = new Color(0x8BC34A, + new int[]{0x9CCC65, 0xAED581, 0xC5E1A5, 0xDCEDC8, 0xF1F8E9}, + 0x7CB342, 0x689F38, 0x558B2F, 0x33691E); + + public static final Color LIGHT_GREEN_ACCENT = new Color(0xB2FF59, new int[]{0xCCFF90}, 0x76FF03, 0x64DD17); + + public static final Color LIME = new Color(0xCDDC39, + new int[]{0xD4E157, 0xDCE775, 0xE6EE9C, 0xF0F4C3, 0xF9FBE7}, + 0xC0CA33, 0xAFB42B, 0x9E9D24, 0x827717); + + public static final Color LIME_ACCENT = new Color(0xEEFF41, new int[]{0xF4FF81}, 0xC6FF00, 0xAEEA00); + + public static final Color YELLOW = new Color(0xFFEB3B, + new int[]{0xFFEE58, 0xFFF176, 0xFFF59D, 0xFFF9C4, 0xFFFDE7}, + 0xFDD835, 0xFBC02D, 0xF9A825, 0xF57F17); + + public static final Color YELLOW_ACCENT = new Color(0xFFFF00, new int[]{0xFFFF8D}, 0xFFEA00, 0xFFD600); + + public static final Color AMBER = new Color(0xFFC107, + new int[]{0xFFCA28, 0xFFD54F, 0xFFE082, 0xFFECB3, 0xFFF8E1}, + 0xFFB300, 0xFFA000, 0xFF8F00, 0xFF6F00); + + public static final Color AMBER_ACCENT = new Color(0xFFD740, new int[]{0xFFE57F}, 0xFFC400, 0xFFAB00); + + public static final Color ORANGE = new Color(0xFF9800, + new int[]{0xFFA726, 0xFFB74D, 0xFFCC80, 0xFFE0B2, 0xFFFFF3E0}, + 0xFB8C00, 0xF57C00, 0xEF6C00, 0xE65100); + + public static final Color ORANGE_ACCENT = new Color(0xFFAB40, new int[]{0xFFD180}, 0xFF9100, 0xFF6D00); + + public static final Color DEEP_ORANGE = new Color(0xFF5722, + new int[]{0xFF7043, 0xFF8A65, 0xFFAB91, 0xFFCCBC, 0xFBE9E7}, + 0xF4511E, 0xE64A19, 0xD84315, 0xBF360C); + + public static final Color DEEP_ORANGE_ACCENT = new Color(0xFF6E40, new int[]{0xFF9E80}, 0xFF3D00, 0xDD2C00); + + public static final Color BROWN = new Color(0x795548, + new int[]{0x8D6E63, 0xA1887F, 0xBCAAA4, 0xD7CCC8, 0xEFEBE9}, + 0x6D4C41, 0x5D4037, 0x4E342E, 0x3E2723); + + public static final Color GREY = new Color(0x9E9E9E, + new int[]{0xBDBDBD, 0xE0E0E0, 0xEEEEEE, 0xF5F5F5, 0xFAFAFA}, + 0x757575, 0x616161, 0x424242, 0x212121); + + public static final Color BLUE_GREY = new Color(0x607D8B, + new int[]{0x78909C, 0x90A4AE, 0xB0BEC5, 0xCFD8DC, 0xECEFF1}, + 0x546E7A, 0x455A64, 0x37474F, 0x263238); + + public final int normal; + private final int[] shadeBright; + private final int[] shadeDark; + private final int[] all; + + public Color(int normal, int[] shadeBright, int... shadeDark) { + this.normal = withAlpha(normal, 255); + this.shadeBright = shadeBright; + this.shadeDark = shadeDark; + for (int i = 0; i < this.shadeBright.length; i++) { + this.shadeBright[i] = withAlpha(this.shadeBright[i], 255); + } + for (int i = 0; i < this.shadeDark.length; i++) { + this.shadeDark[i] = withAlpha(this.shadeDark[i], 255); + } + this.all = new int[shadeBright.length + shadeDark.length + 1]; + int index = 0; + for (int i = shadeBright.length - 1; i >= 0; i--) { + all[index++] = shadeBright[i]; + } + all[index++] = normal; + for (int shade : shadeDark) { + all[index++] = shade; + } + } + + /** + * @param index See actual instances for valid index + */ + public int bright(int index) { + return shadeBright[index]; + } + + /** + * @param index See actual instances for valid index + */ + public int dark(int index) { + return shadeDark[index]; + } + + @NotNull + @Override + public Iterator iterator() { + return Arrays.stream(all).iterator(); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/math/CrossAxisAlignment.java b/src/main/java/com/gtnewhorizons/modularui/api/math/CrossAxisAlignment.java new file mode 100644 index 0000000..043aaf4 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/math/CrossAxisAlignment.java @@ -0,0 +1,5 @@ +package com.gtnewhorizons.modularui.api.math; + +public enum CrossAxisAlignment { + START, CENTER, END; +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/math/GuiArea.java b/src/main/java/com/gtnewhorizons/modularui/api/math/GuiArea.java new file mode 100644 index 0000000..484bafc --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/math/GuiArea.java @@ -0,0 +1,151 @@ +package com.gtnewhorizons.modularui.api.math; + +import java.awt.*; +import java.util.Objects; + +/** + * Describes an area in a gui + * Immutable! + */ +public final class GuiArea { + + public final int x0, x1, y0, y1; + public final int width, height; + + public GuiArea(int x0, int x1, int y0, int y1) { + this.x0 = Math.min(x0, x1); + this.x1 = Math.max(x0, x1); + this.y0 = Math.min(y0, y1); + this.y1 = Math.max(y0, y1); + this.width = this.x1 - this.x0; + this.height = this.y1 - this.y0; + } + + public static GuiArea ofRectangle(Rectangle rectangle) { + return GuiArea.ltwh(rectangle.x, rectangle.y, rectangle.width, rectangle.height); + } + + public static GuiArea ofPoints(Pos2d p0, Pos2d p1) { + return new GuiArea(p0.x, p1.x, p0.y, p1.y); + } + + public static GuiArea of(Size size, Pos2d pos) { + return GuiArea.ltwh(pos.x, pos.y, size.width, size.height); + } + + /** + * Makes an area of the top left corner and width and height + * left - top - width - height + */ + public static GuiArea ltwh(int x, int y, int width, int height) { + return new GuiArea(x, x + width, y, y + height); + } + + public GuiArea withSize(int width, int height) { + return new GuiArea(x0, x0 + width, y0, y0 + height); + } + + public GuiArea withSize(Size size) { + return withSize(size.width, height); + } + + public GuiArea withPos(int x, int y) { + return new GuiArea(x, x + width, y, y + height); + } + + public GuiArea translate(int x, int y) { + return new GuiArea(x0 + x, x1 + x, y0 + y, y1 + y); + } + + public GuiArea scale(float scale, float xPivot, float yPivot) { + return scale(scale, scale, xPivot, yPivot); + } + + public GuiArea scale(float xScale, float yScale, float xPivot, float yPivot) { + int x0 = this.x0, x1 = this.x1, y0 = this.y0, y1 = this.y1; + x0 = (int) (xPivot - (xPivot - x0) * xScale); + y0 = (int) (yPivot - (yPivot - y0) * yScale); + x1 = (int) (xPivot + (x1 - xPivot) * xScale); + y1 = (int) (yPivot + (y1 - yPivot) * yScale); + return new GuiArea(x0, x1, y0, y1); + } + + public boolean contains(float x, float y) { + return x >= x0 && x <= x1 && y >= y0 && y <= y1; + } + + public boolean contains(Pos2d pos) { + return contains(pos.getX(), pos.getY()); + } + + /** + * @param bounds to check + * @return if bounds are partly inside this bounds + */ + public boolean intersects(GuiArea bounds) { + for (Alignment alignment : Alignment.CORNERS) { + if (contains(bounds.corner(alignment))) { + return true; + } + } + return false; + } + + /** + * @param bounds to check + * @return if bounds are fully inside this bounds + */ + public boolean covers(GuiArea bounds) { + for (Alignment alignment : Alignment.CORNERS) { + if (!contains(bounds.corner(alignment))) { + return false; + } + } + return true; + } + + public Pos2d getCenter() { + return new Pos2d((x1 - x0) / 2 + x0, (y1 - y0) / 2 + y0); + } + + public Pos2d getTopLeft() { + return new Pos2d(x0, y0); + } + + public Pos2d corner(Alignment alignment) { + Pos2d center = getCenter(); + int x = (int) (center.getX() + width / 2 * alignment.x); + int y = (int) (center.getY() + height / 2 * alignment.y); + return new Pos2d(x, y); + } + + public float getArea() { + return width * height; + } + + public Size getSize() { + return new Size(width, height); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GuiArea GuiArea = (GuiArea) o; + return GuiArea.x0 == x0 && GuiArea.x1 == x1 && GuiArea.y0 == y0 && GuiArea.y1 == y1; + } + + @Override + public int hashCode() { + return Objects.hash(x0, x1, y0, y1); + } + + @Override + public String toString() { + return "Size:[" + width + ", " + height + "], Pos:[" + x0 + ", " + y0 + "]"; + } + + public Rectangle asRectangle() { + return new Rectangle(x0, y0, width, height); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/math/MainAxisAlignment.java b/src/main/java/com/gtnewhorizons/modularui/api/math/MainAxisAlignment.java new file mode 100644 index 0000000..499bb4c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/math/MainAxisAlignment.java @@ -0,0 +1,5 @@ +package com.gtnewhorizons.modularui.api.math; + +public enum MainAxisAlignment { + START, CENTER, END, SPACE_BETWEEN; +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java b/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java new file mode 100644 index 0000000..a83d77f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java @@ -0,0 +1,231 @@ +package com.gtnewhorizons.modularui.api.math; + +import com.gtnewhorizons.modularui.common.widget.textfield.TextFieldWidget; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MathExpression { + + private static final List DEFAULT = Collections.singletonList(0); + + public static double parseMathExpression(String expr) { + List parsed = buildParsedList(expr); + if (parsed == DEFAULT || parsed.size() == 0) { + return 0; + } + if (parsed.size() == 1) { + Object value = parsed.get(0); + return value instanceof Double ? (double) value : 0; + } + Double lastNum = null; + for (int i = 0; i < parsed.size(); i++) { + Object obj = parsed.get(i); + if (lastNum == null && obj instanceof Double) { + lastNum = (Double) obj; + continue; + } + if (obj == Operator.POWER) { + Double newNum = Math.pow(lastNum, (Double) parsed.get(i + 1)); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.add(i - 1, newNum); + lastNum = newNum; + i--; + continue; + } + lastNum = null; + } + if (lastNum != null) { + lastNum = null; + } + if (parsed.size() > 1) { + for (int i = 0; i < parsed.size(); i++) { + Object obj = parsed.get(i); + if (lastNum == null && obj instanceof Double) { + lastNum = (Double) obj; + continue; + } + if (obj == Operator.MULTIPLY) { + Double newNum = lastNum * (Double) parsed.get(i + 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.add(i - 1, newNum); + lastNum = newNum; + i--; + continue; + } + if (obj == Operator.DIVIDE) { + Double newNum = lastNum / (Double) parsed.get(i + 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.add(i - 1, newNum); + lastNum = newNum; + i--; + continue; + } + if (obj == Operator.MOD) { + Double newNum = lastNum % (Double) parsed.get(i + 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.add(i - 1, newNum); + lastNum = newNum; + i--; + continue; + } + lastNum = null; + } + if (lastNum != null) { + lastNum = null; + } + } + if (parsed.size() > 1) { + for (int i = 0; i < parsed.size(); i++) { + Object obj = parsed.get(i); + if (lastNum == null && obj instanceof Double) { + lastNum = (Double) obj; + continue; + } + if (obj == Operator.PLUS) { + Double newNum = lastNum + (Double) parsed.get(i + 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.add(i - 1, newNum); + lastNum = newNum; + i--; + continue; + } + if (obj == Operator.MINUS) { + Double newNum = lastNum - (Double) parsed.get(i + 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.add(i - 1, newNum); + lastNum = newNum; + i--; + continue; + } + lastNum = null; + } + } + if (parsed.size() != 1) { + throw new IllegalStateException("Calculated expr has more than 1 result. " + parsed); + } + return (Double) parsed.get(0); + } + + public static List buildParsedList(String expr) { + List parsed = new ArrayList<>(); + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < expr.length(); i++) { + char c = expr.charAt(i); + switch (c) { + case '+': { + if (builder.length() > 0) { + parsed.add(parse(builder.toString())); + builder.delete(0, builder.length()); + } + parsed.add(Operator.PLUS); + break; + } + case '-': { + if (builder.length() > 0) { + parsed.add(parse(builder.toString())); + builder.delete(0, builder.length()); + } + parsed.add(Operator.MINUS); + break; + } + case '*': { + if (builder.length() > 0) { + parsed.add(parse(builder.toString())); + builder.delete(0, builder.length()); + } + parsed.add(Operator.MULTIPLY); + break; + } + case '/': { + if (builder.length() > 0) { + parsed.add(parse(builder.toString())); + builder.delete(0, builder.length()); + } + parsed.add(Operator.DIVIDE); + break; + } + case '%': { + if (builder.length() > 0) { + parsed.add(parse(builder.toString())); + builder.delete(0, builder.length()); + } + parsed.add(Operator.MOD); + break; + } + case '^': { + if (builder.length() > 0) { + parsed.add(parse(builder.toString())); + builder.delete(0, builder.length()); + } + parsed.add(Operator.POWER); + break; + } + default: + builder.append(c); + } + } + if (builder.length() > 0) { + parsed.add(parse(builder.toString())); + } + if (parsed.size() >= 2 && parsed.get(0) == Operator.MINUS && parsed.get(1) instanceof Double) { + parsed.add(0, 0.0); + } + boolean shouldBeOperator = false; + for (Object object : parsed) { + if (shouldBeOperator) { + if (!(object instanceof Operator)) { + return DEFAULT; + } + shouldBeOperator = false; + } else { + if (!(object instanceof Double)) { + return DEFAULT; + } + shouldBeOperator = true; + } + } + while (parsed.get(parsed.size() - 1) instanceof Operator) { + parsed.remove(parsed.size() - 1); + } + return parsed; + } + + public static double parse(String num) { + try { + return TextFieldWidget.format.parse(num).doubleValue(); + } catch (ParseException e) { + e.printStackTrace(); + } + return 0; + } + + public enum Operator { + + PLUS("+"), MINUS("-"), MULTIPLY("*"), DIVIDE("/"), MOD("%"), POWER("^"); + public final String sign; + + Operator(String sign) { + this.sign = sign; + } + + @Override + public String toString() { + return sign; + } + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/math/Pos2d.java b/src/main/java/com/gtnewhorizons/modularui/api/math/Pos2d.java new file mode 100644 index 0000000..3f73e24 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/math/Pos2d.java @@ -0,0 +1,155 @@ +package com.gtnewhorizons.modularui.api.math; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.common.internal.JsonHelper; + +import java.awt.*; +import java.util.Objects; + +public class Pos2d { + + public static final Pos2d ZERO = zero(); + + public static Pos2d zero() { + return new Pos2d(0, 0); + } + + public final int x, y; + + public Pos2d(int x, int y) { + this.x = x; + this.y = y; + } + + public Pos2d(double x, double y) { + this((int) x, (int) y); + } + + public static Pos2d ofPoint(Point point) { + return new Pos2d(point.x, point.y); + } + + public static Pos2d cartesian(int x, int y) { + return new Pos2d(x, y); + } + + public static Pos2d polar(float angle, float length) { + float sin = (float) Math.sin(Math.toRadians(angle)); + float cos = (float) Math.cos(Math.toRadians(angle)); + return new Pos2d(cos * length, sin * length); + } + + public Pos2d add(Pos2d p) { + return new Pos2d(x + p.x, y + p.y); + } + + public Pos2d add(int x, int y) { + return new Pos2d(x + this.x, y + this.y); + } + + public Pos2d subtract(Pos2d p) { + return new Pos2d(x - p.x, y - p.y); + } + + public double distance(Pos2d p) { + float x = Math.max(this.x - p.x, p.x - this.x); + float y = Math.max(this.y - p.y, p.y - this.y); + return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); + } + + public float angle(Pos2d p) { + float x = this.x - p.x; + float y = this.y - p.y; + return (float) Math.toDegrees(Math.atan(y / x)) + 90; + } + + public static boolean isInside(int posX, int posY, Pos2d areaPos, Size size) { + return isInside(posX, posY, areaPos.x, areaPos.y, size.width, size.height); + } + + public static boolean isInside(int posX, int posY, int areaX, int areaY, int width, int height) { + return posX >= areaX && + posX < areaX + width && + posY >= areaY && + posY < areaY + height; + } + + public boolean isInside(Pos2d pos, Size size) { + return isInside(pos.x, pos.y, size.width, size.height); + } + + public boolean isInside(Pos2d pos, int width, int height) { + return isInside(pos.x, pos.y, width, height); + } + + public boolean isInside(int x, int y, Size size) { + return isInside(x, y, size.width, size.height); + } + + public boolean isInside(int x, int y, int width, int height) { + return isInside(this.x, this.y, x, y, width, height); + } + + public float getX() { + return x; + } + + public float getY() { + return y; + } + + public boolean isZero() { + return x == 0 && y == 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Pos2d pos = (Pos2d) o; + return Float.compare(pos.x, x) == 0 && Float.compare(pos.y, y) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(x, y); + } + + @Override + public String toString() { + return "[" + x + ", " + y + ']'; + } + + public Point asPoint() { + return new Point(x, y); + } + + public static Pos2d ofJson(JsonElement jsonElement) { + int x = 0, y = 0; + if (jsonElement.isJsonObject()) { + JsonObject json = jsonElement.getAsJsonObject(); + x = JsonHelper.getInt(json, 0, "x"); + y = JsonHelper.getInt(json, 0, "y"); + } else { + String raw = jsonElement.getAsString(); + if (raw.contains(",")) { + String[] parts = raw.split(","); + try { + if (!parts[0].isEmpty()) { + x = Integer.parseInt(parts[0]); + } + if (parts.length > 1 && !parts[1].isEmpty()) { + y = Integer.parseInt(parts[1]); + } + } catch (NumberFormatException e) { + ModularUI.logger.error("Error parsing JSON pos: {}", raw); + e.printStackTrace(); + } + } + } + return new Pos2d(x, y); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/math/Size.java b/src/main/java/com/gtnewhorizons/modularui/api/math/Size.java new file mode 100644 index 0000000..ddb0ea0 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/math/Size.java @@ -0,0 +1,126 @@ +package com.gtnewhorizons.modularui.api.math; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.common.internal.JsonHelper; + +import java.awt.*; +import java.util.Objects; + +public class Size { + + public static final Size ZERO = zero(); + + public static Size zero() { + return new Size(0, 0); + } + + public final int width, height; + + public Size(int width, int height) { + this.width = Math.max(0, width); + this.height = Math.max(0, height); + } + + public Size(double width, double height) { + this((int) width, (int) height); + } + + public Size(Size widthComponent, Size heightComponent) { + this(widthComponent.width, heightComponent.height); + } + + public Size(int width, Size heightComponent) { + this(width, heightComponent.height); + } + + public Size(Size widthComponent, int height) { + this(widthComponent.width, height); + } + + public static Size ofDimension(Dimension dimension) { + return new Size(dimension.width, dimension.height); + } + + public boolean isLargerThan(Size size) { + return (size.width * size.height) < (width * height); + } + + public boolean hasLargerDimensionsThan(Size size) { + return width > size.width && height > size.height; + } + + public Size shrink(int width, int height) { + return new Size(this.width - width, this.height - height); + } + + public Size grow(int width, int height) { + return new Size(this.width + width, this.height + height); + } + + public Size scale(float widthScale, float heightScale) { + return new Size(this.width * widthScale, this.height * heightScale); + } + + public Size scale(float scale) { + return scale(scale, scale); + } + + public boolean isZero() { + return width == 0 && height == 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Size size = (Size) o; + return Float.compare(size.width, width) == 0 && Float.compare(size.height, height) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(width, height); + } + + @Override + public String toString() { + return "[" + width + ", " + height + "]"; + } + + public Dimension asDimension() { + return new Dimension(width, height); + } + + public Rectangle asRectangle() { + return new Rectangle(width, height); + } + + public static Size ofJson(JsonElement jsonElement) { + int width = 0, height = 0; + if (jsonElement.isJsonObject()) { + JsonObject json = jsonElement.getAsJsonObject(); + width = JsonHelper.getInt(json, 0, "width", "w"); + height = JsonHelper.getInt(json, 0, "height", "h"); + } else { + String raw = jsonElement.getAsString(); + if (raw.contains(",")) { + String[] parts = raw.split(","); + try { + if (!parts[0].isEmpty()) { + width = Integer.parseInt(parts[0]); + } + if (parts.length > 1 && !parts[1].isEmpty()) { + height = Integer.parseInt(parts[1]); + } + } catch (NumberFormatException e) { + ModularUI.logger.error("Error parsing JSON pos: {}", raw); + e.printStackTrace(); + } + } + } + return new Size(width, height); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/nei/IGhostIngredientHandler.java b/src/main/java/com/gtnewhorizons/modularui/api/nei/IGhostIngredientHandler.java new file mode 100644 index 0000000..b50b67a --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/nei/IGhostIngredientHandler.java @@ -0,0 +1,52 @@ +package com.gtnewhorizons.modularui.api.nei; + +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.item.ItemStack; + +import java.awt.*; +import java.util.List; +import java.util.function.Consumer; + +/** + * Lets mods accept ghost ingredients from NEI. + * These ingredients are dragged from the ingredient list on to your gui, and are useful + * for setting recipes or anything else that does not need the real ingredient to exist. + */ +public interface IGhostIngredientHandler { + /** + * Called when a player wants to drag an ingredient on to your gui. + * Return the targets that can accept the ingredient. + * + * This is called when a player hovers over an ingredient with doStart=false, + * and called again when they pick up the ingredient with doStart=true. + */ + List getTargets(T gui, ItemStack ingredient, boolean doStart); + + /** + * Called when the player is done dragging an ingredient. + * If the drag succeeded, {@link Target#accept(ItemStack)} was called before this. + * Otherwise, the player failed to drag an ingredient to a {@link Target}. + */ + void onComplete(); + + /** + * @return true if NEI should highlight the targets for the player. + * false to handle highlighting yourself. + */ + default boolean shouldHighlightTargets() { + return true; + } + + interface Target extends Consumer { + /** + * @return the area (in screen coordinates) where the ingredient can be dropped. + */ + Rectangle getArea(); + + /** + * Called with the ingredient when it is dropped on the target. + */ + @Override + void accept(ItemStack ingredient); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/screen/Cursor.java b/src/main/java/com/gtnewhorizons/modularui/api/screen/Cursor.java new file mode 100644 index 0000000..da9b436 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/screen/Cursor.java @@ -0,0 +1,333 @@ +package com.gtnewhorizons.modularui.api.screen; + +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.widget.IDraggable; +import com.gtnewhorizons.modularui.api.widget.IWidgetParent; +import com.gtnewhorizons.modularui.api.widget.Widget; +import net.minecraft.client.Minecraft; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +public class Cursor { + + private final ModularUIContext uiContext; + @Nullable + private IDraggable cursorDraggable; + @Nullable + private Widget hovered; + @Nullable + private Widget focused; + private final List hoveredWidgets = new ArrayList<>(); + private int lastButton = -1; + private long lastClickTime = 0; + + private int timeHovered; + + protected Cursor(ModularUIContext context) { + this.uiContext = context; + } + + public Pos2d getPos() { + return uiContext.getScreen().getMousePos(); + } + + public int getX() { + return getPos().x; + } + + public int getY() { + return getPos().y; + } + + public boolean isAbove(Widget widget) { + return widget.isUnderMouse(getPos()); + } + + public boolean isAbove(IWidgetParent widget) { + return getPos().isInside(widget.getAbsolutePos(), widget.getSize()); + } + + public boolean isHovering(Widget widget) { + return this.hovered == widget; + } + + @Nullable + public Widget getHovered() { + return hovered; + } + + public boolean isFocused(Widget widget) { + return this.focused == widget; + } + + public void removeFocus(Widget widget) { + if (this.focused != null && isFocused(widget)) { + this.focused.onRemoveFocus(); + this.focused = null; + } + } + + @ApiStatus.Internal + public void updateFocused(Widget widget) { + if (widget != null) { + if (focused == null || focused != widget) { + if (focused != null) { + focused.onRemoveFocus(); + } + focused = widget.shouldGetFocus() ? widget : null; + } + } else if (focused != null) { + focused.onRemoveFocus(); + focused = null; + } + } + + @Nullable + public Widget getFocused() { + return focused; + } + + public boolean isRightBelow(Widget widget) { + return !this.hoveredWidgets.isEmpty() && this.hoveredWidgets.get(0) == widget; + } + + public List getAllHovered() { + return hoveredWidgets; + } + + @ApiStatus.Internal + public void updateHovered() { + Widget w = findHoveredWidgets(); + if (w != this.hovered) { + this.hovered = w; + this.timeHovered = 0; + } + } + + @Nullable + public ItemStack getItemStack() { + return uiContext.getPlayer().inventory.getItemStack(); + } + + public void setItemStack(ItemStack stack, boolean sync) { + if (stack != null) { + uiContext.getPlayer().inventory.setItemStack(stack); + if (sync && !uiContext.isClient()) { + uiContext.sendServerPacket(ModularUIContext.DataCodes.SYNC_CURSOR_STACK, null, uiContext.getMainWindow(), buffer -> { + try { + buffer.writeItemStackToBuffer(stack); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + } + } + + public boolean hasDraggable() { + return this.cursorDraggable != null; + } + + public boolean isHoldingSomething() { + return hasDraggable() || getItemStack() != null; + } + + @Nullable + public Rectangle getDraggableArea() { + return this.cursorDraggable == null ? null : this.cursorDraggable.getArea(); + } + + public int getTimeHovered() { + return timeHovered; + } + + @ApiStatus.Internal + public void draw(float partialTicks) { + if (this.cursorDraggable != null) { + this.cursorDraggable.renderMovingState(partialTicks); + } + } + + @ApiStatus.Internal + public void onScreenUpdate() { + if (this.hovered != null) { +// if (hovered instanceof IVanillaSlot) { +// uiContext.getScreen().getAccessor().setHoveredSlot(((IVanillaSlot) hovered).getMcSlot()); +// } + this.timeHovered++; + } + if (this.cursorDraggable != null && getItemStack() != null) { + this.cursorDraggable.onDragEnd(false); + this.cursorDraggable.setMoving(false); + this.cursorDraggable = null; + } + if (this.cursorDraggable != null) { + this.cursorDraggable.onDrag(lastButton, Minecraft.getSystemTime() - lastClickTime); + } + } + + @ApiStatus.Internal + public boolean onMouseClick(int button) { + if ((button == 0 || button == 1) && getItemStack() == null && this.cursorDraggable != null) { + ModularWindow window = findHoveredWindow(); + this.cursorDraggable.onDragEnd(this.cursorDraggable.canDropHere(hovered, window != null)); + this.cursorDraggable.setMoving(false); + this.cursorDraggable = null; + this.lastButton = -1; + this.lastClickTime = 0; + return true; + } + return false; + } + + @ApiStatus.Internal + public boolean onMouseReleased(int button) { + if (button == this.lastButton && getItemStack() == null && this.cursorDraggable != null) { + long time = Minecraft.getSystemTime(); + if (time - this.lastClickTime < 200) return false; + ModularWindow window = findHoveredWindow(); + this.cursorDraggable.onDragEnd(this.cursorDraggable.canDropHere(hovered, window != null)); + this.cursorDraggable.setMoving(false); + this.cursorDraggable = null; + this.lastButton = -1; + this.lastClickTime = 0; + return true; + } + return false; + } + + @ApiStatus.Internal + public boolean onHoveredClick(int button, Object hovered) { + if ((button == 0 || button == 1) && getItemStack() == null && this.cursorDraggable == null) { + IDraggable draggable; + if (hovered instanceof IDraggable) { + draggable = (IDraggable) hovered; + } else if (hovered instanceof ModularWindow && ((ModularWindow) hovered).isDraggable()) { + draggable = new DraggableWindowWrapper((ModularWindow) hovered, getPos().subtract(((ModularWindow) hovered).getPos())); + } else { + return false; + } + if (draggable.onDragStart(button)) { + draggable.setMoving(true); + this.cursorDraggable = draggable; + this.lastButton = button; + this.lastClickTime = Minecraft.getSystemTime(); + return true; + } + } + return false; + } + + @Nullable + public Widget findHoveredWidget() { + return findHoveredWidget(false); + } + + /** + * @return the top most hovered object. Can be a widget or a window + */ + @Nullable + public IDraggable findDraggable() { + IDraggable draggable = null; + for (ModularWindow window : uiContext.getOpenWindows()) { + if (!window.isEnabled()) continue; + AtomicReference hovered = new AtomicReference<>(); + IWidgetParent.forEachByLayer(window, true, widget -> { + if (widget instanceof IDraggable && (hovered.get() == null || widget.getLayer() > hovered.get().getLayer()) && isAbove(widget) && widget.canHover()) { + hovered.set(widget); + } + return false; + }); + if (draggable == null && hovered.get() == null && window.isDraggable() && isAbove(window)) { + draggable = new DraggableWindowWrapper(window, getPos().subtract(window.getPos())); + } else if (hovered.get() != null) { + draggable = (IDraggable) hovered.get(); + } + } + return draggable; + } + + @Nullable + public Widget findHoveredWidget(ModularWindow window) { + return findHoveredWidget(window, false); + } + + @Nullable + public Widget findHoveredWidget(boolean forDebug) { + for (ModularWindow window : uiContext.getOpenWindows()) { + if (!window.isEnabled()) continue; + Widget widget = findHoveredWidget(window, forDebug); + if (widget != null) { + return widget; + } + } + return null; + } + + @Nullable + public Widget findHoveredWidget(ModularWindow window, boolean forDebug) { + AtomicReference hovered = new AtomicReference<>(); + IWidgetParent.forEachByLayer(window, widget -> { + if ((hovered.get() == null || widget.getLayer() > hovered.get().getLayer()) && widget.isEnabled() && isAbove(widget) && (forDebug || widget.canHover())) { + hovered.set(widget); + } + return false; + }); + return hovered.get(); + } + + @Nullable + public ModularWindow findHoveredWindow() { + for (ModularWindow window : uiContext.getOpenWindows()) { + if (window.isEnabled() && isAbove(window)) { + return window; + } + } + return null; + } + + private Widget findHoveredWidgets() { + this.hoveredWidgets.clear(); + Widget hovered = null; + LinkedList stack = new LinkedList<>(); + boolean nextWindow = true; + for (ModularWindow window : uiContext.getOpenWindowsReversed()) { + if (!window.isEnabled()) continue; + if (isAbove(window)) { + hoveredWidgets.add(0, window); + hovered = null; + } + stack.clear(); + stack.addLast(window); + while (!stack.isEmpty()) { + IWidgetParent parent1 = stack.pollFirst(); + for (Widget child : parent1.getChildren()) { + if (!child.isEnabled()) continue; + boolean above = isAbove(child); + if (above) { + hoveredWidgets.add(0, child); + if (child.canHover()) { + if (hovered == null || (nextWindow || child.getLayer() > hovered.getLayer())) { + hovered = child; + nextWindow = false; + } + } + } + if (child instanceof IWidgetParent && (!((IWidgetParent) child).childrenMustBeInBounds() || above)) { + stack.addLast((IWidgetParent) child); + } + } + } + nextWindow = true; + } + return hovered; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/screen/DraggableWindowWrapper.java b/src/main/java/com/gtnewhorizons/modularui/api/screen/DraggableWindowWrapper.java new file mode 100644 index 0000000..027c1d2 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/screen/DraggableWindowWrapper.java @@ -0,0 +1,72 @@ +package com.gtnewhorizons.modularui.api.screen; + +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.widget.IDraggable; +import com.gtnewhorizons.modularui.api.widget.Widget; +import org.jetbrains.annotations.Nullable; +import java.awt.*; + +public class DraggableWindowWrapper implements IDraggable { + + private final ModularWindow window; + private final Pos2d relativeClickPos; + private boolean moving; + private final Rectangle area = new Rectangle(); + + public DraggableWindowWrapper(ModularWindow window, Pos2d relativeClickPos) { + this.window = window; + this.relativeClickPos = relativeClickPos; + this.area.setSize(window.getSize().width, window.getSize().height); + } + + @Override + public void renderMovingState(float delta) { + Cursor cursor = window.getContext().getCursor(); + GlStateManager.pushMatrix(); + GlStateManager.translate(-window.getPos().x, -window.getPos().y, 0); + GlStateManager.translate(cursor.getX() - relativeClickPos.x, cursor.getY() - relativeClickPos.y, 0); + window.drawWidgets(delta, false); + GlStateManager.popMatrix(); + } + + @Override + public boolean onDragStart(int button) { + return button == 0; + } + + @Override + public void onDragEnd(boolean successful) { + if (successful) { + window.setPos(window.getContext().getCursor().getPos().subtract(relativeClickPos)); + window.markNeedsRebuild(); + } + } + + @Override + public void onDrag(int mouseButton, long timeSinceLastClick) { + } + + @Override + public boolean canDropHere(@Nullable Widget widget, boolean isInBounds) { + return true; + } + + @Override + public @Nullable Rectangle getArea() { + Pos2d cursor = window.getContext().getCursor().getPos(); + this.area.setLocation(cursor.x - relativeClickPos.x, cursor.y - relativeClickPos.y); + return this.area; + } + + @Override + public boolean isMoving() { + return moving; + } + + @Override + public void setMoving(boolean moving) { + this.moving = moving; + this.window.setEnabled(!moving); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/screen/IContainerCreator.java b/src/main/java/com/gtnewhorizons/modularui/api/screen/IContainerCreator.java new file mode 100644 index 0000000..0444cd7 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/screen/IContainerCreator.java @@ -0,0 +1,12 @@ +package com.gtnewhorizons.modularui.api.screen; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.Container; +import net.minecraft.world.World; + +@FunctionalInterface +public interface IContainerCreator { + + Container create(EntityPlayer player, World world, int x, int y, int z); + +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/screen/IGuiCreator.java b/src/main/java/com/gtnewhorizons/modularui/api/screen/IGuiCreator.java new file mode 100644 index 0000000..a45473d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/screen/IGuiCreator.java @@ -0,0 +1,14 @@ +package com.gtnewhorizons.modularui.api.screen; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.world.World; + +@FunctionalInterface +public interface IGuiCreator { + + @SideOnly(Side.CLIENT) + Object create(EntityPlayer player, World world, int x, int y, int z); + +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/screen/ITileWithModularUI.java b/src/main/java/com/gtnewhorizons/modularui/api/screen/ITileWithModularUI.java new file mode 100644 index 0000000..9088422 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/screen/ITileWithModularUI.java @@ -0,0 +1,10 @@ +package com.gtnewhorizons.modularui.api.screen; + +/** + * Implement this interface for your {@link net.minecraft.tileentity.TileEntity} + * to display Modular UI + */ +public interface ITileWithModularUI { + + ModularWindow createWindow(UIBuildContext buildContext); +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/screen/IWindowCreator.java b/src/main/java/com/gtnewhorizons/modularui/api/screen/IWindowCreator.java new file mode 100644 index 0000000..b81b238 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/screen/IWindowCreator.java @@ -0,0 +1,9 @@ +package com.gtnewhorizons.modularui.api.screen; + +import net.minecraft.entity.player.EntityPlayer; + +public interface IWindowCreator { + + ModularWindow create(EntityPlayer player); + +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/screen/ModularUIContext.java b/src/main/java/com/gtnewhorizons/modularui/api/screen/ModularUIContext.java new file mode 100644 index 0000000..7ed57c1 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/screen/ModularUIContext.java @@ -0,0 +1,406 @@ +package com.gtnewhorizons.modularui.api.screen; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.ImmutableMap; +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.common.internal.network.CWidgetUpdate; +import com.gtnewhorizons.modularui.common.internal.network.NetworkHandler; +import com.gtnewhorizons.modularui.common.internal.network.NetworkUtils; +import com.gtnewhorizons.modularui.common.internal.network.SWidgetUpdate; +import com.gtnewhorizons.modularui.common.internal.wrapper.BaseSlot; +import com.gtnewhorizons.modularui.common.internal.wrapper.ModularGui; +import com.gtnewhorizons.modularui.common.internal.wrapper.ModularUIContainer; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.ISyncedWidget; +import com.gtnewhorizons.modularui.api.widget.Widget; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.Unpooled; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.PacketBuffer; + +import java.awt.*; +import java.io.IOException; +import java.util.List; +import java.util.*; +import java.util.function.Consumer; + +public class ModularUIContext { + + private final ImmutableMap syncedWindowsCreators; + private final Deque windows = new LinkedList<>(); + private final BiMap syncedWindows = HashBiMap.create(4); + private final Map lastWindowPos = new HashMap<>(); + private ModularWindow mainWindow; + @SideOnly(Side.CLIENT) + private ModularGui screen; + private final EntityPlayer player; + private final com.gtnewhorizons.modularui.api.screen.Cursor cursor; + private final List jeiExclusionZone = new ArrayList<>(); + private final List queuedOpenWindow = new ArrayList<>(); + public final boolean clientOnly; + private boolean isClosing = false; + private final List closeListeners; + private final boolean showJei; + + private Size screenSize = NetworkUtils.isDedicatedClient() ? new Size(Minecraft.getMinecraft().displayWidth, Minecraft.getMinecraft().displayHeight) : Size.ZERO; + + private ModularUIContainer container; + + public ModularUIContext(UIBuildContext context) { + this(context, false); + } + + public ModularUIContext(UIBuildContext context, boolean clientOnly) { + this.player = context.player; + if (!isClient() && clientOnly) { + throw new IllegalArgumentException("Client only ModularUI can not be opened on server!"); + } + this.clientOnly = clientOnly; + this.syncedWindowsCreators = context.syncedWindows.build(); + this.cursor = new com.gtnewhorizons.modularui.api.screen.Cursor(this); + this.closeListeners = context.closeListeners; + this.showJei = context.showJei; + } + + public boolean isClient() { + return NetworkUtils.isClient(this.player); + } + + public void initialize(ModularUIContainer container, ModularWindow mainWindow) { + this.container = container; + this.mainWindow = mainWindow; + pushWindow(mainWindow); + this.syncedWindows.put(0, mainWindow); + mainWindow.draggable = false; + if (isClient()) { + // if on client, notify the server that the client initialized, to allow syncing to client + mainWindow.initialized = true; + sendClientPacket(DataCodes.SYNC_INIT, null, mainWindow, NetworkUtils.EMPTY_PACKET); + } + } + + @SideOnly(Side.CLIENT) + public void initializeClient(ModularGui screen) { + this.screen = screen; + } + + @SideOnly(Side.CLIENT) + public void buildWindowOnStart() { + for (ModularWindow window : windows) { + window.rebuild(); + } + } + + @SideOnly(Side.CLIENT) + public void resize(Size scaledSize) { + this.screenSize = scaledSize; + for (ModularWindow window : this.windows) { + window.onResize(scaledSize); + if (window == this.mainWindow) { + getScreen().setMainWindowArea(window.getPos(), window.getSize()); + } + } + } + + public void onClientTick() { + if (!queuedOpenWindow.isEmpty()) { + queuedOpenWindow.removeIf(windowId -> { + ModularWindow oldWindow = syncedWindows.get(windowId); + if (oldWindow != null && oldWindow.isClosing()) return false; + ModularWindow newWindow = openWindow(syncedWindowsCreators.get(windowId)); + syncedWindows.put(windowId, newWindow); + newWindow.onResize(screenSize); + newWindow.rebuild(); + newWindow.onOpen(); + newWindow.initialized = true; + sendClientPacket(DataCodes.INIT_WINDOW, null, newWindow, NetworkUtils.EMPTY_PACKET); + return true; + }); + } + } + + public boolean isWindowOpen(int id) { + return this.syncedWindows.containsKey(id); + } + + public void openSyncedWindow(int id) { + if (isClient()) { + ModularUI.logger.error("Synced windows must be opened on server!"); + return; + } + if (isWindowOpen(id)) { + return; + } + if (syncedWindowsCreators.containsKey(id)) { + sendServerPacket(DataCodes.OPEN_WINDOW, null, mainWindow, buf -> buf.writeVarIntToBuffer(id)); + ModularWindow window = openWindow(syncedWindowsCreators.get(id)); + syncedWindows.put(id, window); + } else { + ModularUI.logger.error("Could not find window with id {}", id); + } + } + + public ModularWindow openWindow(IWindowCreator windowCreator) { + ModularWindow window = windowCreator.create(player); + pushWindow(window); + return window; + } + + public void closeWindow(int id) { + if (syncedWindows.containsKey(id)) { + closeWindow(syncedWindows.get(id)); + } else { + ModularUI.logger.error("Could not close window with id {}", id); + } + } + + public void closeWindow(ModularWindow window) { + if (window == null) { + return; + } + if (windows.removeLastOccurrence(window)) { + window.destroyWindow(); + } + if (isClient()) { + if (!hasWindows() || window == mainWindow) { + close(); + } + } else { + sendServerPacket(DataCodes.CLOSE_WINDOW, null, window, NetworkUtils.EMPTY_PACKET); + } + if (syncedWindows.containsValue(window)) { + syncedWindows.inverse().remove(window); + } + } + + private void pushWindow(ModularWindow window) { + if (windows.offerLast(window)) { + window.initialize(this); + } else { + ModularUI.logger.error("Failed opening window"); + } + } + + public ModularWindow getCurrentWindow() { + return windows.isEmpty() ? mainWindow : windows.peekLast(); + } + + public ModularWindow getMainWindow() { + return mainWindow; + } + + public void tryClose() { + if (this.isClosing) { + for (ModularWindow window : getOpenWindows()) { + closeWindow(window); + } + return; + } + for (ModularWindow window : getOpenWindows()) { + if (window.tryClose() && window == mainWindow) { + this.isClosing = true; + } + } + } + + public void close() { + player.closeScreen(); + } + + public void closeAllButMain() { + for (ModularWindow window : getOpenWindows()) { + if (window != mainWindow) { + window.tryClose(); + } + } + } + + public void storeWindowPos(ModularWindow window, Pos2d pos) { + if (syncedWindows.containsValue(window)) { + this.lastWindowPos.put(this.syncedWindows.inverse().get(window), pos); + } + } + + public boolean tryApplyStoredPos(ModularWindow window) { + if (window == this.mainWindow || !this.syncedWindows.containsValue(window)) return false; + int id = this.syncedWindows.inverse().get(window); + if (this.lastWindowPos.containsKey(id)) { + window.setPos(this.lastWindowPos.get(id)); + return true; + } + return false; + } + + @SideOnly(Side.CLIENT) + public Pos2d getMousePos() { + return screen.getMousePos(); + } + + public boolean hasWindows() { + return !windows.isEmpty(); + } + + public EntityPlayer getPlayer() { + return player; + } + + public ModularUIContainer getContainer() { + return container; + } + + public ModularGui getScreen() { + return screen; + } + + public boolean isClientOnly() { + return clientOnly; + } + + public Cursor getCursor() { + return cursor; + } + + public Iterable getOpenWindows() { + return windows::descendingIterator; + } + + public Iterable getOpenWindowsReversed() { + return windows; + } + + @SideOnly(Side.CLIENT) + public Size getScaledScreenSize() { + return screenSize; + } + + public boolean doShowJei() { + return showJei; + } + + public void registerExclusionZone(Widget widget) { + this.jeiExclusionZone.add(widget); + } + + public List getJeiExclusionZones() { + List zones = new ArrayList<>(); + for (ModularWindow window : getOpenWindows()) { + if (window.isEnabled()) { + zones.add(window.getRectangle()); + } + } + for (Widget widget : jeiExclusionZone) { + zones.add(widget.getRectangle()); + } + Rectangle draggableArea = cursor.getDraggableArea(); + if (draggableArea != null) { + zones.add(draggableArea); + } + return zones; + } + + public List getCloseListeners() { + return closeListeners; + } + + public void addCloseListener(Runnable runnable) { + this.closeListeners.add(runnable); + } + + public void syncSlotContent(BaseSlot slot) { + if (slot != getContainer().inventorySlots.get(slot.slotNumber)) { + throw new IllegalStateException("Slot does not have the same index in the container!"); + } + getContainer().sendSlotChange(slot.getStack(), slot.slotNumber); + } + +// public void syncHeldItem() { +// getContainer().sendHeldItemUpdate(); +// } + + public void readClientPacket(PacketBuffer buf, int widgetId) throws IOException { + int id = buf.readVarIntFromBuffer(); + ModularWindow window = syncedWindows.get(buf.readVarIntFromBuffer()); + if (widgetId == DataCodes.INTERNAL_SYNC) { + if (id == DataCodes.SYNC_INIT) { + mainWindow.initialized = true; + this.mainWindow.clientOnly = false; + } else if (id == DataCodes.INIT_WINDOW) { + window.initialized = true; + } else if (id == DataCodes.CLOSE_WINDOW) { + if (windows.removeLastOccurrence(window)) { + window.destroyWindow(); + } + syncedWindows.inverse().remove(window); + } + } else if (window != null) { + ISyncedWidget syncedWidget = window.getSyncedWidget(widgetId); + syncedWidget.readOnServer(id, buf); + } + } + + @SideOnly(Side.CLIENT) + public void readServerPacket(PacketBuffer buf, int widgetId) throws IOException { + int id = buf.readVarIntFromBuffer(); + ModularWindow window = syncedWindows.get(buf.readVarIntFromBuffer()); + if (widgetId == DataCodes.INTERNAL_SYNC) { + if (id == DataCodes.SYNC_CURSOR_STACK) { + player.inventory.setItemStack(buf.readItemStackFromBuffer()); + } else if (id == DataCodes.OPEN_WINDOW) { + queuedOpenWindow.add(buf.readVarIntFromBuffer()); + } else if (id == DataCodes.CLOSE_WINDOW) { + window.tryClose(); + } + } else if (window != null) { + ISyncedWidget syncedWidget = window.getSyncedWidget(widgetId); + syncedWidget.readOnClient(id, buf); + } + } + + @SideOnly(Side.CLIENT) + public void sendClientPacket(int discriminator, ISyncedWidget syncedWidget, ModularWindow window, Consumer bufferConsumer) { + if (isClient() && !isClientOnly()) { + if (!syncedWindows.containsValue(window)) { + ModularUI.logger.error("Window is not synced!"); + return; + } + int syncId = syncedWidget == null ? DataCodes.INTERNAL_SYNC : window.getSyncedWidgetId(syncedWidget); + PacketBuffer buffer = new PacketBuffer(Unpooled.buffer()); + buffer.writeVarIntToBuffer(discriminator); + buffer.writeVarIntToBuffer(syncedWindows.inverse().get(window)); + bufferConsumer.accept(buffer); + CWidgetUpdate packet = new CWidgetUpdate(buffer, syncId); + NetworkHandler.sendToServer(packet); + } + } + + public void sendServerPacket(int discriminator, ISyncedWidget syncedWidget, ModularWindow window, Consumer bufferConsumer) { + if (!isClient()) { + if (!syncedWindows.containsValue(window)) { + ModularUI.logger.error("Window is not synced!"); + return; + } + int syncId = syncedWidget == null ? DataCodes.INTERNAL_SYNC : window.getSyncedWidgetId(syncedWidget); + PacketBuffer buffer = new PacketBuffer(Unpooled.buffer()); + buffer.writeVarIntToBuffer(discriminator); + buffer.writeVarIntToBuffer(syncedWindows.inverse().get(window)); + bufferConsumer.accept(buffer); + SWidgetUpdate packet = new SWidgetUpdate(buffer, syncId); + NetworkHandler.sendToPlayer(packet, (EntityPlayerMP) player); + } + } + + protected static class DataCodes { + static final int INTERNAL_SYNC = -1; + static final int SYNC_CURSOR_STACK = 1; + static final int SYNC_INIT = 2; + static final int OPEN_WINDOW = 3; + static final int INIT_WINDOW = 4; + static final int CLOSE_WINDOW = 5; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/screen/ModularWindow.java b/src/main/java/com/gtnewhorizons/modularui/api/screen/ModularWindow.java new file mode 100644 index 0000000..d09722b --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/screen/ModularWindow.java @@ -0,0 +1,511 @@ +package com.gtnewhorizons.modularui.api.screen; + +import com.gtnewhorizons.modularui.config.Config; +import com.gtnewhorizons.modularui.api.ModularUITextures; +import com.gtnewhorizons.modularui.api.widget.ISyncedWidget; +import com.gtnewhorizons.modularui.api.widget.IWidgetBuilder; +import com.gtnewhorizons.modularui.api.widget.IWidgetParent; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.common.internal.Theme; +import com.gtnewhorizons.modularui.api.math.Color; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.ImmutableBiMap; +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.animation.Eases; +import com.gtnewhorizons.modularui.api.animation.Interpolator; +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.drawable.Text; +import com.gtnewhorizons.modularui.api.math.Alignment; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.common.widget.TextWidget; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.entity.player.EntityPlayer; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +/** + * A window in a modular gui. Only the "main" window can exist on both, server and client. + * All other only exist and needs to be opened on client. + */ +public class ModularWindow implements IWidgetParent { + + /** + * Build your ModularWindow from here. You can chain methods as you want + * and finally call {@link Builder#build()}. + *
See also: {@link ITileWithModularUI#createWindow(UIBuildContext)} + */ + public static Builder builder(int width, int height) { + return new Builder(new Size(width, height)); + } + + public static Builder builder(Size size) { + return new Builder(size); + } + + public static Builder builderFullScreen() { + return new Builder(Size.ZERO); + } + + private ModularUIContext context; + private final List children; + public final ImmutableBiMap syncedWidgets; + private final BiMap dynamicSyncedWidgets = HashBiMap.create(); + private final List interactionListeners = new ArrayList<>(); + protected boolean initialized = false; + protected boolean clientOnly = true; + + private final boolean fullScreen; + private Size size; + private Pos2d pos = Pos2d.ZERO; + private PosProvider posProvider; + private final Alignment alignment = Alignment.Center; + private final IDrawable[] background; + protected boolean draggable; + private boolean enabled = true; + private boolean needsRebuild = false; + private boolean initSync = true; + private int alpha = Color.getAlpha(Theme.INSTANCE.getBackground()); + private float scale = 1f; + private float rotation = 0; + private float translateX = 0, translateY = 0; + + private Interpolator openAnimation, closeAnimation; + + public ModularWindow(Size size, List children, IDrawable... background) { + this.fullScreen = size.isZero(); + this.size = size; + this.children = children; + this.background = background; + // latest point at which synced widgets can be added + IWidgetParent.forEachByLayer(this, widget -> { + if (widget instanceof IWidgetParent) { + ((IWidgetParent) widget).initChildren(); + } + }); + + ImmutableBiMap.Builder syncedWidgetBuilder = ImmutableBiMap.builder(); + AtomicInteger i = new AtomicInteger(); + IWidgetParent.forEachByLayer(this, widget -> { + if (widget instanceof ISyncedWidget) { + syncedWidgetBuilder.put(i.getAndIncrement(), (ISyncedWidget) widget); + } + if (i.get() == 0x10000) { + throw new IndexOutOfBoundsException("Too many synced widgets!"); + } + return false; + }); + this.syncedWidgets = syncedWidgetBuilder.build(); + this.posProvider = ((screenSize, mainWindow) -> Alignment.Center.getAlignedPos(screenSize, this.size)); + } + + protected void initialize(ModularUIContext context) { + this.context = context; + for (Widget widget : children) { + widget.initialize(this, this, 0); + } + } + + public void onResize(Size screenSize) { + if (this.fullScreen) { + this.size = screenSize; + this.pos = Pos2d.ZERO; + } else if (!this.context.tryApplyStoredPos(this)) { + setPos(this.posProvider.getPos(screenSize, this.context.getMainWindow())); + } + markNeedsRebuild(); + } + + public static boolean anyAnimation() { + return Config.openCloseDurationMs > 0 && + (Config.openCloseFade || + Config.openCloseTranslateFromBottom || + Config.openCloseScale || + Config.openCloseRotateFast); + } + + /** + * The final call after the window is initialized & positioned + */ + public void onOpen() { + if (openAnimation == null && anyAnimation()) { + final int startY = context.getScaledScreenSize().height - pos.y; + openAnimation = new Interpolator(0, 1, Config.openCloseDurationMs, Eases.EaseQuadOut, value -> { + float val = (float) value; + if (Config.openCloseFade) { + alpha = (int) (val * Color.getAlpha(Theme.INSTANCE.getBackground())); + } + if (Config.openCloseTranslateFromBottom) { + translateY = startY * (1 - val); + } + if (Config.openCloseScale) { + scale = val; + } + if (Config.openCloseRotateFast) { + rotation = val * 360; + } + }, val -> { + alpha = Color.getAlpha(Theme.INSTANCE.getBackground()); + translateX = 0; + translateY = 0; + scale = 1f; + rotation = 360; + }); + closeAnimation = openAnimation.getReversed(Config.openCloseDurationMs, Eases.EaseQuadIn); + openAnimation.forward(); + closeAnimation.setCallback(val -> { + closeWindow(); + openAnimation = null; + closeAnimation = null; + }); + } + //this.pos = new Pos2d(pos.x, getContext().getScaledScreenSize().height); + } + + /** + * Called when the player tries to close the ui. Starts animation or closes directly. + */ + public boolean tryClose() { + if (closeAnimation == null) { + closeWindow(); + } else if (!closeAnimation.isRunning()) { + closeAnimation.forward(); + return true; + } + return false; + } + + public boolean isClosing() { + return closeAnimation != null && closeAnimation.isRunning(); + } + + public void update() { + for (IDrawable drawable : background) { + drawable.tick(); + } + IWidgetParent.forEachByLayer(this, widget -> { + widget.onScreenUpdate(); + Consumer ticker = widget.getTicker(); + if (ticker != null) { + ticker.accept(widget); + } + IDrawable[] background = widget.getBackground(); + if (background != null) { + for (IDrawable drawable : background) { + if (drawable != null) { + drawable.tick(); + } + } + } + }); + if (needsRebuild) { + rebuild(); + } + } + + public void frameUpdate(float partialTicks) { + if (openAnimation != null) { + openAnimation.update(partialTicks); + } + if (closeAnimation != null) { + closeAnimation.update(partialTicks); + } + } + + public void serverUpdate() { + for (ISyncedWidget syncedWidget : syncedWidgets.values()) { + syncedWidget.detectAndSendChanges(this.initSync); + } + this.initSync = false; + } + + @SideOnly(Side.CLIENT) + protected void rebuild() { + // check auto size of each child from top to bottom + for (Widget child : getChildren()) { + child.buildTopToBottom(size.asDimension()); + } + // position widgets from bottom to top + for (Widget child : getChildren()) { + child.buildBottomToTop(); + } + needsRebuild = false; + } + + public void closeWindow() { + context.closeWindow(this); + } + + protected void destroyWindow() { + IWidgetParent.forEachByLayer(this, widget -> { + if (isEnabled()) { + widget.onPause(); + } + widget.onDestroy(); + }); + } + + public void drawWidgets(float partialTicks, boolean foreground) { + if (foreground) { + IWidgetParent.forEachByLayer(this, widget -> { + widget.drawInForeground(partialTicks); + return false; + }); + + } else { + GlStateManager.pushMatrix(); + // rotate around center + if (Config.openCloseRotateFast) { + GlStateManager.translate(pos.x + size.width / 2f, pos.y + size.height / 2f, 0); + GlStateManager.rotate(rotation, 0, 0, 1); + GlStateManager.translate(-(pos.x + size.width / 2f), -(pos.y + size.height / 2f), 0); + } + GlStateManager.translate(translateX, translateY, 0); + GlStateManager.scale(scale, scale, 1); + + GlStateManager.pushMatrix(); + float x = (pos.x + size.width / 2f * (1 - scale)) / scale; + float y = (pos.y + size.height / 2f * (1 - scale)) / scale; + GlStateManager.translate(x, y, 0); + int color = Color.withAlpha(Theme.INSTANCE.getBackground(), alpha); + for (IDrawable drawable : background) { + drawable.applyThemeColor(color); + drawable.draw(Pos2d.ZERO, size, partialTicks); + } + GlStateManager.popMatrix(); + + for (Widget widget : getChildren()) { + widget.drawInternal(partialTicks); + } + GlStateManager.color(1, 1, 1, 1); + GlStateManager.popMatrix(); + } + } + + @Override + public Size getSize() { + return size; + } + + @Override + public Pos2d getAbsolutePos() { + return pos; + } + + @Override + public Pos2d getPos() { + return pos; + } + + @Override + public List getChildren() { + return children; + } + + public boolean isDraggable() { + return draggable; + } + + public ModularUIContext getContext() { + return context; + } + + public void markNeedsRebuild() { + this.needsRebuild = true; + } + + public void setPos(Pos2d pos) { + this.pos = pos; + this.context.storeWindowPos(this, pos); + } + + public boolean doesNeedRebuild() { + return needsRebuild; + } + + public float getScale() { + return scale; + } + + public int getAlpha() { + return alpha; + } + + public boolean isInitialized() { + return initialized; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + if (this.enabled != enabled) { + this.enabled = enabled; + if (this.enabled) { + IWidgetParent.forEachByLayer(this, Widget::onResume); + } else { + IWidgetParent.forEachByLayer(this, Widget::onPause); + } + } + } + + public Rectangle getRectangle() { + return new Rectangle(pos.x, pos.y, size.width, size.height); + } + + /** + * The events of the added listeners are always called. + */ + public void addInteractionListener(Interactable interactable) { + interactionListeners.add(interactable); + } + + public List getInteractionListeners() { + return interactionListeners; + } + + /** + * Adds a dynamic synced widget. + * + * @param id a id for the synced widget. Must be 32768 > id > 0 + * @param syncedWidget dynamic synced widget + * @param parent the synced widget that added the dynamic widget + * @throws IllegalArgumentException if id is > 32767 or < 1 + * @throws NullPointerException if dynamic widget or parent is null + * @throws IllegalStateException if parent is a dynamic widget + */ + public void addDynamicSyncedWidget(int id, ISyncedWidget syncedWidget, ISyncedWidget parent) { + if (id <= 0 || id > 0xFFFF) { + throw new IllegalArgumentException("Dynamic Synced widget id must be greater than 0 and smaller than 65535 (0xFFFF)"); + } + if (syncedWidget == null || parent == null) { + throw new NullPointerException("Can't add dynamic null widget or with null parent!"); + } + if (dynamicSyncedWidgets.containsValue(syncedWidget)) { + dynamicSyncedWidgets.inverse().remove(syncedWidget); + } + int parentId = getSyncedWidgetId(parent); + if ((parentId & ~0xFFFF) != 0) { + throw new IllegalStateException("Dynamic synced widgets can't have other dynamic widgets as parent! It's possible with some trickery tho."); + } + // generate unique id + // first 2 bytes is passed id, last 2 bytes is parent id + id = ((id << 16) & ~0xFFFF) | parentId; + dynamicSyncedWidgets.put(id, syncedWidget); + } + + public int getSyncedWidgetId(ISyncedWidget syncedWidget) { + Integer id = syncedWidgets.inverse().get(syncedWidget); + if (id == null) { + id = dynamicSyncedWidgets.inverse().get(syncedWidget); + if (id == null) { + throw new NoSuchElementException("Can't find id for ISyncedWidget " + syncedWidget); + } + } + return id; + } + + public ISyncedWidget getSyncedWidget(int id) { + ISyncedWidget syncedWidget = syncedWidgets.get(id); + if (syncedWidget == null) { + syncedWidget = dynamicSyncedWidgets.get(id); + if (syncedWidget == null) { + throw new NoSuchElementException("Can't find ISyncedWidget for id " + id); + } + } + return syncedWidget; + } + + public static class Builder implements IWidgetBuilder { + + private final List widgets = new ArrayList<>(); + private IDrawable[] background = {}; + private Size size; + private PosProvider pos = null; + private boolean draggable = true; + + private Builder(Size size) { + this.size = size; + } + + /** + * Set background, but not limited to png files. + * See {@link ModularUITextures} for default examples. + */ + public Builder setBackground(IDrawable... background) { + this.background = background; + return this; + } + + public Builder setSize(Size size) { + this.size = size; + return this; + } + + public Builder setSize(int width, int height) { + return setSize(new Size(width, height)); + } + + /** + * Set position of this Window displayed. + * {@link Alignment#getAlignedPos(Size, Size)} is useful for specifying rough location. + * Center is selected as default. + * @param pos BiFunction providing {@link Pos2d} + * out of sizes of game screen and this window + */ + public Builder setPos(PosProvider pos) { + this.pos = pos; + return this; + } + + public Builder setDraggable(boolean draggable) { + this.draggable = draggable; + return this; + } + + public Builder bindPlayerInventory(EntityPlayer player, int marginBottom) { + return bindPlayerInventory(player, new Pos2d(size.width / 2 - 81, size.height - marginBottom - 76)); + } + + /** + * Bind player inventory to window. + */ + public Builder bindPlayerInventory(EntityPlayer player) { + return bindPlayerInventory(player, 7); + } + + public Builder addPlayerInventoryLabel(int x, int y) { + return widget(new TextWidget(Text.localised("container.inventory")).setPos(x, y)); + } + + @Override + public void addWidgetInternal(Widget widget) { + widgets.add(widget); + } + + /** + * Build this window. + */ + public ModularWindow build() { + ModularWindow window = new ModularWindow(size, widgets, background); + window.draggable = draggable; + if (pos != null) { + window.posProvider = pos; + } + return window; + } + } + + public interface PosProvider { + Pos2d getPos(Size screenSize, ModularWindow mainWindow); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/screen/UIBuildContext.java b/src/main/java/com/gtnewhorizons/modularui/api/screen/UIBuildContext.java new file mode 100644 index 0000000..58e5e61 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/screen/UIBuildContext.java @@ -0,0 +1,74 @@ +package com.gtnewhorizons.modularui.api.screen; + +import com.google.common.collect.ImmutableMap; +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.api.widget.Widget; +import net.minecraft.entity.player.EntityPlayer; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.Consumer; + +public class UIBuildContext { + + protected final EntityPlayer player; + private final Map jsonWidgets = new HashMap<>(); + protected final ImmutableMap.Builder syncedWindows = new ImmutableMap.Builder<>(); + protected final List closeListeners = new ArrayList<>(); + protected boolean showJei = true; + + public UIBuildContext(EntityPlayer player) { + this.player = player; + } + + public EntityPlayer getPlayer() { + return player; + } + + public void addJsonWidgets(String name, Widget widget) { + if (jsonWidgets.containsKey(name)) { + ModularUI.logger.warn("Widget {} is already registered from json", name); + } + jsonWidgets.put(name, widget); + } + + public void addCloseListener(Runnable runnable) { + closeListeners.add(runnable); + } + + @Nullable + public Widget getJsonWidget(String name) { + return jsonWidgets.get(name); + } + + @Nullable + public T getJsonWidget(String name, Class clazz) { + Widget widget = getJsonWidget(name); + if (widget != null && widget.getClass().isAssignableFrom(clazz)) { + return (T) widget; + } + return null; + } + + public void applyToWidget(String name, Class clazz, Consumer consumer) { + T t = getJsonWidget(name, clazz); + if (t != null) { + consumer.accept(t); + } else { + ModularUI.logger.error("Expected Widget with name {}, of class {}, but was not found!", name, clazz.getName()); + } + } + + public void addSyncedWindow(int id, IWindowCreator windowCreator) { + if (id <= 0) { + ModularUI.logger.error("Window id must be > 0"); + return; + } + syncedWindows.put(id, Objects.requireNonNull(windowCreator)); + } + + public void setShowJei(boolean showJei) { + this.showJei = showJei; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/widget/IDraggable.java b/src/main/java/com/gtnewhorizons/modularui/api/widget/IDraggable.java new file mode 100644 index 0000000..1d0c2ee --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/widget/IDraggable.java @@ -0,0 +1,58 @@ +package com.gtnewhorizons.modularui.api.widget; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; + +@ApiStatus.Experimental +public interface IDraggable { + + /** + * Get's called from the cursor + * Usually you just call {@link Widget#drawInternal(float)} + * + * @param partialTicks difference from last from + */ + void renderMovingState(float partialTicks); + + /** + * @param button the mouse button that's holding down + * @return false if the action should be canceled + */ + boolean onDragStart(int button); + + /** + * The dragging has ended and getState == IDLE + * + * @param successful is false if this returned to it's old position + */ + void onDragEnd(boolean successful); + + void onDrag(int mouseButton, long timeSinceLastClick); + + /** + * Gets called when the mouse is released + * + * @param widget current top most widget below the mouse + * @param isInBounds if the mouse is in the gui bounds + * @return if the location is valid + */ + default boolean canDropHere(@Nullable Widget widget, boolean isInBounds) { + return isInBounds; + } + + /** + * @return the size and pos during move + */ + @Nullable + Rectangle getArea(); + + default boolean shouldRenderChildren() { + return true; + } + + boolean isMoving(); + + void setMoving(boolean moving); +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/widget/IGhostIngredientTarget.java b/src/main/java/com/gtnewhorizons/modularui/api/widget/IGhostIngredientTarget.java new file mode 100644 index 0000000..96e7678 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/widget/IGhostIngredientTarget.java @@ -0,0 +1,30 @@ +package com.gtnewhorizons.modularui.api.widget; + +import com.gtnewhorizons.modularui.api.nei.IGhostIngredientHandler; +import com.gtnewhorizons.modularui.common.internal.wrapper.GhostIngredientWrapper; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Interface for {@link Widget} classes. + * Implement this, to be able to drag items from NEI onto this widget + */ +public interface IGhostIngredientTarget { + + /** + * Called when the users tries to drag an ItemStack from NEI. + * + * @param ingredient object the cursor is holding. Should be validated for Type + * @return the NEI target. Usually an instance of {@link GhostIngredientWrapper} + */ + @Nullable + IGhostIngredientHandler.Target getTarget(@NotNull ItemStack ingredient); + + /** + * Called when this widget is clicked with a object + * + * @param ingredient object the cursor is holding + */ + void accept(@NotNull ItemStack ingredient); +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/widget/IIngredientProvider.java b/src/main/java/com/gtnewhorizons/modularui/api/widget/IIngredientProvider.java new file mode 100644 index 0000000..448d23b --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/widget/IIngredientProvider.java @@ -0,0 +1,15 @@ +package com.gtnewhorizons.modularui.api.widget; + +import org.jetbrains.annotations.Nullable; + +/** + * A wrapper for NEI. Return f.e. a {@link net.minecraft.item.ItemStack} or a {@link net.minecraftforge.fluids.FluidStack} that is stored in this widget. + */ +public interface IIngredientProvider { + + /** + * @return ingredient stored in this widget + */ + @Nullable + Object getIngredient(); +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/widget/ISyncedWidget.java b/src/main/java/com/gtnewhorizons/modularui/api/widget/ISyncedWidget.java new file mode 100644 index 0000000..2a8abb2 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/widget/ISyncedWidget.java @@ -0,0 +1,64 @@ +package com.gtnewhorizons.modularui.api.widget; + +import com.gtnewhorizons.modularui.api.screen.ModularWindow; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.network.PacketBuffer; + +import java.io.IOException; +import java.util.function.Consumer; + +/** + * Implement this to let them synchronize data between server and client. + * See also: {@link Interactable} + */ +public interface ISyncedWidget { + + @SideOnly(Side.CLIENT) + void readOnClient(int id, PacketBuffer buf) throws IOException; + + void readOnServer(int id, PacketBuffer buf) throws IOException; + + /** + * Called AT LEAST each tick on server. Use it to detect and sync changes + * + * @param init true if it is called the first time after init + */ + default void detectAndSendChanges(boolean init) { + } + + /** + * Sends the written data to {@link #readOnServer(int, PacketBuffer)} + * + * @param id helper to determine the type. Must not be -1! + * @param bufBuilder data builder + */ + @SideOnly(Side.CLIENT) + default void syncToServer(int id, Consumer bufBuilder) { + if (!(this instanceof Widget)) { + throw new IllegalStateException("Tried syncing a non Widget ISyncedWidget"); + } + if (id == -1) { + throw new IllegalArgumentException("Id -1 is already reserved for syncing!"); + } + getWindow().getContext().sendClientPacket(id, this, getWindow(), bufBuilder); + } + + /** + * Sends the written data to {@link #readOnClient(int, PacketBuffer)} + * + * @param id helper to determine the type. Must not be -1! + * @param bufBuilder data builder + */ + default void syncToClient(int id, Consumer bufBuilder) { + if (!(this instanceof Widget)) { + throw new IllegalStateException("Tried syncing a non Widget ISyncedWidget"); + } + if (id == -1) { + throw new IllegalArgumentException("Id -1 is already reserved for syncing!"); + } + getWindow().getContext().sendServerPacket(id, this, getWindow(), bufBuilder); + } + + ModularWindow getWindow(); +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/widget/IVanillaSlot.java b/src/main/java/com/gtnewhorizons/modularui/api/widget/IVanillaSlot.java new file mode 100644 index 0000000..f004132 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/widget/IVanillaSlot.java @@ -0,0 +1,15 @@ +package com.gtnewhorizons.modularui.api.widget; + +import com.gtnewhorizons.modularui.common.internal.wrapper.BaseSlot; + +import java.util.Collections; +import java.util.List; + +public interface IVanillaSlot { + + BaseSlot getMcSlot(); + + default List getExtraTooltip() { + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/widget/IWidgetBuilder.java b/src/main/java/com/gtnewhorizons/modularui/api/widget/IWidgetBuilder.java new file mode 100644 index 0000000..3d882a8 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/widget/IWidgetBuilder.java @@ -0,0 +1,91 @@ +package com.gtnewhorizons.modularui.api.widget; + +import com.google.gson.JsonObject; +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.screen.UIBuildContext; +import com.gtnewhorizons.modularui.common.internal.JsonHelper; +import com.gtnewhorizons.modularui.common.internal.JsonLoader; +import com.gtnewhorizons.modularui.common.internal.wrapper.BaseSlot; +import com.gtnewhorizons.modularui.common.widget.SlotGroup; +import com.gtnewhorizons.modularui.common.widget.SlotWidget; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; + +import java.util.Collection; + +public interface IWidgetBuilder> { + + @ApiStatus.Internal + void addWidgetInternal(Widget widget); + + /** + * Add Widget + */ + default T widget(Widget widget) { + addWidgetInternal(widget); + return (T) this; + } + + /** + * Add Widgets + */ + default T widgets(Widget... widgets) { + for (Widget widget : widgets) { + addWidgetInternal(widget); + } + return (T) this; + } + + /** + * Add Widgets + */ + default T widgets(Collection widgets) { + return widgets(widgets.toArray(new Widget[0])); + } + + /** + * Add Widget only when {@code doAdd} is true + */ + default T widgetWhen(boolean doAdd, Widget widget) { + if (doAdd) widget(widget); + return (T) this; + } + + default T drawable(IDrawable drawable) { + return widget(drawable.asWidget()); + } + + default T slot(BaseSlot slot) { + return widget(new SlotWidget(slot)); + } + + default T bindPlayerInventory(EntityPlayer player, Pos2d pos) { + return widget(SlotGroup.playerInventoryGroup(player).setPos(pos)); + } + + default T bindPlayerInventory(EntityPlayer player, int x, int y) { + return widget(SlotGroup.playerInventoryGroup(player).setPos(new Pos2d(x, y))); + } + + default T addFromJson(String mod, String location, UIBuildContext buildContext) { + return addFromJson(new ResourceLocation(mod, location), buildContext); + } + + default T addFromJson(String location, UIBuildContext buildContext) { + return addFromJson(new ResourceLocation(location), buildContext); + } + + default T addFromJson(ResourceLocation location, UIBuildContext buildContext) { + JsonObject json = JsonLoader.GUIS.get(location); + if (json == null) { + ModularUI.logger.error("Couldn't not find json file {}", location); + return (T) this; + } + JsonHelper.parseJson(this, json, buildContext); + return (T) this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/widget/IWidgetDrawable.java b/src/main/java/com/gtnewhorizons/modularui/api/widget/IWidgetDrawable.java new file mode 100644 index 0000000..6b09f49 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/widget/IWidgetDrawable.java @@ -0,0 +1,13 @@ +package com.gtnewhorizons.modularui.api.widget; + +@FunctionalInterface +public interface IWidgetDrawable { + + /** + * Is called before any other draw calls in gui + * + * @param widget widget to draw + * @param partialTicks ticks since last draw + */ + void drawWidgetCustom(Widget widget, float partialTicks); +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/widget/IWidgetParent.java b/src/main/java/com/gtnewhorizons/modularui/api/widget/IWidgetParent.java new file mode 100644 index 0000000..caa2c96 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/widget/IWidgetParent.java @@ -0,0 +1,148 @@ +package com.gtnewhorizons.modularui.api.widget; + +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.screen.ModularUIContext; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * "Parent" widget that can contain another widgets as children. + */ +public interface IWidgetParent { + + Size getSize(); + + Pos2d getAbsolutePos(); + + Pos2d getPos(); + + @Unmodifiable + List getChildren(); + + ModularUIContext getContext(); + + /** + * Called right after the ui is created and right before synced widgets are registered. Last chance to add sub widgets + */ + default void initChildren() { + } + + /** + * Called during rebuild. + * {@link Widget#isAutoPositioned()} must be checked for each child!!! + */ + default void layoutChildren(int maxWidth, int maxHeight) { + } + + @SideOnly(Side.CLIENT) + default void drawChildren(float partialTicks) { + for (Widget child : getChildren()) { + child.drawInternal(partialTicks); + } + } + + default boolean childrenMustBeInBounds() { + return false; + } + + static boolean forEachByLayer(List parent, Function consumer) { + return forEachByLayer(new Wrapper(parent), consumer); + } + + static boolean forEachByLayer(Widget parent, Function consumer) { + if (parent instanceof IWidgetParent) { + return forEachByLayer((IWidgetParent) parent, consumer); + } + return consumer.apply(parent); + } + + static boolean forEachByLayer(IWidgetParent parent, Function consumer) { + return forEachByLayer(parent, false, consumer); + } + + static boolean forEachByLayer(IWidgetParent parent, boolean onlyEnabled, Function consumer) { + LinkedList stack = new LinkedList<>(); + stack.addLast(parent); + while (!stack.isEmpty()) { + IWidgetParent parent1 = stack.pollFirst(); + for (Widget child : parent1.getChildren()) { + if (onlyEnabled && !child.isEnabled()) { + continue; + } + if (consumer.apply(child)) { + return false; + } + if (child instanceof IWidgetParent) { + stack.addLast((IWidgetParent) child); + } + } + } + return true; + } + + static boolean forEachByBranch(IWidgetParent parent, Function consumer) { + for (Widget widget : parent.getChildren()) { + if (consumer.apply(widget)) { + return false; + } + if (widget instanceof IWidgetParent) { + forEachByBranch((IWidgetParent) widget, consumer); + } + } + return true; + } + + static boolean forEachByLayer(Widget parent, Consumer consumer) { + return forEachByLayer(parent, widget -> { + consumer.accept(widget); + return false; + }); + } + + static boolean forEachByLayer(IWidgetParent parent, Consumer consumer) { + return forEachByLayer(parent, widget -> { + consumer.accept(widget); + return false; + }); + } + + class Wrapper implements IWidgetParent { + private final List children; + + public Wrapper(List children) { + this.children = children; + } + + @Override + public Size getSize() { + throw new UnsupportedOperationException(); + } + + @Override + public Pos2d getAbsolutePos() { + throw new UnsupportedOperationException(); + } + + @Override + public Pos2d getPos() { + throw new UnsupportedOperationException(); + } + + @Override + public List getChildren() { + return children; + } + + @Override + public ModularUIContext getContext() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/widget/Interactable.java b/src/main/java/com/gtnewhorizons/modularui/api/widget/Interactable.java new file mode 100644 index 0000000..5fd199b --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/widget/Interactable.java @@ -0,0 +1,138 @@ +package com.gtnewhorizons.modularui.api.widget; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.Minecraft; +import net.minecraft.client.audio.PositionedSoundRecord; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; +import org.lwjgl.input.Keyboard; + +/** + * An interface that handles user interactions. + * These methods get called on the client. + * Can also be used as a listener. + */ +public interface Interactable { + + /** + * called when clicked on the Interactable + * + * @param buttonId the button id (Left == 0, right == 1) + * @param doubleClick if it is the second click within 400ms + * @return if further operations should abort + */ + @ApiStatus.OverrideOnly + default ClickResult onClick(int buttonId, boolean doubleClick) { + return ClickResult.IGNORE; + } + + /** + * called when released a click on the Interactable + * + * @param buttonId the button id (Left == 0, right == 1) + * @return if further operations should abort + */ + @ApiStatus.OverrideOnly + default boolean onClickReleased(int buttonId) { + return false; + } + + /** + * called when the interactable is focused and the mouse gets dragged + * + * @param buttonId the button id (Left == 0, right == 1) + * @param deltaTime milliseconds since last mouse event + */ + @ApiStatus.OverrideOnly + default void onMouseDragged(int buttonId, long deltaTime) { + } + + /** + * Called the mouse wheel moved + * + * @param direction -1 for down, 1 for up + */ + @ApiStatus.OverrideOnly + default boolean onMouseScroll(int direction) { + return false; + } + + /** + * called when the interactable is focused and a key is pressed + * + * @param character the typed character. Is equal to {@link Character#MIN_VALUE} if it's not a char + * @param keyCode code of the typed key. See {@link Keyboard} + * @return if further operations should abort + */ + @ApiStatus.OverrideOnly + default boolean onKeyPressed(char character, int keyCode) { + return false; + } + + /** + * @return if left or right ctrl/cmd is pressed + */ + @SideOnly(Side.CLIENT) + static boolean hasControlDown() { + return GuiScreen.isCtrlKeyDown(); + } + + /** + * @return if left or right shift is pressed + */ + @SideOnly(Side.CLIENT) + static boolean hasShiftDown() { + return GuiScreen.isShiftKeyDown(); + } + + // todo resurrect this +// /** +// * @return if alt or alt gr is pressed +// */ +// @SideOnly(Side.CLIENT) +// static boolean hasAltDown() { +// return GuiScreen.isAltKeyDown(); +// } + + /** + * @param key key id, see {@link Keyboard} + * @return if the key is pressed + */ + @SideOnly(Side.CLIENT) + static boolean isKeyPressed(int key) { + return Keyboard.isKeyDown(key); + } + + /** + * Plays the default button click sound + */ + @SideOnly(Side.CLIENT) + static void playButtonClickSound() { + Minecraft.getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.func_147674_a(new ResourceLocation("gui.button.press"), 1.0F)); + } + + enum ClickResult { + /** + * Nothing happened on this click + */ + IGNORE, + /** + * Nothing happened, but it was clicked + */ + ACKNOWLEDGED, + /** + * Nothing happened and no other hovered should get interacted + */ + REJECT, + /** + * Success, but don't try to get focus + */ + ACCEPT, + /** + * Successfully clicked. Should be returned if it should try to receive focus + */ + SUCCESS + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/widget/Widget.java b/src/main/java/com/gtnewhorizons/modularui/api/widget/Widget.java new file mode 100644 index 0000000..4bf9083 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/widget/Widget.java @@ -0,0 +1,816 @@ +package com.gtnewhorizons.modularui.api.widget; + +import com.google.gson.JsonObject; +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.drawable.Text; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.screen.ModularUIContext; +import com.gtnewhorizons.modularui.api.screen.ModularWindow; +import com.gtnewhorizons.modularui.common.internal.JsonHelper; +import com.gtnewhorizons.modularui.common.internal.Theme; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.network.PacketBuffer; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * This class draws a functional element of ModularUI + */ +public abstract class Widget { + + // gui + private String name = ""; + private ModularWindow window = null; + private IWidgetParent parent = null; + + // sizing and positioning + protected Size size = Size.ZERO; + protected Pos2d relativePos = Pos2d.ZERO; + protected Pos2d pos = Pos2d.ZERO; + protected Pos2d fixedPos = null; + @Nullable + private SizeProvider sizeProvider; + @Nullable + private PosProvider posProvider; + private boolean fillParent = false; + private boolean autoSized = true; + private boolean autoPositioned = true; + + // flags and stuff + protected boolean enabled = true; + private int layer = -1; + private boolean respectNEIArea = false; + private boolean tooltipDirty = true; + private boolean firstRebuild = true; + + // visuals + @Nullable + private IDrawable[] background; + private final List additionalTooltip = new ArrayList<>(); + private final List mainTooltip = new ArrayList<>(); + private int tooltipShowUpDelay = 0; + @Nullable + private String debugLabel; + + @Nullable + private Consumer ticker; + + public Widget() { + } + + public Widget(Size size) { + this(); + setSize(size); + } + + public Widget(Pos2d pos) { + this(); + setPos(pos); + } + + public Widget(Size size, Pos2d pos) { + this(); + setSize(size); + setPos(pos); + } + + /** + * @return if we are on logical client or server + */ + public boolean isClient() { + return getContext().isClient(); + } + + /** + * Called when this widget is created from json. Make sure to call super.readJson(json, type); + * + * @param json the widget json + * @param type the type this widget was created with + */ + public void readJson(JsonObject json, String type) { + this.name = JsonHelper.getString(json, "", "name"); + this.relativePos = JsonHelper.getElement(json, relativePos, Pos2d::ofJson, "pos"); + this.fixedPos = JsonHelper.getElement(json, null, Pos2d::ofJson, "fixedPos"); + this.size = JsonHelper.getElement(json, size, Size::ofJson, "size"); + this.fillParent = JsonHelper.getBoolean(json, false, "fillParent"); + this.enabled = JsonHelper.getBoolean(json, true, "enabled"); + this.autoSized = JsonHelper.getBoolean(json, !json.has("size"), "autoSized"); + IDrawable drawable = JsonHelper.getObject(json, null, IDrawable::ofJson, "drawable", "background"); + if (drawable != null) { + setBackground(drawable); + } + } + + + //==== Internal methods ==== + + /** + * You shall not call this + */ + @ApiStatus.Internal + public final void initialize(ModularWindow window, IWidgetParent parent, int layer) { + if (window == null || parent == null || isInitialised()) { + throw new IllegalStateException("Illegal initialise call to widget!! " + toString()); + } + this.window = window; + this.parent = parent; + this.layer = layer; + + if (this.respectNEIArea) { + getContext().registerExclusionZone(this); + } + + onInit(); + + if (this instanceof IWidgetParent) { + int nextLayer = layer + 1; + for (Widget widget : ((IWidgetParent) this).getChildren()) { + widget.initialize(this.window, (IWidgetParent) this, nextLayer); + } + } + + onPostInit(); + } + + @SideOnly(Side.CLIENT) + @ApiStatus.Internal + public final void buildTopToBottom(Dimension constraints) { + if (!isInitialised()) { + return; + } + int cw = constraints.width, ch = constraints.height; + if (this instanceof IWidgetParent) { + modifyConstraints(constraints); + cw = constraints.width; + ch = constraints.height; + IWidgetParent parentThis = (IWidgetParent) this; + for (Widget widget : parentThis.getChildren()) { + widget.buildTopToBottom(constraints); + } + parentThis.layoutChildren(cw, ch); + } + if (isAutoSized() && !isFillParent()) { + this.size = determineSize(cw, ch); + } + } + + /** + * You shall not call this + */ + @SideOnly(Side.CLIENT) + @ApiStatus.Internal + public final void buildBottomToTop() { + if (!isInitialised()) { + return; + } + if (isAutoSized() && isFillParent()) { + this.size = parent.getSize(); + } else if (this.sizeProvider != null) { + this.size = this.sizeProvider.getSize(getContext().getScaledScreenSize(), getWindow(), this.parent); + } + // calculate positions + if (isFixed() && !isAutoPositioned()) { + relativePos = fixedPos.subtract(parent.getAbsolutePos()); + pos = fixedPos; + } else { + if (this.posProvider != null) { + this.relativePos = this.posProvider.getPos(getContext().getScaledScreenSize(), getWindow(), this.parent); + } + this.pos = this.parent.getAbsolutePos().add(this.relativePos); + } + + if (this instanceof IWidgetParent) { + IWidgetParent parentThis = (IWidgetParent) this; + // rebuild children + for (Widget child : parentThis.getChildren()) { + child.buildBottomToTop(); + } + } + if (firstRebuild) { + onFirstRebuild(); + firstRebuild = false; + } + onRebuild(); + } + + @SideOnly(Side.CLIENT) + @ApiStatus.Internal + public final void drawInternal(float partialTicks) { + drawInternal(partialTicks, false); + } + + /** + * You shall not call this + */ + @SideOnly(Side.CLIENT) + @ApiStatus.Internal + public final void drawInternal(float partialTicks, boolean ignoreEnabled) { + onFrameUpdate(); + if (isEnabled() || ignoreEnabled) { + GlStateManager.pushMatrix(); + Pos2d windowPos = getWindow().getPos(); + Size windowSize = getWindow().getSize(); + int alpha = getWindow().getAlpha(); + float scale = getWindow().getScale(); + float sf = 1 / scale; + // translate to center according to scale + float x = (windowPos.x + windowSize.width / 2f * (1 - scale) + (pos.x - windowPos.x) * scale) * sf; + float y = (windowPos.y + windowSize.height / 2f * (1 - scale) + (pos.y - windowPos.y) * scale) * sf; + GlStateManager.translate(x, y, 0); + GlStateManager.color(1, 1, 1, alpha); + GlStateManager.enableBlend(); + drawBackground(partialTicks); + draw(partialTicks); + GlStateManager.popMatrix(); + + if (this instanceof IWidgetParent) { + ((IWidgetParent) this).drawChildren(partialTicks); + } + } + } + + + //==== Sizing & Positioning ==== + + /** + * Called before this widget ask for the children Size. + * + * @param constraints constraints to modify + */ + @SideOnly(Side.CLIENT) + protected void modifyConstraints(Dimension constraints) { + } + + /** + * Called during rebuild + * + * @param maxWidth maximum width to fit in parent + * @param maxHeight maximum height to fit in parent + * @return the preferred size + */ + @SideOnly(Side.CLIENT) + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + return new Size(maxWidth, maxHeight); + } + + /** + * Called after this widget is rebuild aka size and pos are set. + */ + @ApiStatus.OverrideOnly + @SideOnly(Side.CLIENT) + public void onRebuild() { + } + + /** + * Called the first time this widget is fully build + */ + @ApiStatus.OverrideOnly + @SideOnly(Side.CLIENT) + public void onFirstRebuild() { + } + + /** + * Causes the modular ui to re-layout all children next screen update + */ + public void checkNeedsRebuild() { + if (isInitialised() && isClient()) { + window.markNeedsRebuild(); + } + } + + + //==== Update ==== + + /** + * Called once per tick + */ + @ApiStatus.OverrideOnly + @SideOnly(Side.CLIENT) + public void onScreenUpdate() { + } + + /** + * Called each frame, approximately 60 times per second + */ + @ApiStatus.OverrideOnly + @SideOnly(Side.CLIENT) + public void onFrameUpdate() { + } + + + //==== Rendering ==== + + @SideOnly(Side.CLIENT) + public void drawBackground(float partialTicks) { + IDrawable[] background = getBackground(); + if (background != null) { + int themeColor = Theme.INSTANCE.getColor(getBackgroundColorKey()); + for (IDrawable drawable : background) { + if (drawable != null) { + drawable.applyThemeColor(themeColor); + drawable.draw(Pos2d.ZERO, getSize(), partialTicks); + } + } + } + } + + /** + * Draw the widget here + * + * @param partialTicks ticks since last draw + */ + @ApiStatus.OverrideOnly + @SideOnly(Side.CLIENT) + public void draw(float partialTicks) { + } + + /** + * Is called after all widgets of the window are drawn. Can be used for special tooltip rendering. + * + * @param partialTicks ticks since last draw + */ + @ApiStatus.OverrideOnly + @SideOnly(Side.CLIENT) + public void drawInForeground(float partialTicks) { + } + + /** + * Called after {@link #notifyTooltipChange()} is called. Result list is cached + * + * @param tooltip tooltip + */ + @ApiStatus.OverrideOnly + @SideOnly(Side.CLIENT) + public void buildTooltip(List tooltip) { + } + + /** + * @return the color key for the background + * @see Theme + */ + @SideOnly(Side.CLIENT) + @Nullable + public String getBackgroundColorKey() { + return Theme.KEY_BACKGROUND; + } + + + //==== Lifecycle ==== + + /** + * Called once when the window opens, before children get initialised. + */ + @ApiStatus.OverrideOnly + public void onInit() { + } + + /** + * Called once when the window opens, after children get initialised. + */ + @ApiStatus.OverrideOnly + public void onPostInit() { + } + + /** + * Called when another window opens over the current one + * or when this window is active and it closes + */ + @ApiStatus.OverrideOnly + public void onPause() { + } + + /** + * Called when this window becomes active after being paused + */ + @ApiStatus.OverrideOnly + public void onResume() { + } + + /** + * Called when this window closes + */ + @ApiStatus.OverrideOnly + public void onDestroy() { + } + + + //==== focus ==== + + /** + * Called when this widget is clicked. Also acts as a onReceiveFocus method. + * + * @return if the ui focus should be set to this widget + */ + @ApiStatus.OverrideOnly + @SideOnly(Side.CLIENT) + public boolean shouldGetFocus() { + return this instanceof Interactable; + } + + /** + * Called when this widget was focused and now something else is focused + */ + @ApiStatus.OverrideOnly + @SideOnly(Side.CLIENT) + public void onRemoveFocus() { + } + + /** + * @return if the modular ui currently has this widget focused + */ + @SideOnly(Side.CLIENT) + public boolean isFocused() { + return getContext().getCursor().isFocused(this); + } + + /** + * Removes the focus from this widget. Does nothing if it isn't focused + */ + @SideOnly(Side.CLIENT) + public void removeFocus() { + getContext().getCursor().removeFocus(this); + } + + @SideOnly(Side.CLIENT) + public boolean canHover() { + return this instanceof Interactable || hasTooltip(); + } + + /** + * @return if this is currently the top most widget under the mouse + */ + @SideOnly(Side.CLIENT) + public boolean isHovering() { + return getContext().getCursor().isHovering(this); + } + + @SideOnly(Side.CLIENT) + public boolean isRightBelowMouse() { + return getContext().getCursor().isRightBelow(this); + } + + + //==== Debug ==== + + @Override + public String toString() { + if (debugLabel == null && name.isEmpty()) { + return getClass().getSimpleName(); + } + if (debugLabel == null) { + return getClass().getSimpleName() + "#" + name; + } + return getClass().getSimpleName() + "#" + name + "#" + debugLabel; + } + + + //==== Getter ==== + + public boolean isUnderMouse(Pos2d mousePos) { + return mousePos.isInside(pos, size); + } + + public String getName() { + return name; + } + + public ModularUIContext getContext() { + return window.getContext(); + } + + public ModularWindow getWindow() { + return window; + } + + public IWidgetParent getParent() { + return parent; + } + + @SideOnly(Side.CLIENT) + public Rectangle getArea() { + return new Rectangle(pos.x, pos.y, size.width, size.height); + } + + public Pos2d getPos() { + return relativePos; + } + + public Pos2d getAbsolutePos() { + return pos; + } + + public Size getSize() { + return size; + } + + public boolean isEnabled() { + return enabled; + } + + public int getLayer() { + return layer; + } + + public final boolean isInitialised() { + return window != null; + } + + public boolean isFixed() { + return fixedPos != null; + } + + public boolean isAutoSized() { + return autoSized; + } + + public boolean isAutoPositioned() { + return autoPositioned; + } + + public boolean isFillParent() { + return fillParent; + } + + @Nullable + public String getDebugLabel() { + return debugLabel; + } + + @Nullable + public IDrawable[] getBackground() { + return background; + } + + private void checkTooltip() { + if (this.tooltipDirty) { + this.mainTooltip.clear(); + buildTooltip(this.mainTooltip); + this.tooltipDirty = false; + } + } + + public void notifyTooltipChange() { + this.tooltipDirty = true; + } + + public boolean hasTooltip() { + checkTooltip(); + return !this.mainTooltip.isEmpty() || !this.additionalTooltip.isEmpty(); + } + + public List getTooltip() { + if (!hasTooltip()) { + return Collections.emptyList(); + } + List tooltip = new ArrayList<>(this.mainTooltip); + tooltip.addAll(this.additionalTooltip); + return tooltip; + } + + public int getTooltipShowUpDelay() { + return tooltipShowUpDelay; + } + + @Nullable + public Consumer getTicker() { + return ticker; + } + + public boolean intersects(Widget widget) { + return !(widget.getAbsolutePos().x > getAbsolutePos().x + getSize().width || + widget.getAbsolutePos().x + widget.getSize().width < getAbsolutePos().x || + widget.getAbsolutePos().y > getAbsolutePos().y + getSize().height || + widget.getAbsolutePos().y + widget.getSize().height < getAbsolutePos().y); + } + + public Rectangle getRectangle() { + return new Rectangle(pos.x, pos.y, size.width, size.height); + } + + + //==== Setter/Builder ==== + + /** + * If widgets are NOT enabled, they won't be rendered and they can not be interacted with. + * + * @param enabled if this widget should be enabled + */ + public Widget setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public Widget setSize(int width, int height) { + return setSize(new Size(width, height)); + } + + /** + * Forces the widget to a size + * + * @param size size of this widget + */ + public Widget setSize(Size size) { + checkNeedsRebuild(); + this.autoSized = false; + this.fillParent = false; + this.size = size; + return this; + } + + public Widget setSizeProvider(SizeProvider sizeProvider) { + this.autoSized = false; + this.fillParent = false; + this.sizeProvider = sizeProvider; + return this; + } + + public void setSizeSilent(Size size) { + this.size = size; + } + + public Widget setPos(int x, int y) { + return setPos(new Pos2d(x, y)); + } + + /** + * Sets this widget to a pos relative to the parents pos + * + * @param relativePos relative pos + */ + public Widget setPos(Pos2d relativePos) { + checkNeedsRebuild(); + this.autoPositioned = false; + this.relativePos = relativePos; + this.fixedPos = null; + return this; + } + + public void setPosSilent(Pos2d relativePos) { + this.relativePos = relativePos; + if (isInitialised()) { + this.pos = parent.getAbsolutePos().add(this.relativePos); + if (this instanceof IWidgetParent) { + for (Widget child : ((IWidgetParent) this).getChildren()) { + child.setPosSilent(child.getPos()); + } + } + } + } + + public Widget setFixedPos(int x, int y) { + return setFixedPos(new Pos2d(x, y)); + } + + /** + * Sets the widgets pos to a fixed point. It will never move + * + * @param pos pos to fix this widget to + */ + public Widget setFixedPos(@Nullable Pos2d pos) { + checkNeedsRebuild(); + this.autoPositioned = false; + this.fixedPos = pos; + return this; + } + + public Widget setPosProvider(PosProvider posProvider) { + this.autoPositioned = false; + this.posProvider = posProvider; + this.fixedPos = null; + return this; + } + + /** + * Sets the widgets size to its parent size + */ + public Widget fillParent() { + this.fillParent = true; + this.autoSized = true; + this.autoPositioned = false; + return this; + } + + /** + * Sets a static background drawable. + * + * @param drawables background to render + */ + public Widget setBackground(IDrawable... drawables) { + this.background = drawables; + return this; + } + + /** + * Adds a line to the tooltip + */ + public Widget addTooltip(Text tooltip) { + this.additionalTooltip.add(tooltip); + return this; + } + + /** + * Adds a line to the tooltip + */ + public Widget addTooltip(String tooltip) { + return addTooltip(new Text(tooltip)); + } + + public Widget setTooltipShowUpDelay(int tooltipShowUpDelay) { + this.tooltipShowUpDelay = tooltipShowUpDelay; + return this; + } + + public Widget setDebugLabel(String debugLabel) { + this.debugLabel = debugLabel; + return this; + } + + /** + * Applies this action each tick on client. Can be used to dynamically enable/disable the widget + * + * @param ticker tick function + */ + public Widget setTicker(@Nullable Consumer ticker) { + this.ticker = ticker; + return this; + } + + /** + * Consumes the widget. Can be used to apply advanced actions in a builder. + * + * @param widgetConsumer action to apply + */ + public Widget consume(Consumer widgetConsumer) { + widgetConsumer.accept(this); + return this; + } + + /** + * Makes NEI always respect this widget's area. + * This should be used when the widget is outside its windows area and overlaps with NEI + */ + public Widget respectAreaInNEI() { + if (!this.respectNEIArea) { + this.respectNEIArea = true; + if (isInitialised()) { + getContext().registerExclusionZone(this); + } + } + return this; + } + + + //==== Utility ==== + + public interface SizeProvider { + Size getSize(Size screenSize, ModularWindow window, IWidgetParent parent); + } + + public interface PosProvider { + Pos2d getPos(Size screenSize, ModularWindow window, IWidgetParent parent); + } + + public static class ClickData { + public final int mouseButton; + public final boolean doubleClick; + public final boolean shift; + public final boolean ctrl; +// public final boolean alt; + + public ClickData(int mouseButton, boolean doubleClick, boolean shift, boolean ctrl) { + this.mouseButton = mouseButton; + this.doubleClick = doubleClick; + this.shift = shift; + this.ctrl = ctrl; +// this.alt = alt; + } + + public void writeToPacket(PacketBuffer buffer) { + short data = (short) (mouseButton & 0xFF); + if (doubleClick) data |= 1 << 8; + if (shift) data |= 1 << 9; + if (ctrl) data |= 1 << 10; + buffer.writeShort(data); + } + + public static ClickData readPacket(PacketBuffer buffer) { + short data = buffer.readShort(); + return new ClickData(data & 0xFF, (data & 1 << 8) > 0, (data & 1 << 9) > 0, (data & 1 << 10) > 0); + } + + @SideOnly(Side.CLIENT) + public static ClickData create(int mouse, boolean doubleClick) { + return new ClickData(mouse, doubleClick, Interactable.hasShiftDown(), Interactable.hasControlDown()); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/widget/scroll/IHorizontalScrollable.java b/src/main/java/com/gtnewhorizons/modularui/api/widget/scroll/IHorizontalScrollable.java new file mode 100644 index 0000000..f38d5ab --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/widget/scroll/IHorizontalScrollable.java @@ -0,0 +1,18 @@ +package com.gtnewhorizons.modularui.api.widget.scroll; + +import org.jetbrains.annotations.Range; + +public interface IHorizontalScrollable { + + void setHorizontalScrollOffset(int offset); + + int getHorizontalScrollOffset(); + + default @Range(from = 1, to = 20) int getHorizontalBarHeight() { + return 2; + } + + int getVisibleWidth(); + + int getActualWidth(); +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/widget/scroll/IVerticalScrollable.java b/src/main/java/com/gtnewhorizons/modularui/api/widget/scroll/IVerticalScrollable.java new file mode 100644 index 0000000..a26eed4 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/widget/scroll/IVerticalScrollable.java @@ -0,0 +1,16 @@ +package com.gtnewhorizons.modularui.api.widget.scroll; + +public interface IVerticalScrollable { + + void setVerticalScrollOffset(int offset); + + int getVerticalScrollOffset(); + + default int getVerticalBarWidth() { + return 2; + } + + int getVisibleHeight(); + + int getActualHeight(); +} diff --git a/src/main/java/com/gtnewhorizons/modularui/api/widget/scroll/ScrollType.java b/src/main/java/com/gtnewhorizons/modularui/api/widget/scroll/ScrollType.java new file mode 100644 index 0000000..104b371 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/api/widget/scroll/ScrollType.java @@ -0,0 +1,6 @@ +package com.gtnewhorizons.modularui.api.widget.scroll; + +public enum ScrollType { + HORIZONTAL, + VERTICAL +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/builder/UIBuilder.java b/src/main/java/com/gtnewhorizons/modularui/common/builder/UIBuilder.java new file mode 100644 index 0000000..40f5bf2 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/builder/UIBuilder.java @@ -0,0 +1,39 @@ +package com.gtnewhorizons.modularui.common.builder; + +import com.gtnewhorizons.modularui.api.screen.IContainerCreator; +import com.gtnewhorizons.modularui.api.screen.IGuiCreator; + +/** + * Builder for {@link UIInfo} + */ +public class UIBuilder { + + private static final IContainerCreator DUMMY_CONTAINER_CREATOR = (player, world, x, y, z) -> null; + private static final IGuiCreator DUMMY_GUI_CREATOR = (player, world, x, y, z) -> null; + + public static UIBuilder of() { + return new UIBuilder(); + } + + private IContainerCreator containerCreator = DUMMY_CONTAINER_CREATOR; + private IGuiCreator guiCreator = DUMMY_GUI_CREATOR; + + private UIBuilder() { + + } + + public UIBuilder container(IContainerCreator containerCreator) { + this.containerCreator = containerCreator; + return this; + } + + public UIBuilder gui(IGuiCreator guiCreator) { + this.guiCreator = guiCreator; + return this; + } + + public UIInfo build() { + return new UIInfo<>(this.containerCreator, this.guiCreator); + } + +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/builder/UIInfo.java b/src/main/java/com/gtnewhorizons/modularui/common/builder/UIInfo.java new file mode 100644 index 0000000..0076ab3 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/builder/UIInfo.java @@ -0,0 +1,61 @@ +package com.gtnewhorizons.modularui.common.builder; + +import com.gtnewhorizons.modularui.api.UIInfos; +import com.gtnewhorizons.modularui.common.internal.network.NetworkUtils; +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.api.screen.IContainerCreator; +import com.gtnewhorizons.modularui.api.screen.IGuiCreator; +import com.gtnewhorizons.modularui.common.internal.InternalUIMapper; +import cpw.mods.fml.common.network.internal.FMLNetworkHandler; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; + +import java.util.function.Function; + +/** + * Responsible for registering actual Container and GuiContainer, and opening them. + */ +public class UIInfo { + + private final int id; + private final CC containerCreator; + private final GC guiCreator; + + /** + * @param containerCreator {@link IContainerCreator} + * @param guiCreator {@link IGuiCreator} + */ + UIInfo(CC containerCreator, GC guiCreator) { + this.id = InternalUIMapper.getInstance().register(containerCreator, guiCreator); + this.containerCreator = containerCreator; + this.guiCreator = guiCreator; + } + + /** + * Open GUI of TileEntity at given position. + * This should be called only by logical server. + * For client-only GUI, use {@link UIInfos#openClientUI(EntityPlayer, Function)} + */ + public void open(EntityPlayer player, World world, int x, int y, int z) { + if (NetworkUtils.isClient(player)) { + ModularUI.logger.warn("Please use UIInfos.openClientUI to open a client only ui!"); + } + FMLNetworkHandler.openGui(player, ModularUI.INSTANCE, id, world, x, y, z); + } + + /** + * Open GUI of TileEntity at given position. + * This should be called only by logical server. + * For client-only GUI, use {@link UIInfos#openClientUI(EntityPlayer, Function)} + */ + public void open(EntityPlayer player, World world, Vec3 pos) { + open(player, world, (int)pos.xCoord, (int)pos.yCoord, (int)pos.zCoord); + } + + public void open(EntityPlayer player) { + open(player, player.getEntityWorld(), (int) player.posX, (int) player.posY, (int) player.posZ); + } + +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/InternalUIMapper.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/InternalUIMapper.java new file mode 100644 index 0000000..ceebbfa --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/InternalUIMapper.java @@ -0,0 +1,65 @@ +package com.gtnewhorizons.modularui.common.internal; + +import com.gtnewhorizons.modularui.ModularUI; +import com.gtnewhorizons.modularui.api.screen.IContainerCreator; +import com.gtnewhorizons.modularui.api.screen.IGuiCreator; +import cpw.mods.fml.common.network.IGuiHandler; +import cpw.mods.fml.common.network.NetworkRegistry; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class InternalUIMapper implements IGuiHandler { + + private static InternalUIMapper INSTANCE; + + public static InternalUIMapper getInstance() { + if (INSTANCE == null) { + INSTANCE = new InternalUIMapper(); + } + return INSTANCE; + } + + private int id; + private final List serverContainers; + private final List clientGuis; + + public InternalUIMapper() { + if (ModularUI.INSTANCE == null) { + throw new NullPointerException("Something went wrong! Mod instance should not be null!"); + } + NetworkRegistry.INSTANCE.registerGuiHandler(ModularUI.INSTANCE, this); + this.serverContainers = new ArrayList<>(); + this.clientGuis = new ArrayList<>(); + } + + public int register(CC containerCreator, GC guiCreator) { + this.serverContainers.add(containerCreator); + this.clientGuis.add(guiCreator); + return id++; + } + + @Nullable + @Override + public Object getServerGuiElement(int id, EntityPlayer player, World world, int x, int y, int z) { + return serverContainers.get(id).create(player, world, x, y, z); + } + + @SideOnly(Side.CLIENT) + @Nullable + @Override + public Object getClientGuiElement(int id, EntityPlayer player, World world, int x, int y, int z) { + Object screen = clientGuis.get(id).create(player, world, x, y, z); + if (screen != null && !(screen instanceof GuiScreen)) { + throw new RuntimeException("The returned Object of IGuiCreator must be a instance of GuiScreen!"); + } + return screen; + } + +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/JsonHelper.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/JsonHelper.java new file mode 100644 index 0000000..c4ad5a2 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/JsonHelper.java @@ -0,0 +1,118 @@ +package com.gtnewhorizons.modularui.common.internal; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.gtnewhorizons.modularui.api.screen.UIBuildContext; +import com.gtnewhorizons.modularui.api.widget.IWidgetBuilder; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.common.widget.WidgetJsonRegistry; + +import java.util.function.Function; + +public class JsonHelper { + + public static void parseJson(IWidgetBuilder widgetBuilder, JsonObject json, UIBuildContext buildContext) { + if (json.has("widgets")) { + JsonArray widgets = json.getAsJsonArray("widgets"); + for (JsonElement jsonElement : widgets) { + if (jsonElement.isJsonObject()) { + JsonObject jsonWidget = jsonElement.getAsJsonObject(); + Widget widget = null; + String type = null; + if (!jsonWidget.has("type")) { + continue; + } + type = jsonWidget.get("type").getAsString(); + WidgetJsonRegistry.WidgetFactory widgetFactory = WidgetJsonRegistry.getFactory(type); + if (widgetFactory != null) { + widget = widgetFactory.create(buildContext.getPlayer()); + } + if (widget == null) { + continue; + } + widget.readJson(jsonWidget, type); + if (!widget.getName().isEmpty()) { + buildContext.addJsonWidgets(widget.getName(), widget); + } + widgetBuilder.widget(widget); + if (widget instanceof IWidgetBuilder && jsonWidget.has("widgets")) { + parseJson((IWidgetBuilder) widget, jsonWidget, buildContext); + } + } + } + } + } + + public static float getFloat(JsonObject json, float defaultValue, String... keys) { + for (String key : keys) { + if (json.has(key)) { + JsonElement jsonElement = json.get(key); + if (jsonElement.isJsonPrimitive()) { + return jsonElement.getAsFloat(); + } + return defaultValue; + } + } + return defaultValue; + } + + public static int getInt(JsonObject json, int defaultValue, String... keys) { + for (String key : keys) { + if (json.has(key)) { + JsonElement jsonElement = json.get(key); + if (jsonElement.isJsonPrimitive()) { + return jsonElement.getAsInt(); + } + return defaultValue; + } + } + return defaultValue; + } + + public static boolean getBoolean(JsonObject json, boolean defaultValue, String... keys) { + for (String key : keys) { + if (json.has(key)) { + JsonElement jsonElement = json.get(key); + if (jsonElement.isJsonPrimitive()) { + return jsonElement.getAsBoolean(); + } + return defaultValue; + } + } + return defaultValue; + } + + public static String getString(JsonObject json, String defaultValue, String... keys) { + for (String key : keys) { + if (json.has(key)) { + JsonElement jsonElement = json.get(key); + return jsonElement.getAsString(); + } + } + return defaultValue; + } + + public static T getObject(JsonObject json, T defaultValue, Function factory, String... keys) { + for (String key : keys) { + if (json.has(key)) { + JsonElement jsonElement = json.get(key); + if (jsonElement.isJsonObject()) { + return factory.apply(jsonElement.getAsJsonObject()); + } + return defaultValue; + } + } + return defaultValue; + } + + public static T getElement(JsonObject json, T defaultValue, Function factory, String... keys) { + for (String key : keys) { + if (json.has(key)) { + JsonElement jsonElement = json.get(key); + return factory.apply(jsonElement); + } + } + return defaultValue; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/JsonLoader.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/JsonLoader.java new file mode 100644 index 0000000..3228913 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/JsonLoader.java @@ -0,0 +1,65 @@ +package com.gtnewhorizons.modularui.common.internal; + +import com.google.gson.*; +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.api.forge.CraftingHelper; +import cpw.mods.fml.common.Loader; +import cpw.mods.fml.common.ModContainer; +import net.minecraft.util.ResourceLocation; +//import net.minecraftforge.common.crafting.CraftingHelper; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JsonLoader { + + public static final Map GUIS = new HashMap<>(); + public static final JsonParser jsonParser = new JsonParser(); + public static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + public static void loadJson() { + GUIS.clear(); + List mods = Loader.instance().getActiveModList(); + mods.forEach((mod) -> { + String id = mod.getModId(); + CraftingHelper.findFiles(mod, String.format("assets/%s/guis", id), (path) -> Files.exists(path), + (path, file) -> { + if (file.toString().endsWith(".json")) { + JsonObject json = tryExtractFromFile(file); + if (json != null) { + String fileStr = file.toString().replaceAll("\\\\", "/"); + String guiName = fileStr.substring(fileStr.indexOf("guis/") + 5, fileStr.length() - 5); + + ResourceLocation bookId = new ResourceLocation(id, guiName); + GUIS.put(bookId, json); + } + } + return true; + }, false, true); + }); + ModularUI.logger.info("Loaded {} guis from json", GUIS.size()); + } + + public static JsonObject tryExtractFromFile(Path filePath) { + try (InputStream fileStream = Files.newInputStream(filePath)) { + InputStreamReader streamReader = new InputStreamReader(fileStream, StandardCharsets.UTF_8); + return jsonParser.parse(streamReader).getAsJsonObject(); + } catch (IOException exception) { + ModularUI.logger.error("Failed to read file on path {}", filePath, exception); + } catch (JsonParseException exception) { + ModularUI.logger.error("Failed to extract json from file", exception); + } catch (Exception exception) { + ModularUI.logger.error("Failed to extract json from file on path {}", filePath, exception); + } + + return null; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/Theme.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/Theme.java new file mode 100644 index 0000000..0f4e74a --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/Theme.java @@ -0,0 +1,85 @@ +package com.gtnewhorizons.modularui.common.internal; + +import com.gtnewhorizons.modularui.ModularUI; +import com.gtnewhorizons.modularui.api.math.Color; +import com.google.gson.JsonObject; + +//import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; +//import it.unimi.dsi.fastutil.objects.Object2IntMap; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; + +public class Theme { + + public static final Theme INSTANCE = new Theme(); + + public static final String KEY_BACKGROUND = "bg"; + public static final String KEY_BUTTON = "button"; + public static final String KEY_TEXT = "text"; + public static final String KEY_ITEM_SLOT = "itemslot"; + public static final String KEY_FLUID_SLOT = "fluidslot"; + public static final String KEY_SLOT_HIGHLIGHT = "slothighlight"; + + private final HashMap colors = new HashMap(32); + + private Theme() { + registerThemeColor(KEY_BACKGROUND); + registerThemeColor(KEY_BUTTON); + registerThemeColor(KEY_TEXT, 0x404040); + registerThemeColor(KEY_ITEM_SLOT); + registerThemeColor(KEY_FLUID_SLOT); + registerThemeColor(KEY_SLOT_HIGHLIGHT, Color.withAlpha(Color.WHITE.normal, 0x80)); + } + + public void registerThemeColor(String name) { + registerThemeColor(name, 0xFFFFFFFF); + } + + public void registerThemeColor(String name, int value) { + if (colors.containsKey(name)) { + ModularUI.logger.error("Theme already has a color with key {}", name); + return; + } + colors.put(name, value); + } + + public int getColor(@Nullable String key) { + return key == null ? 0xFFFFFFFF : colors.getOrDefault(key, 0xFFFFFFFF); + } + + public int getBackground() { + return getColor(KEY_BACKGROUND); + } + + public int getButton() { + return getColor(KEY_BUTTON); + } + + public int getText() { + return getColor(KEY_TEXT); + } + + public int getItemSlot() { + return getColor(KEY_ITEM_SLOT); + } + + public int getFluidSlot() { + return getColor(KEY_FLUID_SLOT); + } + + public int getSlotHighlight() { + return getColor(KEY_SLOT_HIGHLIGHT); + } + + public JsonObject readTheme(JsonObject json) { + for (String key : colors.keySet()) { + if (json.has(key)) { + colors.put(key, json.get(key).getAsInt()); + } else { + json.addProperty(key, colors.get(key)); + } + } + return json; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/network/CWidgetUpdate.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/network/CWidgetUpdate.java new file mode 100644 index 0000000..f777272 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/network/CWidgetUpdate.java @@ -0,0 +1,54 @@ +package com.gtnewhorizons.modularui.common.internal.network; + +import com.gtnewhorizons.modularui.ModularUI; +import com.gtnewhorizons.modularui.common.internal.wrapper.ModularUIContainer; + +import com.gtnewhorizons.modularui.api.screen.ModularUIContext; +import net.minecraft.inventory.Container; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.PacketBuffer; + +import java.io.IOException; + +public class CWidgetUpdate implements IPacket { + + public int widgetId; + public PacketBuffer packet; + + public CWidgetUpdate(PacketBuffer packet, int widgetId) { + this.packet = packet; + this.widgetId = widgetId; + } + + public CWidgetUpdate() { + } + + @Override + public void decode(PacketBuffer buf) { + this.widgetId = buf.readVarIntFromBuffer(); + this.packet = NetworkUtils.readPacketBuffer(buf); + } + + @Override + public void encode(PacketBuffer buf) { + buf.writeVarIntToBuffer(widgetId); + NetworkUtils.writePacketBuffer(buf, packet); + } + + @Override + public IPacket executeServer(NetHandlerPlayServer handler) { + Container container = handler.playerEntity.openContainer; + if (container instanceof ModularUIContainer) { + ModularUIContext context = ((ModularUIContainer) container).getContext(); + try { + context.readClientPacket(packet, widgetId); + } catch (IOException e) { + ModularUI.logger.error("Error reading client packet: "); + e.printStackTrace(); + } + } else { + ModularUI.logger.error("Expected ModularUIContainer on server, but got {}", container); + } + return null; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/network/IPacket.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/network/IPacket.java new file mode 100644 index 0000000..32fec24 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/network/IPacket.java @@ -0,0 +1,38 @@ +package com.gtnewhorizons.modularui.common.internal.network; + +import cpw.mods.fml.common.network.simpleimpl.IMessage; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.PacketBuffer; + +/** + * Joinked from Multiblocked + */ +public interface IPacket extends IMessage { + + void encode(PacketBuffer buf); + + void decode(PacketBuffer buf); + + @Override + default void fromBytes(ByteBuf buf) { + decode(new PacketBuffer(buf)); + } + + @Override + default void toBytes(ByteBuf buf) { + encode(new PacketBuffer(buf)); + } + + @SideOnly(Side.CLIENT) + default IPacket executeClient(NetHandlerPlayClient handler) { + return null; + } + + default IPacket executeServer(NetHandlerPlayServer handler) { + return null; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/network/NetworkHandler.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/network/NetworkHandler.java new file mode 100644 index 0000000..713426a --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/network/NetworkHandler.java @@ -0,0 +1,55 @@ +package com.gtnewhorizons.modularui.common.internal.network; + +import com.gtnewhorizons.modularui.ModularUI; +import cpw.mods.fml.common.network.NetworkRegistry; +import cpw.mods.fml.common.network.simpleimpl.IMessageHandler; +import cpw.mods.fml.common.network.simpleimpl.SimpleNetworkWrapper; +import cpw.mods.fml.relauncher.Side; +import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.world.World; + +/** + * Joinked from Multiblocked + */ +public class NetworkHandler { + + public static final SimpleNetworkWrapper network = NetworkRegistry.INSTANCE.newSimpleChannel(ModularUI.MODID); + private static int packetId = 0; + + public static void init() { + registerS2C(SWidgetUpdate.class); + registerC2S(CWidgetUpdate.class); + } + + private static void registerC2S(Class clazz) { + network.registerMessage(C2SHandler, clazz, packetId++, Side.SERVER); + } + + private static void registerS2C(Class clazz) { + network.registerMessage(S2CHandler, clazz, packetId++, Side.CLIENT); + } + + public static void sendToServer(IPacket packet) { + network.sendToServer(packet); + } + + public static void sendToWorld(IPacket packet, World world) { + network.sendToDimension(packet, world.provider.dimensionId); + } + + public static void sendToPlayer(IPacket packet, EntityPlayerMP player) { + network.sendTo(packet, player); + } + + final static IMessageHandler S2CHandler = (message, ctx) -> { + NetHandlerPlayClient handler = ctx.getClientHandler(); + return message.executeClient(handler); + }; + + final static IMessageHandler C2SHandler = (message, ctx) -> { + NetHandlerPlayServer handler = ctx.getServerHandler(); + return message.executeServer(handler); + }; +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/network/NetworkUtils.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/network/NetworkUtils.java new file mode 100644 index 0000000..d2c1cbd --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/network/NetworkUtils.java @@ -0,0 +1,82 @@ +package com.gtnewhorizons.modularui.common.internal.network; + +import com.gtnewhorizons.modularui.ModularUI; + +import cpw.mods.fml.common.FMLCommonHandler; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.minecraft.client.entity.EntityPlayerSP; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.network.PacketBuffer; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; + +public class NetworkUtils { + + public static final Consumer EMPTY_PACKET = buffer -> { + }; + + public static boolean isDedicatedClient() { + return FMLCommonHandler.instance().getSide().isClient(); + } + + public static boolean isClient(EntityPlayer player) { + if (player == null) throw new NullPointerException("Can't get side of null player!"); + return player.getEntityWorld() == null ? player instanceof EntityPlayerSP : player.getEntityWorld().isRemote; + } + + public static void writePacketBuffer(PacketBuffer writeTo, PacketBuffer writeFrom) { + writeTo.writeVarIntToBuffer(writeFrom.readableBytes()); + writeTo.writeBytes(writeFrom); + } + + public static PacketBuffer readPacketBuffer(PacketBuffer buf) { + ByteBuf directSliceBuffer = buf.readBytes(buf.readVarIntFromBuffer()); + ByteBuf copiedDataBuffer = Unpooled.copiedBuffer(directSliceBuffer); + directSliceBuffer.release(); + return new PacketBuffer(copiedDataBuffer); + } + + public static void writeFluidStack(PacketBuffer buffer, @Nullable FluidStack fluidStack) { + if (fluidStack == null) { + buffer.writeBoolean(true); + } else { + buffer.writeBoolean(false); + NBTTagCompound fluidStackTag = fluidStack.writeToNBT(new NBTTagCompound()); + try{ + buffer.writeNBTTagCompoundToBuffer(fluidStackTag); + } + catch(IOException e){ + + } + } + } + + @Nullable + public static FluidStack readFluidStack(PacketBuffer buffer) throws IOException { + if (buffer.readBoolean()) { + return null; + } + return FluidStack.loadFluidStackFromNBT(buffer.readNBTTagCompoundFromBuffer()); + } + + public static void writeStringSafe(PacketBuffer buffer, String string) { + byte[] bytesTest = string.getBytes(StandardCharsets.UTF_8); + byte[] bytes; + + if (bytesTest.length > 32767) { + bytes = new byte[32767]; + System.arraycopy(bytesTest, 0, bytes, 0, 32767); + ModularUI.logger.warn("Warning! Synced string exceeds max length!"); + } else { + bytes = bytesTest; + } + buffer.writeVarIntToBuffer(bytes.length); + buffer.writeBytes(bytes); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/network/SWidgetUpdate.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/network/SWidgetUpdate.java new file mode 100644 index 0000000..1bc8e1c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/network/SWidgetUpdate.java @@ -0,0 +1,55 @@ +package com.gtnewhorizons.modularui.common.internal.network; + +import com.gtnewhorizons.modularui.ModularUI; +import com.gtnewhorizons.modularui.api.screen.ModularUIContext; +import com.gtnewhorizons.modularui.common.internal.wrapper.ModularGui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.network.PacketBuffer; + +import java.io.IOException; + +public class SWidgetUpdate implements IPacket { + + public int widgetId; + public PacketBuffer packet; + + public SWidgetUpdate(PacketBuffer packet, int widgetId) { + this.packet = packet; + this.widgetId = widgetId; + } + + public SWidgetUpdate() { + } + + @Override + public void decode(PacketBuffer buf) { + this.widgetId = buf.readVarIntFromBuffer(); + this.packet = NetworkUtils.readPacketBuffer(buf); + } + + @Override + public void encode(PacketBuffer buf) { + buf.writeVarIntToBuffer(widgetId); + NetworkUtils.writePacketBuffer(buf, packet); + } + + @Override + public IPacket executeClient(NetHandlerPlayClient handler) { + GuiScreen screen = Minecraft.getMinecraft().currentScreen; + if (screen instanceof ModularGui) { + ModularUIContext context = ((ModularGui) screen).getContext(); + try { + context.readServerPacket(packet, widgetId); + } catch (IOException e) { + ModularUI.logger.error("Error reading server packet: "); + e.printStackTrace(); + } + } else { + ModularUI.logger.error("Expected ModularGui screen on client, but got {}", screen); + } + return null; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/BaseSlot.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/BaseSlot.java new file mode 100644 index 0000000..34e167b --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/BaseSlot.java @@ -0,0 +1,182 @@ +package com.gtnewhorizons.modularui.common.internal.wrapper; + +import com.gtnewhorizons.modularui.api.forge.IItemHandlerModifiable; +import com.gtnewhorizons.modularui.api.forge.ItemStackHandler; +import com.gtnewhorizons.modularui.api.forge.SlotItemHandler; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Predicate; + +public class BaseSlot extends SlotItemHandler { + + protected final boolean phantom; + protected boolean canInsert = true, canTake = true; + + protected boolean enabled = true; + // lower priority means it gets targeted first + // hotbar 20, player inventory 40, machine input 0 + private int shiftClickPriority = 0; + private boolean ignoreStackSizeLimit = false; + private Runnable changeListener; + private Predicate filter; + private ItemStack cachedItem = null; + private boolean needsSyncing; + + public static BaseSlot phantom() { + return phantom(new ItemStackHandler(), 0); + } + + public static BaseSlot phantom(IItemHandlerModifiable handler, int index) { + return new BaseSlot(handler, index, true); + } + + public BaseSlot(IItemHandlerModifiable inventory, int index) { + this(inventory, index, false); + } + + public BaseSlot(IItemHandlerModifiable inventory, int index, boolean phantom) { + super(inventory, index, 0, 0); + this.phantom = phantom; +// if (inventory instanceof PlayerMainInvWrapper) { +// setShiftClickPriority(index > 8 ? 40 : 20); +// } + if (this.phantom) { + this.shiftClickPriority += 10; + } + } + + public BaseSlot setShiftClickPriority(int shiftClickPriority) { + this.shiftClickPriority = shiftClickPriority; + return this; + } + + public BaseSlot disableShiftInsert() { + return setShiftClickPriority(Integer.MIN_VALUE); + } + + public BaseSlot setAccess(boolean canInsert, boolean canTake) { + this.canTake = canTake; + this.canInsert = canInsert; + return this; + } + + public BaseSlot setIgnoreStackSizeLimit(boolean ignoreStackSizeLimit) { + this.ignoreStackSizeLimit = ignoreStackSizeLimit; + return this; + } + + @Override + public boolean isItemValid(@NotNull ItemStack stack) { + return !this.phantom && isItemValidPhantom(stack); + } + + public boolean isItemValidPhantom(ItemStack stack) { + return this.canInsert && (filter == null || filter.test(stack)) && getItemHandler().isItemValid(getSlotIndex(), stack); + } + + @Override + public boolean canTakeStack(EntityPlayer playerIn) { + return !this.phantom && canTake && super.canTakeStack(playerIn); + } + + @Override + public boolean func_111238_b() { + return enabled; + } + + public boolean isCanInsert() { + return canInsert; + } + + public boolean isPhantom() { + return phantom; + } + + public boolean isIgnoreStackSizeLimit() { + return ignoreStackSizeLimit; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getShiftClickPriority() { + return shiftClickPriority; + } + + public void setChangeListener(Runnable changeListener) { + this.changeListener = changeListener; + } + + public void setFilter(Predicate filter) { + this.filter = filter; + } + + @Override + public void onSlotChanged() { + if (this.cachedItem != null && ItemStack.areItemStacksEqual(this.cachedItem, getStack())) { + return; + } + if (getStack() != null) { + this.cachedItem = getStack().copy(); + } + this.needsSyncing = true; + if (this.changeListener != null) { + this.changeListener.run(); + } + } + + public boolean isNeedsSyncing() { + return needsSyncing; + } + + public void resetNeedsSyncing() { + this.needsSyncing = false; + } + + // handle background by widgets + @Override + public ResourceLocation getBackgroundIconTexture() { + return null; + } + +// @Nullable +// @Override +// public String getSlotTexture() { +// return null; +// } +// +// @Nullable +// @Override +// public TextureAtlasSprite getBackgroundSprite() { +// return null; +// } + + public void incrementStackCount(int amount) { + ItemStack stack = getStack(); + if (stack == null) { + return; + } + int oldAmount = stack.stackSize; + if (amount < 0) { + amount = Math.max(0, oldAmount + amount); + } else { + if (Integer.MAX_VALUE - amount < oldAmount) { + amount = Integer.MAX_VALUE; + } else { + int maxSize = getItemHandler().getSlotLimit(getSlotIndex()); + if (!isIgnoreStackSizeLimit() && stack.getMaxStackSize() < maxSize) { + maxSize = stack.getMaxStackSize(); + } + amount = Math.min(oldAmount + amount, maxSize); + } + } + if (oldAmount != amount) { + stack.stackSize = amount; + onSlotChanged(); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/FluidTankHandler.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/FluidTankHandler.java new file mode 100644 index 0000000..401bf44 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/FluidTankHandler.java @@ -0,0 +1,64 @@ +package com.gtnewhorizons.modularui.common.internal.wrapper; + +import net.minecraftforge.common.util.ForgeDirection; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.FluidTankInfo; +import net.minecraftforge.fluids.IFluidHandler; +import net.minecraftforge.fluids.IFluidTank; + +import javax.annotation.Nullable; + +public class FluidTankHandler implements IFluidHandler { + + public static IFluidHandler getTankFluidHandler(IFluidTank tank) { + if (tank instanceof IFluidHandler) { + return (IFluidHandler) tank; + } + return new FluidTankHandler(tank); + } + + private final IFluidTank fluidTank; + + public FluidTankHandler(IFluidTank tank) { + this.fluidTank = tank; + } + + @Override + public FluidTankInfo[] getTankInfo(ForgeDirection from) { + return new FluidTankInfo[]{ + fluidTank.getInfo() + }; + } + + @Override + public int fill(ForgeDirection from, FluidStack resource, boolean doFill) { + return fluidTank.fill(resource, doFill); + } + + @Nullable + @Override + public FluidStack drain(ForgeDirection from, FluidStack resource, boolean doDrain) { + FluidStack currentFluid = fluidTank.getFluid(); + if (currentFluid == null || currentFluid.amount <= 0 || !currentFluid.isFluidEqual(resource)) { + return null; + } + return fluidTank.drain(resource.amount, doDrain); + } + + @Nullable + @Override + public FluidStack drain(ForgeDirection from, int maxDrain, boolean doDrain) { + return fluidTank.drain(maxDrain, doDrain); + } + + @Override + public boolean canFill(ForgeDirection from, Fluid fluid) { + return true; + } + + @Override + public boolean canDrain(ForgeDirection from, Fluid fluid) { + return true; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/GhostIngredientWrapper.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/GhostIngredientWrapper.java new file mode 100644 index 0000000..ac7e01e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/GhostIngredientWrapper.java @@ -0,0 +1,30 @@ +package com.gtnewhorizons.modularui.common.internal.wrapper; + +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.nei.IGhostIngredientHandler; +import com.gtnewhorizons.modularui.api.widget.IGhostIngredientTarget; +import com.gtnewhorizons.modularui.api.widget.Widget; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; + +public class GhostIngredientWrapper implements IGhostIngredientHandler.Target { + + private final W widget; + + public GhostIngredientWrapper(W widget) { + this.widget = widget; + } + + @Override + public @NotNull Rectangle getArea() { + Pos2d pos = widget.getAbsolutePos(); + return new Rectangle(pos.x, pos.y, widget.getSize().width, widget.getSize().height); + } + + @Override + public void accept(@NotNull ItemStack ingredient) { + widget.accept(ingredient); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/ModularGui.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/ModularGui.java new file mode 100644 index 0000000..1bca11c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/ModularGui.java @@ -0,0 +1,553 @@ +package com.gtnewhorizons.modularui.common.internal.wrapper; + +import com.gtnewhorizons.modularui.config.Config; +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.drawable.Text; +import com.gtnewhorizons.modularui.api.math.Color; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.screen.Cursor; +import com.gtnewhorizons.modularui.api.screen.ModularUIContext; +import com.gtnewhorizons.modularui.api.screen.ModularWindow; +import com.gtnewhorizons.modularui.api.widget.IVanillaSlot; +import com.gtnewhorizons.modularui.api.widget.IWidgetParent; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.mixins.GuiContainerMixin; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiLabel; +import net.minecraft.client.gui.inventory.GuiContainer; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.entity.RenderItem; +import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.input.Keyboard; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static codechicken.lib.render.FontUtils.fontRenderer; + +@SideOnly(Side.CLIENT) +public class ModularGui extends GuiContainer { + + private final ModularUIContext context; + private Pos2d mousePos = Pos2d.ZERO; + + @Nullable + private Interactable lastClicked; + private long lastClick = -1; + private long lastFocusedClick = -1; + private int drawCalls = 0; + private long drawTime = 0; + private int fps = 0; + + private float partialTicks; + + public ModularGui(ModularUIContainer container) { + super(container); + this.context = container.getContext(); + this.context.initializeClient(this); + } + + public ModularUIContext getContext() { + return context; + } + + public Cursor getCursor() { + return context.getCursor(); + } + + public Pos2d getMousePos() { + return mousePos; + } + +// @Override +// public void onResize(@NotNull Minecraft mc, int w, int h) { +// super.onResize(mc, w, h); +// context.resize(new Size(w, h)); +// } + + public void setMainWindowArea(Pos2d pos, Size size) { + this.guiLeft = pos.x; + this.guiTop = pos.y; + this.xSize = size.width; + this.ySize = size.height; + } + + @Override + public void initGui() { + super.initGui(); + context.resize(new Size(width, height)); + this.context.buildWindowOnStart(); + this.context.getCurrentWindow().onOpen(); + } + + public GuiContainerMixin getAccessor() { + return (GuiContainerMixin) this; + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + mousePos = new Pos2d(mouseX, mouseY); + + int i = this.guiLeft; + int j = this.guiTop; + this.drawGuiContainerBackgroundLayer(partialTicks, mouseX, mouseY); + GlStateManager.disableLighting(); + GlStateManager.disableDepth(); + // mainly for invtweaks compat + drawVanillaElements(mouseX, mouseY, partialTicks); + GlStateManager.pushMatrix(); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableRescaleNormal(); + getAccessor().setHoveredSlot(null); + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240.0F, 240.0F); + GlStateManager.enableRescaleNormal(); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + RenderHelper.disableStandardItemLighting(); + this.drawGuiContainerForegroundLayer(mouseX, mouseY); + RenderHelper.enableGUIStandardItemLighting(); + + getAccessor().setHoveredSlot(null); + Widget hovered = getCursor().getHovered(); + if (hovered instanceof IVanillaSlot) { + getAccessor().setHoveredSlot(((IVanillaSlot) hovered).getMcSlot()); + } + + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.pushMatrix(); + GlStateManager.translate(i, j, 0); +// MinecraftForge.EVENT_BUS.post(new GuiContainerEvent.DrawForeground(this, mouseX, mouseY)); + GlStateManager.popMatrix(); + + InventoryPlayer inventoryplayer = this.mc.thePlayer.inventory; + ItemStack itemstack = getAccessor().getDraggedStack() == null ? inventoryplayer.getItemStack() : getAccessor().getDraggedStack(); + GlStateManager.translate((float) i, (float) j, 0.0F); + if (itemstack != null) { + int k2 = getAccessor().getDraggedStack() == null ? 8 : 16; + String s = null; + + if (getAccessor().getDraggedStack() != null && getAccessor().getIsRightMouseClick()) { + itemstack = itemstack.copy(); + itemstack.stackSize = (int)Math.ceil((float) itemstack.stackSize / 2.0F); + } else if (this.isDragSplitting2() && this.getDragSlots().size() > 1) { + itemstack = itemstack.copy(); + itemstack.stackSize = getAccessor().getDragSplittingRemnant(); + + if (itemstack == null) { + s = "" + "§e" + "0"; + } + } + + this.drawItemStack(itemstack, mouseX - i - 8, mouseY - j - k2, s); + } + + if (getAccessor().getReturningStack() != null) { + float f = (float) (Minecraft.getSystemTime() - getAccessor().getReturningStackTime()) / 100.0F; + + if (f >= 1.0F) { + f = 1.0F; + getAccessor().setReturningStack(null); + } + + int l2 = getAccessor().getReturningStackDestSlot().xDisplayPosition - getAccessor().getTouchUpX(); + int i3 = getAccessor().getReturningStackDestSlot().yDisplayPosition - getAccessor().getTouchUpY(); + int l1 = getAccessor().getTouchUpX() + (int) ((float) l2 * f); + int i2 = getAccessor().getTouchUpY() + (int) ((float) i3 * f); + this.drawItemStack(getAccessor().getReturningStack(), l1, i2, null); + } + + GlStateManager.popMatrix(); + + if (Config.debug) { + GlStateManager.disableDepth(); + GlStateManager.disableLighting(); + GlStateManager.enableBlend(); + drawDebugScreen(); + GlStateManager.color(1f, 1f, 1f, 1f); + } + GlStateManager.enableLighting(); + GlStateManager.enableDepth(); + GlStateManager.enableRescaleNormal(); + RenderHelper.enableStandardItemLighting(); + } + + private void drawItemStack(ItemStack stack, int x, int y, String altText) { + GlStateManager.translate(0.0F, 0.0F, 32.0F); + this.zLevel = 200.0F; + itemRender.zLevel = 200.0F; + FontRenderer font = stack.getItem().getFontRenderer(stack); + if (font == null) font = fontRenderer; + itemRender.renderItemAndEffectIntoGUI(font, mc.getTextureManager(), stack, x, y); + itemRender.renderItemOverlayIntoGUI(font, mc.getTextureManager(), stack, x, y - (getDragSlots() != null ? 0 : 8), altText); + this.zLevel = 0.0F; + itemRender.zLevel = 0.0F; + } + + @Override + protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { + if (Config.debug) { + long time = Minecraft.getSystemTime() / 1000; + if (drawTime != time) { + fps = drawCalls; + drawCalls = 0; + drawTime = time; + } + drawCalls++; + } + context.getMainWindow().frameUpdate(partialTicks); + if (context.getMainWindow() != context.getCurrentWindow()) { + context.getCurrentWindow().frameUpdate(partialTicks); + } + drawDefaultBackground(); + + GlStateManager.disableRescaleNormal(); + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableLighting(); + GlStateManager.disableDepth(); + + for (ModularWindow window : context.getOpenWindowsReversed()) { + if (window.isEnabled()) { + window.drawWidgets(partialTicks, false); + } + } + + GlStateManager.enableRescaleNormal(); + GlStateManager.enableLighting(); + RenderHelper.enableStandardItemLighting(); + + this.partialTicks = partialTicks; + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + GlStateManager.pushMatrix(); + GlStateManager.disableRescaleNormal(); + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableLighting(); + GlStateManager.disableDepth(); + + Widget hovered = context.getCursor().getHovered(); + if (hovered != null && !context.getCursor().isHoldingSomething()) { + if (hovered instanceof IVanillaSlot && ((IVanillaSlot) hovered).getMcSlot().getHasStack()) { + renderToolTip(((IVanillaSlot) hovered).getMcSlot().getStack(), mouseX, mouseY, ((IVanillaSlot) hovered).getExtraTooltip()); + } else if (hovered.getTooltipShowUpDelay() <= context.getCursor().getTimeHovered()) { + List tooltip = hovered.getTooltip(); + if (!tooltip.isEmpty()) { + renderToolTip(null, context.getMousePos().x, context.getMousePos().y, tooltip.stream().map(line -> line.getFormatted()).collect(Collectors.toList())); + } + } + } + + if (context.getCurrentWindow().isEnabled()) { + context.getCurrentWindow().drawWidgets(partialTicks, true); + } + context.getCursor().draw(partialTicks); + + GlStateManager.enableRescaleNormal(); + GlStateManager.enableLighting(); + RenderHelper.enableStandardItemLighting(); + GlStateManager.popMatrix(); + } + + public void drawDebugScreen() { + Size screenSize = context.getScaledScreenSize(); + int color = Color.rgb(180, 40, 115); + int lineY = screenSize.height - 13; + drawString(fontRenderer, "Mouse Pos: " + getMousePos(), 5, lineY, color); + lineY -= 11; + drawString(fontRenderer, "FPS: " + fps, 5, screenSize.height - 24, color); + lineY -= 11; + Widget hovered = context.getCursor().findHoveredWidget(true); + if (hovered != null) { + Size size = hovered.getSize(); + Pos2d pos = hovered.getAbsolutePos(); + IWidgetParent parent = hovered.getParent(); + + drawBorder(pos.x, pos.y, size.width, size.height, color, 1f); + drawBorder(parent.getAbsolutePos().x, parent.getAbsolutePos().y, parent.getSize().width, parent.getSize().height, Color.withAlpha(color, 0.3f), 1f); + drawText("Pos: " + hovered.getPos(), 5, lineY, 1, color, false); + lineY -= 11; + drawText("Size: " + size, 5, lineY, 1, color, false); + lineY -= 11; + drawText("Parent: " + (parent instanceof ModularWindow ? "ModularWindow" : parent.toString()), 5, lineY, 1, color, false); + lineY -= 11; + drawText("Class: " + hovered, 5, lineY, 1, color, false); + } + color = Color.withAlpha(color, 25); + for (int i = 5; i < screenSize.width; i += 5) { + drawVerticalLine(i, 0, screenSize.height, color); + } + + for (int i = 5; i < screenSize.height; i += 5) { + drawHorizontalLine(0, screenSize.width, i, color); + } + drawRect(mousePos.x, mousePos.y, mousePos.x + 1, mousePos.y + 1, Color.withAlpha(Color.GREEN.normal, 0.8f)); + } + + protected void renderToolTip(ItemStack stack, int x, int y, List extraLines) { + FontRenderer font = null; + List lines = new ArrayList(); + if(stack != null){ + font = stack.getItem().getFontRenderer(stack); + lines.addAll(stack.getTooltip(context.getPlayer(), this.mc.gameSettings.advancedItemTooltips)); + } + lines.addAll(extraLines); + this.drawHoveringText(lines, x, y, (font == null ? fontRenderer : font)); + } + + protected void drawVanillaElements(int mouseX, int mouseY, float partialTicks) { + for (Object guiButton : this.buttonList) { + ((GuiButton)guiButton).drawButton(this.mc, mouseX, mouseY); + } + for (Object guiLabel : this.labelList) { + ((GuiLabel)guiLabel).func_146159_a(this.mc, mouseX, mouseY); + } + } + + @Override + public void updateScreen() { + super.updateScreen(); + context.onClientTick(); + for (ModularWindow window : context.getOpenWindowsReversed()) { + window.update(); + } + context.getCursor().updateHovered(); + context.getCursor().onScreenUpdate(); + } + + private boolean isDoubleClick(long lastClick, long currentClick) { + return currentClick - lastClick < 500; + } + +// @Override +// protected boolean hasClickedOutside(int p_193983_1_, int p_193983_2_, int p_193983_3_, int p_193983_4_) { +// for (ModularWindow window : context.getOpenWindows()) { +// if (Pos2d.isInside(p_193983_1_, p_193983_2_, window.getAbsolutePos(), window.getSize())) { +// return false; +// } +// } +// return super.hasClickedOutside(p_193983_1_, p_193983_2_, p_193983_3_, p_193983_4_); +// } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton){ + long time = Minecraft.getSystemTime(); + boolean doubleClick = isDoubleClick(lastClick, time); + lastClick = time; + for (Interactable interactable : context.getCurrentWindow().getInteractionListeners()) { + interactable.onClick(mouseButton, doubleClick); + } + + if (context.getCursor().onMouseClick(mouseButton)) { + lastFocusedClick = time; + return; + } + + Interactable probablyClicked = null; + boolean wasSuccess = false; + doubleClick = isDoubleClick(lastFocusedClick, time); + loop: + for (Object hovered : getCursor().getAllHovered()) { + if (context.getCursor().onHoveredClick(mouseButton, hovered)) { + break; + } + if (hovered instanceof Interactable) { + Interactable interactable = (Interactable) hovered; + Interactable.ClickResult result = interactable.onClick(mouseButton, doubleClick && lastClicked == interactable); + switch (result) { + case IGNORE: + continue; + case ACKNOWLEDGED: + if (probablyClicked == null) { + probablyClicked = interactable; + } + continue; + case REJECT: + probablyClicked = null; + break loop; + case ACCEPT: + probablyClicked = interactable; + break loop; + case SUCCESS: + probablyClicked = interactable; + wasSuccess = true; + getCursor().updateFocused((Widget) interactable); + break loop; + } + } + } + this.lastClicked = probablyClicked; + if (!wasSuccess) { + getCursor().updateFocused(null); + } + if (probablyClicked == null) { + super.mouseClicked(mouseX, mouseY, mouseButton); + } + + lastFocusedClick = time; + } + + @Override + protected void mouseMovedOrUp(int mouseX, int mouseY, int mouseButton) { + for (Interactable interactable : context.getCurrentWindow().getInteractionListeners()) { + interactable.onClickReleased(mouseButton); + } + if (!context.getCursor().onMouseReleased(mouseButton) && (lastClicked == null || !lastClicked.onClickReleased(mouseButton))) { + super.mouseMovedOrUp(mouseX, mouseY, mouseButton); + } + } + + @Override + protected void mouseClickMove(int mouseX, int mouseY, int mouseButton, long timeSinceLastClick) { + super.mouseClickMove(mouseX, mouseY, mouseButton, timeSinceLastClick); + for (Interactable interactable : context.getCurrentWindow().getInteractionListeners()) { + interactable.onMouseDragged(mouseButton, timeSinceLastClick); + } + if (lastClicked != null) { + lastClicked.onMouseDragged(mouseButton, timeSinceLastClick); + } + } + + @Override + protected void keyTyped(char typedChar, int keyCode){ + // debug mode C + CTRL + SHIFT + if (keyCode == 46 && isCtrlKeyDown() && isShiftKeyDown()) { + Config.debug = !Config.debug; + } + for (Interactable interactable : context.getCurrentWindow().getInteractionListeners()) { + interactable.onKeyPressed(typedChar, keyCode); + } + + Widget focused = getCursor().getFocused(); + if (focused instanceof Interactable && ((Interactable) focused).onKeyPressed(typedChar, keyCode)) { + return; + } + for (Object hovered : getCursor().getAllHovered()) { + if (focused != hovered && hovered instanceof Interactable && ((Interactable) hovered).onKeyPressed(typedChar, keyCode)) { + return; + } + } + + if (keyCode == Keyboard.KEY_ESCAPE || this.mc.gameSettings.keyBindInventory.getKeyCode() == keyCode) { + this.context.tryClose(); + } else { + super.keyTyped(typedChar, keyCode); + } + } + + public void mouseScroll(int direction) { + for (Interactable interactable : context.getCurrentWindow().getInteractionListeners()) { + interactable.onMouseScroll(direction); + } + Widget focused = getCursor().getFocused(); + if (focused instanceof Interactable && ((Interactable) focused).onMouseScroll(direction)) { + return; + } + for (Object hovered : getCursor().getAllHovered()) { + if (focused != hovered && hovered instanceof Interactable && ((Interactable) hovered).onMouseScroll(direction)) { + return; + } + } + } + + @Override + public void onGuiClosed() { + context.getCloseListeners().forEach(Runnable::run); + } + + public boolean isDragSplitting2() { + return getAccessor().isDragSplitting(); + } + + public Set getDragSlots() { + return getAccessor().getDragSplittingSlots(); + } + + public RenderItem getItemRenderer() { + return itemRender; + } + + public float getZ() { + return zLevel; + } + + public void setZ(float z) { + this.zLevel = z; + } + + public FontRenderer getFontRenderer() { + return fontRenderer; + } + + @SideOnly(Side.CLIENT) + public static void drawBorder(float x, float y, float width, float height, int color, float border) { + drawSolidRect(x - border, y - border, width + 2 * border, border, color); + drawSolidRect(x - border, y + height, width + 2 * border, border, color); + drawSolidRect(x - border, y, border, height, color); + drawSolidRect(x + width, y, border, height, color); + } + + @SideOnly(Side.CLIENT) + public static void drawSolidRect(float x, float y, float width, float height, int color) { + drawRect(x, y, x + width, y + height, color); + GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); + GlStateManager.enableBlend(); + } + + @SideOnly(Side.CLIENT) + public static void drawText(String text, float x, float y, float scale, int color, boolean shadow) { + FontRenderer fontRenderer = Minecraft.getMinecraft().fontRenderer; + GlStateManager.disableBlend(); + GlStateManager.pushMatrix(); + GlStateManager.scale(scale, scale, 0f); + float sf = 1 / scale; + fontRenderer.drawString(text, (int)(x * sf), (int)(y * sf), color, shadow); + GlStateManager.popMatrix(); + GlStateManager.enableBlend(); + } + + public static void drawRect(float left, float top, float right, float bottom, int color) { + if (left < right) { + float i = left; + left = right; + right = i; + } + + if (top < bottom) { + float j = top; + top = bottom; + bottom = j; + } + + float a = (float) (color >> 24 & 255) / 255.0F; + float r = (float) (color >> 16 & 255) / 255.0F; + float g = (float) (color >> 8 & 255) / 255.0F; + float b = (float) (color & 255) / 255.0F; + Tessellator tessellator = Tessellator.instance; + GlStateManager.enableBlend(); + GlStateManager.disableTexture2D(); + GlStateManager.tryBlendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO); + GlStateManager.color(r, g, b, a); + tessellator.startDrawingQuads(); +// bufferbuilder.begin(7, DefaultVertexFormats.POSITION); + tessellator.addVertex(left, bottom, 0.0D); + tessellator.addVertex(right, bottom, 0.0D); + tessellator.addVertex(right, top, 0.0D); + tessellator.addVertex(left, top, 0.0D); + tessellator.draw(); + GlStateManager.enableTexture2D(); + GlStateManager.disableBlend(); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/ModularUIContainer.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/ModularUIContainer.java new file mode 100644 index 0000000..f1f5c2a --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/ModularUIContainer.java @@ -0,0 +1,179 @@ +package com.gtnewhorizons.modularui.common.internal.wrapper; + +import com.gtnewhorizons.modularui.api.forge.ItemHandlerHelper; +import com.gtnewhorizons.modularui.api.screen.ModularUIContext; +import com.gtnewhorizons.modularui.api.screen.ModularWindow; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.Container; +import net.minecraft.inventory.ICrafting; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; + +import java.util.*; + +public class ModularUIContainer extends Container { + + private final ModularUIContext context; + private boolean initialisedContainer = false; + private final List sortedShiftClickSlots = new ArrayList<>(); + + private final Map> sortingAreas = new HashMap<>(); + private final Map sortRowSizes = new HashMap<>(); + + public ModularUIContainer(ModularUIContext context, ModularWindow mainWindow) { + this.context = context; + this.context.initialize(this, mainWindow); + checkSlotIds(); + sortSlots(); + initialisedContainer = true; + } + + public void sortSlots() { + this.sortedShiftClickSlots.sort(Comparator.comparingInt(BaseSlot::getShiftClickPriority)); + } + + public ModularUIContext getContext() { + return context; + } + + @Override + public boolean canInteractWith(EntityPlayer playerIn) { + return playerIn.isEntityAlive(); + } + + private void checkSlotIds() { + for (int i = 0; i < inventorySlots.size(); i++) { + ((Slot)(inventorySlots.get(i))).slotNumber = i; + } + } + + @Override + public Slot addSlotToContainer(Slot slotIn) { + if (slotIn instanceof BaseSlot && ((BaseSlot) slotIn).getShiftClickPriority() > Integer.MIN_VALUE) { + sortedShiftClickSlots.add((BaseSlot) slotIn); + } + Slot ret = super.addSlotToContainer(slotIn); + + if (initialisedContainer) { + sortSlots(); + } + return ret; + } + + public void removeSlot(Slot slot) { + if (slot != inventorySlots.get(slot.slotNumber)) { + throw new IllegalStateException("Could not find slot in container!"); + } + inventorySlots.remove(slot.slotNumber); + + if (slot instanceof BaseSlot && sortedShiftClickSlots.remove(slot) && initialisedContainer) { + sortSlots(); + } + checkSlotIds(); + for (List slots : sortingAreas.values()) { + slots.removeIf(slot1 -> slot1 == slot); + } + } + + public void setRowSize(String sortArea, int size) { + sortRowSizes.put(sortArea, size); + } + + public void setSlotSortable(String area, BaseSlot slot) { + if (slot != inventorySlots.get(slot.slotNumber)) { + throw new IllegalArgumentException("Slot is not at the expected index!"); + } + this.sortingAreas.computeIfAbsent(area, section1 -> new ArrayList<>()).add(slot); + } + + @Override + public void detectAndSendChanges() { + super.detectAndSendChanges(); + for (ModularWindow window : this.context.getOpenWindows()) { + if (window.isInitialized()) { + // do not allow syncing before the client is initialized + window.serverUpdate(); + } + } + } + + public void sendSlotChange(ItemStack stack, int index) { + for (Object listener : this.crafters) { + ((ICrafting)(listener)).sendSlotContents(this, index, stack); + } + } + +// public void sendHeldItemUpdate() { +// for (Object listener : this.crafters) { +// if (listener instanceof EntityPlayerMP) { +// EntityPlayerMP player = (EntityPlayerMP) listener; +// player.connection.sendPacket(new SPacketSetSlot(-1, -1, player.inventory.getItemStack())); +// } +// } +// } + + @Override + public ItemStack transferStackInSlot(EntityPlayer playerIn, int index) { + Slot slot = (Slot)(this.inventorySlots.get(index)); + if (slot instanceof BaseSlot && !((BaseSlot) slot).isPhantom()) { + ItemStack stack = slot.getStack(); + if (stack != null) { + ItemStack remainder = transferItem((BaseSlot) slot, stack.copy()); + stack.stackSize = remainder.stackSize; + if (stack.stackSize < 1) { + slot.putStack(null); + } + return null; + } + } + return null; + } + + protected ItemStack transferItem(BaseSlot fromSlot, ItemStack stack) { + for (BaseSlot slot : this.sortedShiftClickSlots) { + if (fromSlot.getShiftClickPriority() != slot.getShiftClickPriority() && slot.canTake && slot.isItemValidPhantom(stack)) { + ItemStack itemstack = slot.getStack(); + if (slot.isPhantom()) { + if (itemstack == null || (ItemHandlerHelper.canItemStacksStackRelaxed(stack, itemstack) && itemstack.stackSize < slot.getItemStackLimit(itemstack))) { + slot.putStack(stack.copy()); + return stack; + } + } else { + int maxSize = Math.min(slot.getSlotStackLimit(), stack.getMaxStackSize()); + if (itemstack == null) { + int toMove = Math.min(maxSize, stack.stackSize); + ItemStack newStack = stack.copy(); + stack.stackSize -= toMove; + newStack.stackSize = toMove; + slot.putStack(newStack); + } else if (ItemHandlerHelper.canItemStacksStackRelaxed(stack, itemstack)) { + int toMove = Math.max(Math.min(maxSize - itemstack.stackSize, stack.stackSize), 0); + stack.stackSize -= toMove; + itemstack.stackSize += toMove; + slot.onSlotChanged(); + } + + if (stack.stackSize < 1) { + return stack; + } + } + } + } + for (Slot slot1 : this.sortedShiftClickSlots) { + if (!(slot1 instanceof BaseSlot)) { + continue; + } + BaseSlot slot = (BaseSlot) slot1; + ItemStack itemstack = slot.getStack(); + if (fromSlot.getItemHandler() != slot.getItemHandler() && slot.canInsert && itemstack == null && slot.isItemValid(stack)) { + if (stack.stackSize > slot1.getSlotStackLimit()) { + slot.putStack(stack.splitStack(slot.getSlotStackLimit())); + } else { + slot.putStack(stack.splitStack(stack.stackSize)); + } + break; + } + } + return stack; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/MultiList.java b/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/MultiList.java new file mode 100644 index 0000000..9d39c8e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/internal/wrapper/MultiList.java @@ -0,0 +1,87 @@ +package com.gtnewhorizons.modularui.common.internal.wrapper; + +import java.util.*; + +public class MultiList extends AbstractList { + + private final List> lists = new ArrayList<>(); + private final List defaultList = new ArrayList<>(); + + public MultiList() { + this.lists.add(defaultList); + } + + public void addList(List list) { + this.lists.add(list); + } + + public void addElements(T... ts) { + addList(Arrays.asList(ts)); + } + + public void clearLists() { + lists.clear(); + defaultList.clear(); + lists.add(defaultList); + } + + @Override + public void clear() { + for (List list : lists) { + list.clear(); + } + } + + @Override + public boolean add(T element) { + defaultList.add(element); + return true; + } + + @Override + public T get(int index) { + if (index < 0) throw new IndexOutOfBoundsException("Index out of bounds: " + index); + for (List list : lists) { + if (index >= list.size()) { + index -= list.size(); + continue; + } + return list.get(index); + } + throw new IndexOutOfBoundsException("Index out of bounds: " + index); + } + + @Override + public int size() { + return lists.stream().mapToInt(List::size).sum(); + } + + @Override + public Iterator iterator() { + return new MultiListIterator(); + } + + private class MultiListIterator implements Iterator { + + private int listCursor = 0, cursor = 0; + private List currentLists; + + @Override + public boolean hasNext() { + return listCursor < lists.size() || cursor < currentLists.size(); + } + + @Override + public T next() { + try { + while (currentLists == null || cursor == currentLists.size()) { + currentLists = lists.get(listCursor++); + cursor = 0; + } + return currentLists.get(cursor++); + } catch (IndexOutOfBoundsException e) { + throw new NoSuchElementException(); + } + } + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/keybind/KeyBindHandler.java b/src/main/java/com/gtnewhorizons/modularui/common/keybind/KeyBindHandler.java new file mode 100644 index 0000000..d244ed6 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/keybind/KeyBindHandler.java @@ -0,0 +1,59 @@ +package com.gtnewhorizons.modularui.common.keybind; + +public class KeyBindHandler { + +// @Mod.EventHandler +// public void preInit(FMLPreInitializationEvent event) { +// MinecraftForge.EVENT_BUS.register(this); +// } +// +// private static void checkKeyState(int key, boolean state) { +// if (key != 0) { +// // imitates KeyBinding.setKeyBindState() +// for (KeyBinding keyBinding : getKeyBindingMap().lookupAll(key)) { +// if (KeyBindAPI.doForceCheckKeyBind(keyBinding)) { +// ((KeyBindAccess) keyBinding).setPressed(state); +// } +// } +// // imitates KeyBinding.onTick() +// if (state) { +// KeyBinding keyBinding = getKeyBindingMap().lookupActive(key); +// if (keyBinding != null) { +// if (KeyBindAPI.doForceCheckKeyBind(keyBinding)) { +// incrementPressTime(keyBinding); +// } +// +// Collection compatibles = KeyBindAPI.getCompatibles(keyBinding); +// if (compatibles.isEmpty()) return; +// for (KeyBinding keyBinding1 : compatibles) { +// if (keyBinding1.isActiveAndMatches(key) && KeyBindAPI.doForceCheckKeyBind(keyBinding1)) { +// incrementPressTime(keyBinding1); +// } +// } +// } +// } +// } +// } +// +// @SubscribeEvent(priority = EventPriority.HIGHEST) +// public static void onGuiKeyInput(GuiScreenEvent.ActionPerformedEvent.Pre event) { +// if (!event.gui.mc.inGameHasFocus) { +// int key = Keyboard.getEventKey(); +// boolean state = Keyboard.getEventKeyState(); +// checkKeyState(key, state); +// } +// } +// +// @SubscribeEvent(priority = EventPriority.HIGHEST) +// public static void onMouseInput(GuiScreenEvent.ActionPerformedEvent.Pre event) { +// if (!event.gui.mc.inGameHasFocus) { +// int key = Mouse.getEventButton() - 100; +// boolean state = Mouse.getEventButtonState(); +// checkKeyState(key, state); +// } +// } +// +// public static IntHashMap getKeyBindingMap() { +// return ((KeyBindAccess) Minecraft.getMinecraft().gameSettings.keyBindPickBlock).getHash(); +// } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/ButtonWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/ButtonWidget.java new file mode 100644 index 0000000..eb12d9f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/ButtonWidget.java @@ -0,0 +1,85 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.ModularUITextures; +import com.gtnewhorizons.modularui.api.drawable.Text; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.common.internal.Theme; +import net.minecraft.network.PacketBuffer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.BiConsumer; + +/** + * Clickable button widget. + */ +public class ButtonWidget extends SyncedWidget implements Interactable { + + public static ButtonWidget openSyncedWindowButton(int id) { + return (ButtonWidget) new ButtonWidget() + .setOnClick((clickData, widget) -> { + if (!widget.isClient()) + widget.getContext().openSyncedWindow(id); + }) + .setBackground(ModularUITextures.VANILLA_BACKGROUND, new Text("Window")); + } + + public static ButtonWidget closeWindowButton(boolean syncedWindow) { + return (ButtonWidget) new ButtonWidget() + .setOnClick((clickData, widget) -> { + if (!syncedWindow || !widget.isClient()) { + widget.getWindow().closeWindow(); + } + }) + .setBackground(ModularUITextures.VANILLA_BACKGROUND, new Text("x")) + .setSize(12, 12); + } + + private BiConsumer clickAction; + + /** + * Set callback that will be invoked when button is clicked. + */ + public ButtonWidget setOnClick(BiConsumer clickAction) { + this.clickAction = clickAction; + return this; + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + return new Size(20, 20); + } + + @Override + public @Nullable String getBackgroundColorKey() { + return Theme.KEY_BUTTON; + } + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + if (clickAction != null) { + Widget.ClickData clickData = Widget.ClickData.create(buttonId, doubleClick); + clickAction.accept(clickData, this); + if (syncsToServer()) { + syncToServer(1, clickData::writeToPacket); + } + Interactable.playButtonClickSound(); + return ClickResult.ACCEPT; + } + return ClickResult.ACKNOWLEDGED; + } + + @Override + public void readOnClient(int id, PacketBuffer buf) { + } + + @Override + public void readOnServer(int id, PacketBuffer buf) { + if (id == 1) { + Widget.ClickData data = Widget.ClickData.readPacket(buf); + clickAction.accept(data, this); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/ChangeableWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/ChangeableWidget.java new file mode 100644 index 0000000..037b4f0 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/ChangeableWidget.java @@ -0,0 +1,137 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.ISyncedWidget; +import com.gtnewhorizons.modularui.api.widget.IWidgetParent; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.common.internal.network.NetworkUtils; +import net.minecraft.network.PacketBuffer; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +@ApiStatus.Experimental +public class ChangeableWidget extends Widget implements ISyncedWidget, IWidgetParent { + + private final List child = new ArrayList<>(); + @Nullable + private Widget queuedChild = null; + private final Supplier widgetSupplier; + private boolean initialised = false; + private boolean firstTick = true; + + /** + * Creates a widget which child can be changed dynamically. + * Call {@link #notifyChangeServer()} to notify the widget for a change. + * + * @param widgetSupplier widget to supply. Can return null + */ + public ChangeableWidget(Supplier widgetSupplier) { + this.widgetSupplier = widgetSupplier; + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + if (this.child.isEmpty()) { + return Size.ZERO; + } + return this.child.get(0).getSize(); + } + + /** + * Notifies the widget that the child probably changed. + * Only executed on server and synced to client. This method is preferred! + */ + public void notifyChangeServer() { + if (!isClient()) { + notifyChange(true); + } + } + + private void notifyChange(boolean sync) { + if (this.widgetSupplier == null || !isInitialised()) { + return; + } + if (sync && !isClient()) { + syncToClient(0, NetworkUtils.EMPTY_PACKET); + } + removeCurrentChild(); + this.queuedChild = this.widgetSupplier.get(); + this.initialised = false; + } + + private void initQueuedChild() { + if (this.queuedChild != null) { + IWidgetParent.forEachByLayer(this.queuedChild, widget -> { + if (widget instanceof IWidgetParent) { + ((IWidgetParent) widget).initChildren(); + } + }); + AtomicInteger syncId = new AtomicInteger(1); + IWidgetParent.forEachByLayer(this.queuedChild, widget1 -> { + if (widget1 instanceof ISyncedWidget) { + getWindow().addDynamicSyncedWidget(syncId.getAndIncrement(), (ISyncedWidget) widget1, this); + } + return false; + }); + this.queuedChild.initialize(getWindow(), this, getLayer() + 1); + this.child.add(this.queuedChild); + this.initialised = true; + this.queuedChild = null; + this.firstTick = true; + } + checkNeedsRebuild(); + } + + public void removeCurrentChild() { + if (!this.child.isEmpty()) { + Widget widget = this.child.get(0); + widget.setEnabled(false); + IWidgetParent.forEachByLayer(widget, Widget::onPause); + IWidgetParent.forEachByLayer(widget, Widget::onDestroy); + this.child.clear(); + } + } + + @Override + public void detectAndSendChanges(boolean init) { + if (init) { + notifyChangeServer(); + } + if (this.initialised && !this.child.isEmpty()) { + IWidgetParent.forEachByLayer(this.child.get(0), widget -> { + if (widget instanceof ISyncedWidget) { + ((ISyncedWidget) widget).detectAndSendChanges(firstTick); + } + }); + firstTick = false; + } + } + + @Override + public void readOnClient(int id, PacketBuffer packetBuffer) throws IOException { + if (id == 0) { + notifyChange(false); + initQueuedChild(); + syncToServer(1, NetworkUtils.EMPTY_PACKET); + } + } + + @Override + public void readOnServer(int id, PacketBuffer packetBuffer) throws IOException { + if (id == 1) { + initQueuedChild(); + } + } + + @Override + public List getChildren() { + return this.child; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/Column.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/Column.java new file mode 100644 index 0000000..cce1f66 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/Column.java @@ -0,0 +1,88 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.widget.IWidgetBuilder; +import com.gtnewhorizons.modularui.api.math.CrossAxisAlignment; +import com.gtnewhorizons.modularui.api.math.MainAxisAlignment; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.Widget; +import org.jetbrains.annotations.NotNull; + +public class Column extends MultiChildWidget implements IWidgetBuilder { + + private MainAxisAlignment maa = MainAxisAlignment.START; + private CrossAxisAlignment caa = CrossAxisAlignment.START; + private int maxHeight = -1, maxWidth = 0; + + @Override + public void addWidgetInternal(Widget widget) { + addChild(widget); + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + if (maa == MainAxisAlignment.START) { + return getSizeOf(children); + } + return new Size(this.maxWidth, this.maxHeight); + } + + @Override + public void layoutChildren(int maxWidthC, int maxHeightC) { + if (maxHeight < 0 && maa != MainAxisAlignment.START) { + if (isAutoSized()) { + maxHeight = maxHeightC - getPos().x; + } else { + maxHeight = getSize().height; + } + } + + this.maxWidth = 0; + int totalHeight = 0; + + for (Widget widget : getChildren()) { + totalHeight += widget.getSize().height; + maxWidth = Math.max(maxWidth, widget.getSize().width); + } + + int lastY = 0; + if (maa == MainAxisAlignment.CENTER) { + lastY = (int) (maxHeight / 2f - totalHeight / 2f); + } else if (maa == MainAxisAlignment.END) { + lastY = maxHeight - totalHeight; + } + + for (Widget widget : getChildren()) { + int x = 0; + if (caa == CrossAxisAlignment.CENTER) { + x = (int) (maxWidth / 2f - widget.getSize().width / 2f); + } else if (caa == CrossAxisAlignment.END) { + x = maxWidth - widget.getSize().width; + } + widget.setPosSilent(new Pos2d(x, lastY)); + lastY += widget.getSize().height; + if (maa == MainAxisAlignment.SPACE_BETWEEN) { + lastY += (maxHeight - totalHeight) / (getChildren().size() - 1); + } + } + } + + public Column setAlignment(MainAxisAlignment maa) { + return setAlignment(maa, caa); + } + + public Column setAlignment(CrossAxisAlignment caa) { + return setAlignment(maa, caa); + } + + public Column setAlignment(MainAxisAlignment maa, CrossAxisAlignment caa) { + this.maa = maa; + this.caa = caa; + return this; + } + + public Column setMaxHeight(int maxHeight) { + this.maxHeight = maxHeight; + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/CycleButtonWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/CycleButtonWidget.java new file mode 100644 index 0000000..7e32985 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/CycleButtonWidget.java @@ -0,0 +1,252 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.drawable.Text; +import com.gtnewhorizons.modularui.api.drawable.UITexture; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import com.gtnewhorizons.modularui.common.internal.JsonHelper; +import com.gtnewhorizons.modularui.common.internal.Theme; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.minecraft.network.PacketBuffer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.*; + +public class CycleButtonWidget extends SyncedWidget implements Interactable { + + private int state = 0; + private int length = 1; + private IntConsumer setter; + private IntSupplier getter; + private Function textureGetter; + private IDrawable texture = IDrawable.EMPTY; + private final List> stateTooltip = new ArrayList<>(); + + public CycleButtonWidget() { + } + + @Override + public void readJson(JsonObject json, String type) { + super.readJson(json, type); + this.length = JsonHelper.getInt(json, 1, "length", "size"); + this.state = JsonHelper.getInt(json, 0, "defaultState"); + if (json.has("texture")) { + JsonElement element = json.get("texture"); + if (element.isJsonArray()) { + JsonArray array = element.getAsJsonArray(); + this.length = array.size(); + IDrawable[] textures = new IDrawable[this.length]; + for (int i = 0; i < array.size(); i++) { + JsonElement element1 = array.get(i); + if (element1.isJsonObject()) { + textures[i] = IDrawable.ofJson(element1.getAsJsonObject()); + } else { + textures[i] = IDrawable.EMPTY; + ModularUI.logger.error("Texture needs to be a json object"); + } + } + this.textureGetter = val -> textures[val]; + } else if (element.isJsonObject()) { + IDrawable drawable = IDrawable.ofJson(element.getAsJsonObject()); + if (drawable instanceof UITexture) { + setTexture((UITexture) drawable); + } else { + this.textureGetter = val -> drawable; + } + } + } + } + + @Override + public void onInit() { + if (setter == null || getter == null) { + ModularUI.logger.warn("{} was not properly initialised!", this); + return; + } + if (textureGetter == null) { + ModularUI.logger.warn("Texture Getter of {} was not set!", this); + textureGetter = val -> IDrawable.EMPTY; + } + setState(getter.getAsInt(), false, false); + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + return new Size(20, 20); + } + + @Override + public @Nullable String getBackgroundColorKey() { + return Theme.KEY_BUTTON; + } + + public void next() { + if (++state == length) { + state = 0; + } + setState(state, true, true); + } + + public void prev() { + if (--state == -1) { + state = length - 1; + } + setState(state, true, true); + } + + public void setState(int state, boolean sync, boolean setSource) { + if (state >= length) { + throw new IndexOutOfBoundsException("CycleButton state out of bounds"); + } + this.state = state; + if (sync) { + if (isClient()) { + if (syncsToServer()) { + syncToServer(1, buffer -> buffer.writeVarIntToBuffer(state)); + } + } else { + syncToClient(1, buffer -> buffer.writeVarIntToBuffer(state)); + } + } + if (setSource) { + setter.accept(this.state); + } + if (isClient()) { + this.texture = textureGetter.apply(this.state); + } + } + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + switch (buttonId) { + case 0: + next(); + Interactable.playButtonClickSound(); + return ClickResult.ACCEPT; + case 1: + prev(); + Interactable.playButtonClickSound(); + return ClickResult.ACCEPT; + } + return ClickResult.ACKNOWLEDGED; + } + + @Override + public void detectAndSendChanges(boolean init) { + if (syncsToClient()) { + int actualValue = getter.getAsInt(); + if (init || actualValue != state) { + setState(actualValue, true, false); + } + } + } + + @Override + public void draw(float partialTicks) { + texture.applyThemeColor(); + texture.draw(Pos2d.ZERO, getSize(), partialTicks); + } + + @Override + public void readOnClient(int id, PacketBuffer buf) { + if (id == 1) { + setState(buf.readVarIntFromBuffer(), false, true); + } + } + + @Override + public void readOnServer(int id, PacketBuffer buf) { + if (id == 1) { + setState(buf.readVarIntFromBuffer(), false, true); + } + } + + @Override + public boolean hasTooltip() { + return super.hasTooltip() || (this.stateTooltip.size() > this.state && !this.stateTooltip.get(this.state).isEmpty()); + } + + @Override + public List getTooltip() { + List texts = super.getTooltip(); + if (texts.isEmpty()) { + return this.stateTooltip.get(this.state); + } + texts.addAll(this.stateTooltip.get(this.state)); + return texts; + } + + public CycleButtonWidget setSetter(IntConsumer setter) { + this.setter = setter; + return this; + } + + public CycleButtonWidget setGetter(IntSupplier getter) { + this.getter = getter; + return this; + } + + public > CycleButtonWidget setForEnum(Class clazz, Supplier getter, Consumer setter) { + setSetter(val -> setter.accept(clazz.getEnumConstants()[val])); + setGetter(() -> getter.get().ordinal()); + setLength(clazz.getEnumConstants().length); + return this; + } + + public CycleButtonWidget setToggle(BooleanSupplier getter, Consumer setter) { + setSetter(val -> setter.accept(val == 1)); + setGetter(() -> getter.getAsBoolean() ? 1 : 0); + setLength(2); + return this; + } + + public CycleButtonWidget setTextureGetter(Function textureGetter) { + this.textureGetter = textureGetter; + return this; + } + + public CycleButtonWidget setTexture(UITexture texture) { + return setTextureGetter(val -> { + float a = 1f / length; + return texture.getSubArea(0, val * a, 1, val * a + a); + }); + } + + /** + * Adds a line to the tooltip + */ + public CycleButtonWidget addTooltip(int state, Text tooltip) { + if (state >= this.stateTooltip.size() || state < 0) { + throw new IndexOutOfBoundsException(); + } + this.stateTooltip.get(state).add(tooltip); + return this; + } + + /** + * Adds a line to the tooltip + */ + public CycleButtonWidget addTooltip(int state, String tooltip) { + return addTooltip(state, new Text(tooltip)); + } + + public CycleButtonWidget setLength(int length) { + this.length = length; + while (this.stateTooltip.size() < this.length) { + this.stateTooltip.add(new ArrayList<>()); + } + while (this.stateTooltip.size() > this.length) { + this.stateTooltip.remove(this.stateTooltip.size() - 1); + } + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/DrawableWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/DrawableWidget.java new file mode 100644 index 0000000..3fcd94a --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/DrawableWidget.java @@ -0,0 +1,55 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.google.gson.JsonObject; +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.drawable.Text; +import com.gtnewhorizons.modularui.api.drawable.UITexture; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.widget.Widget; +import org.jetbrains.annotations.Nullable; + +/** + * Uses {@link IDrawable} to draw widget. + */ +public class DrawableWidget extends Widget { + + @Nullable + private IDrawable drawable = IDrawable.EMPTY; + + @Override + public void readJson(JsonObject json, String type) { + super.readJson(json, type); + if (type.equals("image")) { + setDrawable(UITexture.ofJson(json)); + } + } + + @Override + public void onScreenUpdate() { + if (drawable != null) { + drawable.tick(); + } + } + + @Override + public void draw(float partialTicks) { + if (drawable != null) { + drawable.draw(Pos2d.ZERO, getSize(), partialTicks); + } + } + + @Nullable + public IDrawable getDrawable() { + return drawable; + } + + public DrawableWidget setDrawable(IDrawable drawable) { + if (drawable instanceof Text) { + ModularUI.logger.warn("Please use TextWidget for Text"); + } + this.drawable = drawable; + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/DynamicTextWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/DynamicTextWidget.java new file mode 100644 index 0000000..1817bf6 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/DynamicTextWidget.java @@ -0,0 +1,28 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.drawable.Text; + +import java.util.function.Supplier; + +public class DynamicTextWidget extends TextWidget { + + private final Supplier textSupplier; + + public DynamicTextWidget(Supplier text) { + this.textSupplier = text; + } + + @Override + public void onScreenUpdate() { + String l = textSupplier.get().getFormatted(); + if (!l.equals(localised)) { + checkNeedsRebuild(); + localised = l; + } + } + + @Override + public Text getText() { + return textSupplier.get(); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/ExpandTab.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/ExpandTab.java new file mode 100644 index 0000000..45f4231 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/ExpandTab.java @@ -0,0 +1,240 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.animation.Eases; +import com.gtnewhorizons.modularui.api.animation.Interpolator; +import com.gtnewhorizons.modularui.api.drawable.GuiHelper; +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.IWidgetBuilder; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.common.internal.Theme; +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.gameevent.TickEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Tab-styled widget that can contain multiple widget children + * and toggle expanded/collapsed by clicking this. + */ +public class ExpandTab extends MultiChildWidget implements Interactable, IWidgetBuilder { + + private boolean expanded = false, animating = false, firstBuild = true; + private Interpolator openAnimator; + private Interpolator closeAnimator; + private Size expandedSize; + private Size normalSize; + private Pos2d expandedPos; + private Pos2d normalPos; + private int animateDuration = 250; + private float animateX, animateY, animateWidth, animateHeight; + @Nullable + private IDrawable[] normalTexture; + private float ticktime; + + @Override + public void onInit() { + this.openAnimator = new Interpolator(0, 1, this.animateDuration, Eases.EaseQuadOut, value -> { + float val = (float) value; + this.animateX = (this.expandedPos.x - this.normalPos.x) * val + this.normalPos.x; + this.animateY = (this.expandedPos.y - this.normalPos.y) * val + this.normalPos.y; + this.animateWidth = (this.expandedSize.width - this.normalSize.width) * val + this.normalSize.width; + this.animateHeight = (this.expandedSize.height - this.normalSize.height) * val + this.normalSize.height; + }, val -> { + this.animateX = this.expandedPos.x; + this.animateY = this.expandedPos.y; + this.animateWidth = this.expandedSize.width; + this.animateHeight = this.expandedSize.height; + this.animating = false; + }); + this.closeAnimator = this.openAnimator.getReversed(this.animateDuration, Eases.EaseQuadIn); + this.closeAnimator.setCallback(val -> { + this.animateX = this.normalPos.x; + this.animateY = this.normalPos.y; + this.animateWidth = this.normalSize.width; + this.animateHeight = this.normalSize.height; + this.animating = false; + for (Widget widget : getChildren()) { + widget.setEnabled(false); + } + }); + for (Widget widget : getChildren()) { + widget.setEnabled(false); + } + + FMLCommonHandler.instance().bus().register(this); + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + return new Size(20, 20); + } + + @Override + public void onRebuild() { + if (firstBuild) { + if (this.normalPos == null) { + this.normalPos = getPos(); + } + if (this.normalSize == null) { + this.normalSize = getSize(); + } + if (this.expandedPos == null) { + this.expandedPos = this.normalPos; + } + if (this.expandedSize == null) { + this.expandedSize = new Size(this.normalSize.width * 3, this.normalSize.height * 3); + } + this.animateX = getPos().x; + this.animateY = getPos().y; + this.animateWidth = getSize().width; + this.animateHeight = getSize().height; + this.firstBuild = false; + } + } + + + @SubscribeEvent + public void onRenderTick(TickEvent.RenderTickEvent event) { + ticktime = event.renderTickTime; + } + + @Override + public void onFrameUpdate() { + if (this.animating) { + if (expanded) { +// this.openAnimator.update(Minecraft.getMinecraft().getTickLength()); + this.openAnimator.update(ticktime); + } else { + this.closeAnimator.update(ticktime); + } + } + } + + @Override + @SideOnly(Side.CLIENT) + public void drawBackground(float partialTicks) { + IDrawable[] background = getBackground(); + if (background != null) { + int themeColor = Theme.INSTANCE.getColor(getBackgroundColorKey()); + for (IDrawable drawable : background) { + if (drawable != null) { + drawable.applyThemeColor(themeColor); + drawable.draw(animateX - getPos().x, animateY - getPos().y, animateWidth, animateHeight, partialTicks); + } + } + } + } + + @Override + public void draw(float partialTicks) { + if (!isExpanded() && this.normalTexture != null) { + for (IDrawable drawable : this.normalTexture) { + if (drawable != null) { + drawable.applyThemeColor(); + drawable.draw(Pos2d.ZERO, this.normalSize, partialTicks); + } + } + } + } + + @Override + public void drawChildren(float partialTicks) { + if (isExpanded() || animating) { + Pos2d parentPos = getParent().getAbsolutePos(); + if (animating) { + GuiHelper.useScissor((int) (parentPos.x + this.animateX), (int) (parentPos.y + this.animateY), (int) this.animateWidth, (int) this.animateHeight, () -> { + super.drawChildren(partialTicks); + }); + } else { + super.drawChildren(partialTicks); + } + } + } + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + if (buttonId == 0) { + setExpanded(!isExpanded()); + return ClickResult.ACCEPT; + } + return ClickResult.ACKNOWLEDGED; + } + + public void setExpanded(boolean expanded) { + if (this.expanded != expanded) { + this.expanded = expanded; + this.animating = true; + + if (isExpanded()) { + for (Widget widget : getChildren()) { + widget.setEnabled(true); + } + openAnimator.forward(); + this.size = expandedSize; + this.pos = expandedPos; + } else { + closeAnimator.forward(); + this.size = normalSize; + this.pos = normalPos; + } + checkNeedsRebuild(); + } + } + + public boolean isExpanded() { + return expanded; + } + + @Override + public void addWidgetInternal(Widget widget) { + addChild(widget); + } + + public ExpandTab setExpandedPos(int x, int y) { + return setExpandedPos(new Pos2d(x, y)); + } + + public ExpandTab setExpandedPos(Pos2d expandedPos) { + this.expandedPos = expandedPos; + return this; + } + + public ExpandTab setExpandedSize(int width, int height) { + return setExpandedSize(new Size(width, height)); + } + + public ExpandTab setExpandedSize(Size expandedSize) { + this.expandedSize = expandedSize; + return this; + } + + @Override + public ExpandTab setSize(Size size) { + super.setSize(size); + this.normalSize = size; + return this; + } + + @Override + public ExpandTab setPos(Pos2d relativePos) { + super.setPos(relativePos); + this.normalPos = relativePos; + return this; + } + + public ExpandTab setAnimateDuration(int animateDuration) { + this.animateDuration = animateDuration; + return this; + } + + public ExpandTab setNormalTexture(IDrawable... normalTexture) { + this.normalTexture = normalTexture; + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/FluidSlotWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/FluidSlotWidget.java new file mode 100644 index 0000000..7327fd4 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/FluidSlotWidget.java @@ -0,0 +1,497 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.ModularUITextures; +import com.gtnewhorizons.modularui.api.NumberFormat; +import com.gtnewhorizons.modularui.api.drawable.GuiHelper; +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.drawable.Text; +import com.gtnewhorizons.modularui.api.drawable.TextRenderer; +import com.gtnewhorizons.modularui.api.math.Alignment; +import com.gtnewhorizons.modularui.api.math.Color; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.nei.IGhostIngredientHandler; +import com.gtnewhorizons.modularui.api.widget.IGhostIngredientTarget; +import com.gtnewhorizons.modularui.api.widget.IIngredientProvider; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import com.gtnewhorizons.modularui.common.internal.Theme; +import com.gtnewhorizons.modularui.common.internal.network.NetworkUtils; +import com.gtnewhorizons.modularui.common.internal.wrapper.FluidTankHandler; +import com.gtnewhorizons.modularui.common.internal.wrapper.ModularGui; +import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketBuffer; +import net.minecraftforge.fluids.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.List; + +public class FluidSlotWidget extends SyncedWidget implements Interactable, IIngredientProvider, IGhostIngredientTarget { + + public static final Size SIZE = new Size(18, 18); + + @Nullable + private IDrawable overlayTexture; + private final TextRenderer textRenderer = new TextRenderer(); + private final IFluidTank fluidTank; + private final IFluidHandler tankHandler; + @Nullable + private FluidStack cachedFluid; + private FluidStack lastStoredPhantomFluid; + private Pos2d contentOffset = new Pos2d(1, 1); + private boolean alwaysShowFull = true; + private boolean canDrainSlot = true; + private boolean canFillSlot = true; + private boolean phantom = false; + private boolean controlsAmount = true; + private boolean lastShift = false; + + public FluidSlotWidget(IFluidTank fluidTank) { + this.fluidTank = fluidTank; + this.tankHandler = FluidTankHandler.getTankFluidHandler(fluidTank); + this.textRenderer.setColor(Color.WHITE.normal); + this.textRenderer.setShadow(true); + } + + public static FluidSlotWidget phantom(IFluidTank fluidTank, boolean controlsAmount) { + FluidSlotWidget slot = new FluidSlotWidget(fluidTank); + slot.phantom = true; + slot.controlsAmount = controlsAmount; + return slot; + } + + public static FluidSlotWidget phantom(int capacity) { + return phantom(new FluidTank(capacity > 0 ? capacity : 1), capacity > 1); + } + + public FluidStack getContent() { + return this.fluidTank.getFluid(); + } + + @Override + public void onInit() { + if (isClient()) { + this.textRenderer.setShadow(true); + this.textRenderer.setScale(0.5f); + } + if (getBackground() == null) { + setBackground(ModularUITextures.FLUID_SLOT); + } + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + return SIZE; + } + + public void setControlsAmount(boolean controlsAmount, boolean sync) { + if (this.controlsAmount != controlsAmount) { + this.controlsAmount = controlsAmount; + if (sync) { + if (isClient()) { + syncToServer(3, buffer -> buffer.writeBoolean(controlsAmount)); + } else { + syncToClient(3, buffer -> buffer.writeBoolean(controlsAmount)); + } + } + } + } + + @Override + public void buildTooltip(List tooltip) { + FluidStack fluid = cachedFluid; + if (phantom) { + if (fluid != null) { + tooltip.add(new Text(fluid.getLocalizedName())); + if (controlsAmount) { + tooltip.add(Text.localised("modularui.fluid.phantom.amount", fluid.amount)); + } + } else { + tooltip.add(Text.localised("modularui.fluid.empty")); + } + if (controlsAmount) { + tooltip.add(Text.localised("modularui.fluid.phantom.control")); + } + } else { + if (fluid != null) { + tooltip.add(new Text(fluid.getLocalizedName())); + tooltip.add(Text.localised("modularui.fluid.amount", fluid.amount, fluidTank.getCapacity())); + addAdditionalFluidInfo(tooltip, fluid); + } else { + tooltip.add(Text.localised("modularui.fluid.empty")); + } + if (canFillSlot || canDrainSlot) { + tooltip.add(Text.EMPTY); // Add an empty line to separate from the bottom material tooltips + if (Interactable.hasShiftDown()) { + if (canFillSlot && canDrainSlot) { + tooltip.add(Text.localised("modularui.fluid.click_combined")); + } else if (canDrainSlot) { + tooltip.add(Text.localised("modularui.fluid.click_to_fill")); + } else if (canFillSlot) { + tooltip.add(Text.localised("modularui.fluid.click_to_empty")); + } + } else { + tooltip.add(Text.localised("modularui.tooltip.shift")); + } + } + } + } + + /** + * Mods can override this to add custom tooltips for the fluid + * + * @param tooltipContainer add lines here + * @param fluid the nonnull fluid + */ + public void addAdditionalFluidInfo(List tooltipContainer, @NotNull FluidStack fluid) { + } + + @Override + public @Nullable String getBackgroundColorKey() { + return Theme.KEY_FLUID_SLOT; + } + + @Override + public void draw(float partialTicks) { + FluidStack content = cachedFluid; + if (content != null) { + float y = contentOffset.y; + float height = size.height - contentOffset.y * 2; + if (!alwaysShowFull) { + float newHeight = height * content.amount * 1f / fluidTank.getCapacity(); + y += height - newHeight; + height = newHeight; + } + GuiHelper.drawFluidTexture(content, contentOffset.x, y, size.width - contentOffset.x * 2, height, 0); + } + if (overlayTexture != null) { + overlayTexture.draw(Pos2d.ZERO, size, partialTicks); + } + if (content != null && this.controlsAmount) { + String s = NumberFormat.format(content.amount); + textRenderer.setAlignment(Alignment.CenterRight, size.width - contentOffset.x - 1f); + textRenderer.setPos((int) (contentOffset.x + 0.5f), (int) (size.height - 5.5f)); + textRenderer.draw(s); + } + if (isHovering()) { + if (isHovering()) { + GlStateManager.colorMask(true, true, true, false); + ModularGui.drawSolidRect(1, 1, 16, 16, Theme.INSTANCE.getSlotHighlight()); + GlStateManager.colorMask(true, true, true, true); + } + } + } + + @Override + public void onScreenUpdate() { + if (lastShift != Interactable.hasShiftDown()) { + lastShift = Interactable.hasShiftDown(); + notifyTooltipChange(); + } + } + + @Override + public Object getIngredient() { + return cachedFluid; + } + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + if (!this.canFillSlot && !this.canDrainSlot) { + return ClickResult.ACKNOWLEDGED; + } + ItemStack cursorStack = getContext().getCursor().getItemStack(); + // todo fluid capability check + if (this.phantom || (cursorStack != null)) { + syncToServer(1, buffer -> { + buffer.writeVarIntToBuffer(buttonId); + buffer.writeBoolean(Interactable.hasShiftDown()); + }); + Interactable.playButtonClickSound(); + return ClickResult.ACCEPT; + } + return ClickResult.ACKNOWLEDGED; + } + + @Override + public boolean onMouseScroll(int direction) { + if (this.phantom) { + if ((direction > 0 && !this.canFillSlot) || (direction < 0 && !this.canDrainSlot)) { + return false; + } + if (Interactable.hasShiftDown()) { + direction *= 10; + } + if (Interactable.hasControlDown()) { + direction *= 100; + } + final int finalDirection = direction; + syncToServer(2, buffer -> buffer.writeVarIntToBuffer(finalDirection)); + return true; + } + return false; + } + + @Override + public void detectAndSendChanges(boolean init) { + FluidStack currentFluid = this.fluidTank.getFluid(); + if (init || fluidChanged(currentFluid, this.cachedFluid)) { + this.cachedFluid = currentFluid == null ? null : currentFluid.copy(); + syncToClient(1, buffer -> NetworkUtils.writeFluidStack(buffer, currentFluid)); + } + } + + public static boolean fluidChanged(@Nullable FluidStack current, @Nullable FluidStack cached) { + return current == null ^ cached == null || (current != null && (current.amount != cached.amount || !current.isFluidEqual(cached))); + } + + @Override + public void readOnClient(int id, PacketBuffer buf) throws IOException { + if (id == 1) { + this.cachedFluid = NetworkUtils.readFluidStack(buf); + notifyTooltipChange(); + } else if (id == 3) { + this.controlsAmount = buf.readBoolean(); + } + } + + @Override + public void readOnServer(int id, PacketBuffer buf) throws IOException { + if (id == 1) { + if (this.phantom) { + tryClickPhantom(buf.readVarIntFromBuffer(), buf.readBoolean()); + } else { + tryClickContainer(buf.readVarIntFromBuffer(), buf.readBoolean()); + } + } else if (id == 2) { + if (this.phantom) { + tryScrollPhantom(buf.readVarIntFromBuffer()); + } + } else if (id == 3) { + this.controlsAmount = buf.readBoolean(); + } else if (id == 4) { + this.fluidTank.drain(Integer.MAX_VALUE, true); + this.fluidTank.fill(NetworkUtils.readFluidStack(buf), true); + } + } + + private void tryClickContainer(int mouseButton, boolean isShiftKeyDown) { +// EntityPlayer player = getContext().getPlayer(); +// ItemStack currentStack = getContext().getCursor().getItemStack(); +// if (!currentStack.hasCapability(CapabilityFluidHandler.FLUID_HANDLER_ITEM_CAPABILITY, null)) { +// return; +// } +// int maxAttempts = isShiftKeyDown ? currentStack.stackSize : 1; +// if (mouseButton == 0 && canFillSlot) { +// boolean performedTransfer = false; +// for (int i = 0; i < maxAttempts; i++) { +// FluidActionResult result = FluidUtil.tryEmptyContainer(currentStack, tankHandler, Integer.MAX_VALUE, null, false); +// ItemStack remainingStack = result.getResult(); +// if (!result.isSuccess() || (currentStack.stackSize > 1 && remainingStack != null && !player.inventory.addItemStackToInventory(remainingStack))) { +// player.dropItem(remainingStack.getItem(), remainingStack.stackSize); +// break; //do not continue if we can't add resulting container into inventory +// } +// +// remainingStack = FluidUtil.tryEmptyContainer(currentStack, tankHandler, Integer.MAX_VALUE, null, true).result; +// if (currentStack.stackSize == 1) { +// currentStack = remainingStack; +// } else { +// currentStack.stackSize -= 1; +// } +// performedTransfer = true; +// if (currentStack == null) { +// break; +// } +// } +// FluidStack fluid = fluidTank.getFluid(); +// if (performedTransfer && fluid != null) { +// playSound(fluid, false); +// getContext().getCursor().setItemStack(currentStack, true); +// } +// return; +// } +// FluidStack currentFluid = fluidTank.getFluid(); +// if (mouseButton == 1 && canDrainSlot && currentFluid != null && currentFluid.amount > 0) { +// boolean performedTransfer = false; +// for (int i = 0; i < maxAttempts; i++) { +// FluidActionResult result = FluidUtil.tryFillContainer(currentStack, tankHandler, Integer.MAX_VALUE, null, false); +// ItemStack remainingStack = result.getResult(); +// if (!result.isSuccess() || (currentStack.stackSize > 1 && remainingStack != null && !player.inventory.addItemStackToInventory(remainingStack))) { +// break; //do not continue if we can't add resulting container into inventory +// } +// +// remainingStack = FluidUtil.tryFillContainer(currentStack, tankHandler, Integer.MAX_VALUE, null, true).result; +// if (currentStack.getCount() == 1) { +// currentStack = remainingStack; +// } else { +// currentStack.shrink(1); +// } +// performedTransfer = true; +// if (currentStack.isEmpty()) { +// break; +// } +// } +// if (performedTransfer) { +// playSound(currentFluid, true); +// getContext().getCursor().setItemStack(currentStack, true); +// } +// } + } + + public void tryClickPhantom(int mouseButton, boolean isShiftKeyDown) { +// EntityPlayer player = getContext().getPlayer(); +// ItemStack currentStack = getContext().getCursor().getItemStack(); +// FluidStack currentFluid = this.fluidTank.getFluid(); +// IFluidHandlerItem fluidHandlerItem = currentStack.getCapability(CapabilityFluidHandler.FLUID_HANDLER_ITEM_CAPABILITY, null); +// +// if (mouseButton == 0) { +// if (currentStack == null || fluidHandlerItem == null) { +// if (canDrainSlot) { +// this.fluidTank.drain(isShiftKeyDown ? Integer.MAX_VALUE : 1000, true); +// } +// } else { +// FluidStack cellFluid = fluidHandlerItem.drain(Integer.MAX_VALUE, false); +// if ((this.controlsAmount || currentFluid == null) && cellFluid != null) { +// if (canFillSlot) { +// if (!this.controlsAmount) { +// cellFluid.amount = 1; +// } +// if (this.fluidTank.fill(cellFluid, true) > 0) { +// this.lastStoredPhantomFluid = cellFluid.copy(); +// } +// } +// } else { +// if (canDrainSlot) { +// fluidTank.drain(isShiftKeyDown ? Integer.MAX_VALUE : 1000, true); +// } +// } +// } +// } else if (mouseButton == 1) { +// if (canFillSlot) { +// if (currentFluid != null) { +// if (this.controlsAmount) { +// FluidStack toFill = currentFluid.copy(); +// toFill.amount = 1000; +// this.fluidTank.fill(toFill, true); +// } +// } else if (lastStoredPhantomFluid != null) { +// FluidStack toFill = this.lastStoredPhantomFluid.copy(); +// toFill.amount = this.controlsAmount ? 1000 : 1; +// this.fluidTank.fill(toFill, true); +// } +// } +// } else if (mouseButton == 2 && currentFluid != null && canDrainSlot) { +// this.fluidTank.drain(isShiftKeyDown ? Integer.MAX_VALUE : 1000, true); +// } + } + + public void tryScrollPhantom(int direction) { + FluidStack currentFluid = this.fluidTank.getFluid(); + if (currentFluid == null) { + if (direction > 0 && this.lastStoredPhantomFluid != null) { + FluidStack toFill = this.lastStoredPhantomFluid.copy(); + toFill.amount = this.controlsAmount ? direction : 1; + this.fluidTank.fill(toFill, true); + } + return; + } + if (direction > 0 && this.controlsAmount) { + FluidStack toFill = currentFluid.copy(); + toFill.amount = direction; + this.fluidTank.fill(toFill, true); + } else if (direction < 0) { + this.fluidTank.drain(-direction, true); + } + } + + private void playSound(FluidStack fluid, boolean fill) { +// EntityPlayer player = getContext().getPlayer(); +// SoundEvent soundevent = fill ? fluid.getFluid().getFillSound(fluid) : fluid.getFluid().getEmptySound(fluid); +// player.world.playSound(null, player.posX, player.posY + 0.5, player.posZ, soundevent, SoundCategory.BLOCKS, 1.0F, 1.0F); + } + + @Override + public IGhostIngredientHandler.@Nullable Target getTarget(@NotNull ItemStack ingredient) { +// if (!isPhantom()) { +// return null; +// } +// if (ingredient instanceof FluidStack) { +// return ((FluidStack) ingredient).amount > 0 ? new GhostIngredientWrapper<>(this) : null; +// } +// if (ingredient instanceof ItemStack) { +// if (((ItemStack) ingredient).isEmpty()) return null; +// IFluidHandlerItem fluidHandlerItem = ((ItemStack) ingredient).getCapability(CapabilityFluidHandler.FLUID_HANDLER_ITEM_CAPABILITY, null); +// if (fluidHandlerItem != null) { +// return new GhostIngredientWrapper<>(this); +// } +// } + return null; + } + + @Override + public void accept(@NotNull ItemStack ingredient) { +// FluidStack fluid = null; +// if (ingredient instanceof FluidStack) { +// fluid = (FluidStack) ingredient; +// } else if (ingredient instanceof ItemStack) { +// IFluidHandlerItem fluidHandlerItem = ((ItemStack) ingredient).getCapability(CapabilityFluidHandler.FLUID_HANDLER_ITEM_CAPABILITY, null); +// if (fluidHandlerItem == null) return; +// fluid = fluidHandlerItem.drain(Integer.MAX_VALUE, false); +// } +// if (fluid == null) return; +// final FluidStack finalFluid = fluid; +// syncToServer(4, buffer -> NetworkUtils.writeFluidStack(buffer, finalFluid)); + } + + public boolean canFillSlot() { + return canFillSlot; + } + + public boolean canDrainSlot() { + return canDrainSlot; + } + + public boolean alwaysShowFull() { + return alwaysShowFull; + } + + public Pos2d getContentOffset() { + return contentOffset; + } + + public boolean controlsAmount() { + return controlsAmount; + } + + public boolean isPhantom() { + return phantom; + } + + @Nullable + public IDrawable getOverlayTexture() { + return overlayTexture; + } + + public FluidSlotWidget setInteraction(boolean canDrainSlot, boolean canFillSlot) { + this.canDrainSlot = canDrainSlot; + this.canFillSlot = canFillSlot; + return this; + } + + public FluidSlotWidget setAlwaysShowFull(boolean alwaysShowFull) { + this.alwaysShowFull = alwaysShowFull; + return this; + } + + public FluidSlotWidget setContentOffset(Pos2d contentOffset) { + this.contentOffset = contentOffset; + return this; + } + + public FluidSlotWidget setOverlayTexture(@Nullable IDrawable overlayTexture) { + this.overlayTexture = overlayTexture; + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/ListWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/ListWidget.java new file mode 100644 index 0000000..70954cf --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/ListWidget.java @@ -0,0 +1,155 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.common.internal.wrapper.MultiList; +import com.gtnewhorizons.modularui.api.drawable.GuiHelper; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.api.widget.scroll.IVerticalScrollable; +import com.gtnewhorizons.modularui.api.widget.scroll.ScrollType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class ListWidget extends MultiChildWidget implements Interactable, IVerticalScrollable { + + private int scrollOffset = 0; + private int totalHeight = 0; + @Nullable + private ScrollBar scrollBar; + private int maxHeight = -1; + private final MultiList allChildren = new MultiList<>(); + + public static ListWidget builder(List list, BiFunction widgetCreator) { + ListWidget listWidget = new ListWidget(); + int i = 0; + for (T t : list) { + Widget widget = Objects.requireNonNull(widgetCreator.apply(t, i++), "ListWidget creator produced a null child! This is forbidden!"); + listWidget.addChild(widget); + } + return listWidget; + } + + public static ListWidget builder(int size, Function widgetCreator) { + ListWidget listWidget = new ListWidget(); + for (int i = 0; i < size; i++) { + Widget widget = Objects.requireNonNull(widgetCreator.apply(i), "ListWidget creator produced a null child! This is forbidden!"); + listWidget.addChild(widget); + } + return listWidget; + } + + public ListWidget() { + setScrollBar(ScrollBar.defaultTextScrollBar()); + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + Size superSize = super.determineSize(maxWidth, maxHeight); + return this.maxHeight > 0 ? new Size(superSize, this.maxHeight) : new Size(superSize, maxHeight); + } + + @Override + public void initChildren() { + makeChildrenList(); + } + + protected void makeChildrenList() { + this.allChildren.clearLists(); + this.allChildren.addList(this.children); + if (this.scrollBar != null) { + this.allChildren.addElements(this.scrollBar); + } + } + + @Override + public void onRebuild() { + this.totalHeight = 0; + for (Widget child : this.children) { + this.totalHeight += child.getSize().height; + child.setEnabled(intersects(child)); + } + } + + @Override + public void layoutChildren(int maxWidth, int maxHeight) { + int y = -this.scrollOffset; + for (Widget widget : this.children) { + widget.setPosSilent(new Pos2d(0, y)); + y += widget.getSize().height; + } + } + + @Override + public void drawChildren(float partialTicks) { + GuiHelper.useScissor(pos.x, pos.y, size.width, size.height, () -> super.drawChildren(partialTicks)); + } + + @Override + public boolean childrenMustBeInBounds() { + return true; + } + + @Override + public boolean onMouseScroll(int direction) { + if (scrollBar != null) { + return scrollBar.onMouseScroll(direction); + } + return false; + } + + @Override + public List getChildren() { + return allChildren; + } + + @Override + public void setVerticalScrollOffset(int offset) { + if (this.scrollOffset != offset) { + int dif = this.scrollOffset - offset; + this.scrollOffset = offset; + for (Widget widget : children) { + widget.setPosSilent(widget.getPos().add(0, dif)); + widget.setEnabled(intersects(widget)); + } + } + } + + @Override + public int getVerticalScrollOffset() { + return scrollOffset; + } + + @Override + public int getVisibleHeight() { + return size.height; + } + + @Override + public int getActualHeight() { + return totalHeight; + } + + @Override + public ListWidget addChild(Widget widget) { + return (ListWidget) super.addChild(widget); + } + + public ListWidget setMaxHeight(int maxHeight) { + this.maxHeight = maxHeight; + return this; + } + + public ListWidget setScrollBar(@Nullable ScrollBar scrollBar) { + this.scrollBar = scrollBar; + if (this.scrollBar != null) { + this.scrollBar.setScrollType(ScrollType.VERTICAL, null, this); + } + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/MultiChildWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/MultiChildWidget.java new file mode 100644 index 0000000..60c293d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/MultiChildWidget.java @@ -0,0 +1,80 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.IWidgetParent; +import com.gtnewhorizons.modularui.api.widget.Widget; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MultiChildWidget extends Widget implements IWidgetParent { + + protected final List children = new ArrayList<>(); + + public MultiChildWidget addChild(Widget widget) { + if (checkChild(this, widget)) { + children.add(widget); + checkNeedsRebuild(); + } + return this; + } + + public void removeChild(Widget widget) { + if (checkEditable(this)) { + children.remove(widget); + checkNeedsRebuild(); + } + } + + public void removeChild(int index) { + if (checkEditable(this)) { + children.remove(index); + checkNeedsRebuild(); + } + } + + @Override + public List getChildren() { + return Collections.unmodifiableList(children); + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + if (!getChildren().isEmpty()) { + return getSizeOf(getChildren()); + } + return new Size(maxWidth, maxHeight); + } + + public static Size getSizeOf(List widgets) { + int x1 = Integer.MIN_VALUE, y1 = Integer.MIN_VALUE; + for (Widget widget : widgets) { + x1 = Math.max(x1, widget.getPos().x + widget.getSize().width); + y1 = Math.max(y1, widget.getPos().y + widget.getSize().height); + } + return new Size(x1, y1); + } + + public static boolean checkChild(Widget parent, Widget widget) { + if (widget == null) { + ModularUI.logger.error("Tried adding null widget to " + parent.getClass().getSimpleName()); + return false; + } + if (widget == parent) { + ModularUI.logger.error("Can't add self!"); + return false; + } + return checkEditable(parent); + } + + public static boolean checkEditable(Widget parent) { + if (parent.isInitialised() && !parent.getContext().isClientOnly()) { + throw new IllegalStateException("Can only dynamically add/remove widgets when the ui is client only!"); + } + return true; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/PageControlWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/PageControlWidget.java new file mode 100644 index 0000000..f35fadc --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/PageControlWidget.java @@ -0,0 +1,100 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.IWidgetParent; +import com.gtnewhorizons.modularui.api.widget.Widget; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Parent widget that can contain children and switch them with pagination. + * At least one child is required. + * Number of children and pages should match. + */ +public class PageControlWidget extends Widget implements IWidgetParent { + + private int currentPage = 0; + private final List pages = new ArrayList<>(); + + @Override + public void onInit() { + if (pages.isEmpty()) { + throw new IllegalStateException("PageControlWidget must have at least one child!"); + } + for (int i = 0; i < pages.size(); i++) { + if (i != currentPage) { + setPage(i, false); + } + } + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + return pages.isEmpty() ? super.determineSize(maxWidth, maxHeight) : MultiChildWidget.getSizeOf(pages); + } + + protected List getPages() { + return pages; + } + + @Override + public List getChildren() { + return Collections.unmodifiableList(pages); + } + + public int getCurrentPage() { + return currentPage; + } + + public void nextPage() { + if (currentPage + 1 >= pages.size()) { + setActivePage(0); + } else { + setActivePage(currentPage + 1); + } + } + + public void prevPage() { + if (currentPage == 0) { + setActivePage(pages.size() - 1); + } else { + setActivePage(currentPage - 1); + } + } + + public void setActivePage(int page) { + if (page > pages.size() - 1 || page < 0) { + throw new IndexOutOfBoundsException("Tried setting active page to " + page + " while only 0 - " + (pages.size() - 1) + " is allowed"); + } + if (!isInitialised()) { + this.currentPage = page; + return; + } + setPage(currentPage, false); + this.currentPage = page; + setPage(currentPage, true); + } + + private void setPage(int page, boolean active) { + Widget widget = pages.get(page); + widget.setEnabled(active); + } + + public PageControlWidget addPage(Widget page) { + if (page == this) { + ModularUI.logger.error("Can't add self!"); + return this; + } + if (isInitialised()) { + ModularUI.logger.error("Can't add child after initialised!"); + } else { + pages.add(page); + } + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/ProgressBar.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/ProgressBar.java new file mode 100644 index 0000000..7e0f6b0 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/ProgressBar.java @@ -0,0 +1,175 @@ +package com.gtnewhorizons.modularui.common.widget; + +import codechicken.lib.math.MathHelper; +import com.gtnewhorizons.modularui.config.Config; +import com.gtnewhorizons.modularui.api.drawable.UITexture; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.Widget; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Supplier; + +public class ProgressBar extends Widget { + + private Supplier progress; + private UITexture emptyTexture; + private final UITexture fullTexture[] = new UITexture[4]; + private Direction direction = Direction.RIGHT; + private int imageSize = -1; + + @Override + public void onInit() { + if (direction == Direction.CIRCULAR_CW && fullTexture[0] != null) { + UITexture base = fullTexture[0]; + fullTexture[0] = base.getSubArea(0f, 0.5f, 0.5f, 1f); + fullTexture[1] = base.getSubArea(0f, 0f, 0.5f, 0.5f); + fullTexture[2] = base.getSubArea(0.5f, 0f, 1f, 0.5f); + fullTexture[3] = base.getSubArea(0.5f, 0.5f, 1f, 1f); + } + } + + @Override + public void onRebuild() { + if (imageSize < 0) { + imageSize = size.width; + } + } + + @Override + public void draw(float partialTicks) { + if (emptyTexture != null) { + emptyTexture.draw(Pos2d.ZERO, getSize(), partialTicks); + } + float progress = this.progress.get(); + if (fullTexture[0] != null && progress > 0) { + if (direction == Direction.CIRCULAR_CW) { + drawCircular(progress); + return; + } + if (progress >= 1) { + fullTexture[0].draw(Pos2d.ZERO, getSize(), partialTicks); + } else { + progress = getProgressUV(progress); + float u0 = 0, v0 = 0, u1 = 1, v1 = 1; + float x = 0, y = 0, width = size.width, height = size.height; + switch (direction) { + case RIGHT: + u1 = progress; + width *= progress; + break; + case LEFT: + u0 = 1 - progress; + width *= progress; + x = size.width - width; + break; + case DOWN: + v1 = progress; + height *= progress; + break; + case UP: + v0 = 1 - progress; + height *= progress; + y = size.height - height; + break; + } + fullTexture[0].drawSubArea(x, y, width, height, u0, v0, u1, v1); + } + } + } + + public float getProgressUV(float uv) { + if (Config.smoothProgressbar) { + return uv; + } + return (float) (Math.floor(uv * imageSize) / imageSize); + } + + private void drawCircular(float progress) { + float[] subAreas = { + getProgressUV((float)MathHelper.clip(progress / 0.25f, 0, 1)), + getProgressUV((float)MathHelper.clip((progress - 0.25f) / 0.25f, 0, 1)), + getProgressUV((float)MathHelper.clip((progress - 0.5f) / 0.25f, 0, 1)), + getProgressUV((float)MathHelper.clip((progress - 0.75f) / 0.25f, 0, 1)) + }; + float halfWidth = size.width / 2f; + float halfHeight = size.height / 2f; + + float progressScaled = subAreas[0] * halfHeight; + fullTexture[0].drawSubArea( + 0, size.height - progressScaled, + halfWidth, progressScaled, + 0.0f, 1.0f - progressScaled / halfHeight, + 1.0f, 1.0f + ); // BL, draw UP + + progressScaled = subAreas[1] * halfWidth; + fullTexture[1].drawSubArea( + 0, 0, + progressScaled, halfHeight, + 0.0f, 0.0f, + progressScaled / (halfWidth), 1.0f + ); // TL, draw RIGHT + + progressScaled = subAreas[2] * halfHeight; + fullTexture[2].drawSubArea( + halfWidth, 0, + halfWidth, progressScaled, + 0.0f, 0.0f, + 1.0f, progressScaled / halfHeight + ); // TR, draw DOWN + + progressScaled = subAreas[3] * halfWidth; + fullTexture[3].drawSubArea( + size.width - progressScaled, halfHeight, + progressScaled, halfHeight, + 1.0f - progressScaled / halfWidth, 0.0f, + 1.0f, 1.0f + ); // BR, draw LEFT + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + return new Size(20, 20); + } + + public ProgressBar setProgress(Supplier progress) { + this.progress = progress; + return this; + } + + public ProgressBar setProgress(float progress) { + this.progress = () -> progress; + return this; + } + + /** + * Sets the texture to render + * + * @param emptyTexture empty bar, always rendered + * @param fullTexture full bar, partly rendered, based on progress + * @param imageSize image size in direction of progress. used for non smooth rendering + */ + public ProgressBar setTexture(UITexture emptyTexture, UITexture fullTexture, int imageSize) { + this.emptyTexture = emptyTexture; + this.fullTexture[0] = fullTexture; + this.imageSize = imageSize; + return this; + } + + /** + * @param texture a texture where the empty and full bar are stacked on top of each other + */ + public ProgressBar setTexture(UITexture texture, int imageSize) { + return setTexture(texture.getSubArea(0, 0, 1, 0.5f), texture.getSubArea(0, 0.5f, 1, 1), imageSize); + } + + public ProgressBar setDirection(Direction direction) { + this.direction = direction; + return this; + } + + public enum Direction { + LEFT, RIGHT, UP, DOWN, CIRCULAR_CW; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/Row.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/Row.java new file mode 100644 index 0000000..b9fb189 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/Row.java @@ -0,0 +1,88 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.math.CrossAxisAlignment; +import com.gtnewhorizons.modularui.api.math.MainAxisAlignment; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.IWidgetBuilder; +import com.gtnewhorizons.modularui.api.widget.Widget; +import org.jetbrains.annotations.NotNull; + +public class Row extends MultiChildWidget implements IWidgetBuilder { + + private MainAxisAlignment maa = MainAxisAlignment.START; + private CrossAxisAlignment caa = CrossAxisAlignment.START; + private int maxWidth = -1, maxHeight = 0; + + @Override + public void addWidgetInternal(Widget widget) { + addChild(widget); + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + if (maa == MainAxisAlignment.START) { + return getSizeOf(children); + } + return new Size(this.maxWidth, this.maxHeight); + } + + @Override + public void layoutChildren(int maxWidthC, int maxHeightC) { + if (maxWidth < 0 && maa != MainAxisAlignment.START) { + if (isAutoSized()) { + maxWidth = maxWidthC - getPos().x; + } else { + maxWidth = getSize().width; + } + } + + this.maxHeight = 0; + int totalWidth = 0; + + for (Widget widget : getChildren()) { + totalWidth += widget.getSize().width; + maxHeight = Math.max(maxHeight, widget.getSize().height); + } + + int lastX = 0; + if (maa == MainAxisAlignment.CENTER) { + lastX = (int) (maxWidth / 2f - totalWidth / 2f); + } else if (maa == MainAxisAlignment.END) { + lastX = maxWidth - totalWidth; + } + + for (Widget widget : getChildren()) { + int y = 0; + if (caa == CrossAxisAlignment.CENTER) { + y = (int) (maxHeight / 2f - widget.getSize().height / 2f); + } else if (caa == CrossAxisAlignment.END) { + y = maxHeight - widget.getSize().height; + } + widget.setPosSilent(new Pos2d(lastX, y)); + lastX += widget.getSize().width; + if (maa == MainAxisAlignment.SPACE_BETWEEN) { + lastX += (maxWidth - totalWidth) / (getChildren().size() - 1); + } + } + } + + public Row setAlignment(MainAxisAlignment maa) { + return setAlignment(maa, caa); + } + + public Row setAlignment(CrossAxisAlignment caa) { + return setAlignment(maa, caa); + } + + public Row setAlignment(MainAxisAlignment maa, CrossAxisAlignment caa) { + this.maa = maa; + this.caa = caa; + return this; + } + + public Row setMaxWidth(int maxWidth) { + this.maxWidth = maxWidth; + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/ScrollBar.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/ScrollBar.java new file mode 100644 index 0000000..2da0af1 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/ScrollBar.java @@ -0,0 +1,225 @@ +package com.gtnewhorizons.modularui.common.widget; + +import codechicken.lib.math.MathHelper; +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.drawable.shapes.Rectangle; +import com.gtnewhorizons.modularui.api.math.Color; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.api.widget.scroll.IHorizontalScrollable; +import com.gtnewhorizons.modularui.api.widget.scroll.IVerticalScrollable; +import com.gtnewhorizons.modularui.api.widget.scroll.ScrollType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ScrollBar extends Widget implements Interactable { + + public static ScrollBar defaultTextScrollBar() { + return new ScrollBar() + .setBarTexture(new Rectangle() + .setColor(Color.WHITE.normal) + .setCornerRadius(1)); + } + + private ScrollType scrollType; + private IVerticalScrollable verticalScrollable; + private IHorizontalScrollable horizontalScrollable; + private IDrawable barTexture = IDrawable.EMPTY; + private int handleClickOffset = -1; + private int posOffset = 0; + + public void setScrollType(ScrollType scrollType, @Nullable IHorizontalScrollable horizontalScrollable, @Nullable IVerticalScrollable verticalScrollable) { + this.scrollType = scrollType; + this.verticalScrollable = verticalScrollable; + this.horizontalScrollable = horizontalScrollable; + } + + @Override + public void onInit() { + if ((scrollType == ScrollType.VERTICAL && verticalScrollable == null) || (scrollType == ScrollType.HORIZONTAL && horizontalScrollable == null)) { + throw new IllegalStateException("Scroll bar was not properly initialized"); + } + if (isAutoSized()) { + setSizeProvider((screenSize, window, parent) -> { + if (scrollType == ScrollType.HORIZONTAL) { + return new Size(horizontalScrollable.getVisibleWidth(), horizontalScrollable.getHorizontalBarHeight()); + } else if (scrollType == ScrollType.VERTICAL) { + return new Size(verticalScrollable.getVerticalBarWidth(), verticalScrollable.getVisibleHeight()); + } + return Size.ZERO; + }); + } + if (isAutoPositioned()) { + setPosProvider((screenSize, window, parent) -> { + if (scrollType == ScrollType.HORIZONTAL) { + return new Pos2d(0, parent.getSize().height - horizontalScrollable.getHorizontalBarHeight() + posOffset); + } else if (scrollType == ScrollType.VERTICAL) { + return new Pos2d(parent.getSize().width - verticalScrollable.getVerticalBarWidth() + posOffset, 0); + } + return Pos2d.ZERO; + }); + } + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + if (scrollType == ScrollType.HORIZONTAL) { + return new Size(horizontalScrollable.getVisibleWidth(), horizontalScrollable.getHorizontalBarHeight()); + } else if (scrollType == ScrollType.VERTICAL) { + return new Size(verticalScrollable.getVerticalBarWidth(), verticalScrollable.getVisibleHeight()); + } + return super.determineSize(maxWidth, maxHeight); + } + + public int getVisibleSize() { + if (scrollType == ScrollType.HORIZONTAL) { + return horizontalScrollable.getVisibleWidth(); + } else if (scrollType == ScrollType.VERTICAL) { + return verticalScrollable.getVisibleHeight(); + } + return 0; + } + + public int getActualSize() { + if (scrollType == ScrollType.HORIZONTAL) { + return horizontalScrollable.getActualWidth(); + } else if (scrollType == ScrollType.VERTICAL) { + return verticalScrollable.getActualHeight(); + } + return 0; + } + + public boolean isActive() { + int actualSize = getActualSize(); + return actualSize > 0 && actualSize > getVisibleSize(); + } + + @Override + public void draw(float partialTicks) { + if (isActive() && this.barTexture != null) { + int size = calculateMainAxisSize(); + if (scrollType == ScrollType.HORIZONTAL) { + float offset = horizontalScrollable.getHorizontalScrollOffset() / (float) (horizontalScrollable.getActualWidth()); + this.barTexture.draw(horizontalScrollable.getVisibleWidth() * offset, 0, size, horizontalScrollable.getHorizontalBarHeight(), partialTicks); + } else if (scrollType == ScrollType.VERTICAL) { + float offset = verticalScrollable.getVerticalScrollOffset() / (float) (verticalScrollable.getActualHeight()); + this.barTexture.draw(0, verticalScrollable.getVisibleHeight() * offset, verticalScrollable.getVerticalBarWidth(), size, partialTicks); + } + } + } + + public int calculateMainAxisSize() { + int actualSize = getActualSize(); + if (actualSize == 0) { + return 1; + } + int size = getVisibleSize(); + return (int) Math.max(size / (double) actualSize * size, 6); + } + + public void clampScrollOffset() { + setScrollOffset(getScrollOffset()); + } + + public void setScrollOffsetOfCursor(float x) { + int visible = getVisibleSize(); + setScrollOffset((int) (x - visible / 2f)); + } + + public void setScrollOffset(int offset) { + offset = (int) MathHelper.clip(offset, 0, getActualSize() - getVisibleSize()); + if (scrollType == ScrollType.HORIZONTAL) { + horizontalScrollable.setHorizontalScrollOffset(offset); + } else if (scrollType == ScrollType.VERTICAL) { + verticalScrollable.setVerticalScrollOffset(offset); + } + } + + public int getScrollOffset() { + if (scrollType == ScrollType.HORIZONTAL) { + return horizontalScrollable.getHorizontalScrollOffset(); + } else if (scrollType == ScrollType.VERTICAL) { + return verticalScrollable.getVerticalScrollOffset(); + } + return 0; + } + + public int getBarSize() { + if (scrollType == ScrollType.HORIZONTAL) { + return horizontalScrollable.getHorizontalBarHeight(); + } else if (scrollType == ScrollType.VERTICAL) { + return verticalScrollable.getVerticalBarWidth(); + } + return 0; + } + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + Pos2d relative = getContext().getCursor().getPos().subtract(getAbsolutePos()); + int barSize = calculateMainAxisSize(); + int actualSize = getActualSize(); + if (scrollType == ScrollType.HORIZONTAL) { + float offset = horizontalScrollable.getHorizontalScrollOffset() / (float) (actualSize) * horizontalScrollable.getVisibleWidth(); + if (relative.x >= offset && relative.x <= offset + barSize) { + this.handleClickOffset = (int) (relative.x - offset); + } else { + float newOffset = Math.max(0, (relative.x - barSize / 2f) / (float) horizontalScrollable.getVisibleWidth()); + setScrollOffset((int) (newOffset * actualSize)); + } + } else if (scrollType == ScrollType.VERTICAL) { + float offset = verticalScrollable.getVerticalScrollOffset() / (float) (actualSize) * verticalScrollable.getVisibleHeight(); + if (relative.y >= offset && relative.y <= offset + barSize) { + this.handleClickOffset = (int) (relative.y - offset); + } else { + float newOffset = Math.max(0, (relative.y - barSize / 2f) / (float) verticalScrollable.getVisibleHeight()); + setScrollOffset((int) (newOffset * actualSize)); + } + } + return ClickResult.ACCEPT; + } + + @Override + public void onMouseDragged(int buttonId, long deltaTime) { + if (this.handleClickOffset >= 0) { + int actualSize = getActualSize(); + if (scrollType == ScrollType.HORIZONTAL) { + int offset = getContext().getCursor().getX() - pos.x - this.handleClickOffset; + float newOffset = Math.max(0, offset / (float) horizontalScrollable.getVisibleWidth()); + setScrollOffset((int) (newOffset * actualSize)); + } else if (scrollType == ScrollType.VERTICAL) { + int offset = getContext().getCursor().getY() - pos.y - this.handleClickOffset; + float newOffset = Math.max(0, offset / (float) verticalScrollable.getVisibleHeight()); + setScrollOffset((int) (newOffset * actualSize)); + } + } + } + + @Override + public boolean onClickReleased(int buttonId) { + if (this.handleClickOffset >= 0) { + this.handleClickOffset = -1; + return true; + } + return false; + } + + @Override + public boolean onMouseScroll(int direction) { + if (isActive()) { + setScrollOffset(getScrollOffset() - direction * 6); + } + return true; + } + + public ScrollBar setBarTexture(IDrawable barTexture) { + this.barTexture = barTexture; + return this; + } + + public ScrollBar setPosOffset(int posOffset) { + this.posOffset = posOffset; + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/Scrollable.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/Scrollable.java new file mode 100644 index 0000000..4ac3763 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/Scrollable.java @@ -0,0 +1,211 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.drawable.GuiHelper; +import com.gtnewhorizons.modularui.api.drawable.shapes.Rectangle; +import com.gtnewhorizons.modularui.api.math.Color; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.IWidgetBuilder; +import com.gtnewhorizons.modularui.api.widget.IWidgetParent; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.api.widget.scroll.IHorizontalScrollable; +import com.gtnewhorizons.modularui.api.widget.scroll.IVerticalScrollable; +import com.gtnewhorizons.modularui.api.widget.scroll.ScrollType; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; + +import java.util.ArrayList; +import java.util.List; + +public class Scrollable extends Widget implements IWidgetBuilder, IWidgetParent, Interactable, IHorizontalScrollable, IVerticalScrollable { + + private int xScroll = 0, yScroll = 0; + private final List children = new ArrayList<>(); + private final List allChildren = new ArrayList<>(); + private Size actualSize = Size.ZERO; + private int grabScrollX = -1, grabScrollY = -1; + @UnknownNullability + private ScrollBar horizontalScrollBar, verticalScrollBar; + + @Override + public void setHorizontalScrollOffset(int offset) { + if (!canScrollHorizontal() || !this.horizontalScrollBar.isActive()) { + offset = 0; + } + if (this.xScroll != offset) { + int dif = xScroll - offset; + this.xScroll = offset; + for (Widget widget : children) { + widget.setPosSilent(widget.getPos().add(dif, 0)); + widget.setEnabled(intersects(widget)); + } + } + } + + @Override + public int getHorizontalScrollOffset() { + return xScroll; + } + + @Override + public int getVisibleWidth() { + return size.width; + } + + @Override + public int getActualWidth() { + return actualSize.width; + } + + @Override + public void setVerticalScrollOffset(int offset) { + if (!canScrollVertical() || !this.verticalScrollBar.isActive()) { + offset = 0; + } + if (this.yScroll != offset) { + int dif = yScroll - offset; + this.yScroll = offset; + for (Widget widget : children) { + widget.setPosSilent(widget.getPos().add(0, dif)); + widget.setEnabled(intersects(widget)); + } + } + } + + @Override + public int getVerticalScrollOffset() { + return this.yScroll; + } + + @Override + public int getVisibleHeight() { + return size.height; + } + + @Override + public int getActualHeight() { + return actualSize.height; + } + + public boolean canScrollVertical() { + return verticalScrollBar != null; + } + + public boolean canScrollHorizontal() { + return horizontalScrollBar != null; + } + + @Override + public void initChildren() { + if (canScrollHorizontal()) { + this.allChildren.add(horizontalScrollBar); + } + if (canScrollVertical()) { + this.allChildren.add(verticalScrollBar); + } + } + + @Override + public void onRebuild() { + this.actualSize = MultiChildWidget.getSizeOf(children); + if (this.xScroll < 0 || this.yScroll == 0) { + setHorizontalScrollOffset(0); + setVerticalScrollOffset(0); + checkNeedsRebuild(); + } + } + + @Override + public void addWidgetInternal(Widget widget) { + if (MultiChildWidget.checkChild(this, widget)) { + this.children.add(widget); + this.allChildren.add(widget); + } + } + + @Override + public boolean childrenMustBeInBounds() { + return true; + } + + @Override + public void drawChildren(float partialTicks) { + GuiHelper.useScissor(pos.x, pos.y, size.width, size.height, () -> { + IWidgetParent.super.drawChildren(partialTicks); + }); + } + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + this.grabScrollX = getContext().getMousePos().x; + this.grabScrollY = getContext().getMousePos().y; + return ClickResult.ACCEPT; + } + + @Override + public boolean onClickReleased(int buttonId) { + if (this.grabScrollX >= 0 || this.grabScrollY >= 0) { + this.grabScrollX = -1; + this.grabScrollY = -1; + return true; + } + return false; + } + + @Override + public void onMouseDragged(int buttonId, long deltaTime) { + if (this.grabScrollX >= 0 && this.grabScrollY >= 0) { + int dif = getContext().getMousePos().x - grabScrollX; + if (dif != 0 && canScrollHorizontal()) { + horizontalScrollBar.setScrollOffset(xScroll - dif); + grabScrollX = getContext().getMousePos().x; + } + dif = getContext().getMousePos().y - grabScrollY; + if (dif != 0 && canScrollVertical()) { + verticalScrollBar.setScrollOffset(yScroll - dif); + grabScrollY = getContext().getMousePos().y; + } + } + } + + @Override + public boolean onMouseScroll(int direction) { + if (canScrollHorizontal() && (canScrollVertical() && Interactable.hasShiftDown()) || !canScrollVertical()) { + horizontalScrollBar.onMouseScroll(direction); + } else if (canScrollVertical()) { + verticalScrollBar.onMouseScroll(direction); + } + return true; + } + + @Override + public List getChildren() { + return this.allChildren; + } + + public Scrollable setHorizontalScroll(@Nullable ScrollBar scrollBar) { + this.horizontalScrollBar = scrollBar; + if (this.horizontalScrollBar != null) { + this.horizontalScrollBar.setScrollType(ScrollType.HORIZONTAL, this, null); + } + return this; + } + + public Scrollable setHorizontalScroll() { + return setHorizontalScroll(new ScrollBar() + .setBarTexture(new Rectangle().setColor(Color.WHITE.normal).setCornerRadius(1))); + } + + public Scrollable setVerticalScroll(@Nullable ScrollBar scrollBar) { + this.verticalScrollBar = scrollBar; + if (this.verticalScrollBar != null) { + this.verticalScrollBar.setScrollType(ScrollType.VERTICAL, null, this); + } + return this; + } + + public Scrollable setVerticalScroll() { + return setVerticalScroll(new ScrollBar() + .setBarTexture(new Rectangle().setColor(Color.WHITE.normal).setCornerRadius(1))); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/SingleChildWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/SingleChildWidget.java new file mode 100644 index 0000000..e11d0ff --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/SingleChildWidget.java @@ -0,0 +1,51 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.IWidgetParent; +import com.gtnewhorizons.modularui.api.widget.Widget; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class SingleChildWidget extends Widget implements IWidgetParent { + + private Widget child; + + public SingleChildWidget() { + } + + public SingleChildWidget(Size size) { + super(size); + } + + public SingleChildWidget(Size size, Pos2d pos) { + super(size, pos); + } + + public final SingleChildWidget setChild(Widget widget) { + if (this.child != null) { + ModularUI.logger.error("Child is already set!"); + } else if (MultiChildWidget.checkEditable(this)) { + this.child = widget; + } + return this; + } + + @Override + public List getChildren() { + return Collections.singletonList(child); + } + + public Widget getChild() { + return child; + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + return child.getSize(); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/SliderWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/SliderWidget.java new file mode 100644 index 0000000..7adfc46 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/SliderWidget.java @@ -0,0 +1,156 @@ +package com.gtnewhorizons.modularui.common.widget; + +import codechicken.lib.math.MathHelper; +import com.gtnewhorizons.modularui.api.ModularUITextures; +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.network.PacketBuffer; + +import java.io.IOException; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class SliderWidget extends SyncedWidget implements Interactable { + + private IDrawable slider = ModularUITextures.BASE_BUTTON; + private Size handleSize = new Size(8, 0); + private float min = 0, max = 1; + @SideOnly(Side.CLIENT) + private float sliderPos = 0; + private float value = 0; + private Supplier getter; + private Consumer setter; + + private boolean grabHandle = false; + + public SliderWidget() { + setBackground(ModularUITextures.ITEM_SLOT); + } + + public float toValue(float pos) { + return min + (max - min) * (pos / (size.width - handleSize.width)); + } + + public float toPos(float value) { + return (value - min) / (max - min) * (size.width - handleSize.width); + } + + @SideOnly(Side.CLIENT) + public void updateSlider(int relativePos, boolean sync) { + setValue(toValue((float)MathHelper.clip(relativePos - handleSize.width / 2f, 0, size.width - handleSize.width)), sync); + } + + @Override + public void onInit() { + setValue(getter.get(), false); + } + + @Override + public void onRebuild() { + this.handleSize = new Size(handleSize.width > 0 ? handleSize.width : 8, handleSize.height > 0 ? handleSize.height : size.height); + } + + @Override + public void draw(float partialTicks) { + slider.draw(sliderPos, size.height / 2f - handleSize.height / 2f, handleSize.width, handleSize.height, partialTicks); + } + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + this.grabHandle = true; + int clickPos = getContext().getCursor().getX() - getAbsolutePos().x; + if (!(clickPos >= this.sliderPos && clickPos < this.sliderPos + handleSize.width)) { + updateSlider(clickPos, true); + } + return ClickResult.SUCCESS; + } + + @Override + public boolean onClickReleased(int buttonId) { + if (this.grabHandle) { + this.grabHandle = false; + setValue(this.value, true); + return true; + } + return false; + } + + @Override + public void onMouseDragged(int buttonId, long deltaTime) { + if (this.grabHandle) { + updateSlider(getContext().getCursor().getX() - getAbsolutePos().x, false); + } + } + + @Override + public void detectAndSendChanges(boolean init) { + if (syncsToClient()) { + float val = getter.get(); + if (init || val != value) { + setValue(val, true); + } + } + } + + @Override + public void readOnClient(int id, PacketBuffer buf) throws IOException { + if (id == 1) { + setValue(buf.readFloat(), false); + } + } + + @Override + public void readOnServer(int id, PacketBuffer buf) throws IOException { + if (id == 1) { + setValue(buf.readFloat(), false); + } + } + + public void setValue(float value, boolean sync) { + this.value = value; + if (sync) { + if (isClient()) { + syncToServer(1, buffer -> buffer.writeFloat(this.value)); + } else { + syncToClient(1, buffer -> buffer.writeFloat(this.value)); + } + } + if (isClient()) { + this.sliderPos = toPos(this.value); + } + this.setter.accept(this.value); + } + + public float getValue() { + return this.value; + } + + public SliderWidget setHandleTexture(IDrawable slider) { + this.slider = slider; + return this; + } + + public SliderWidget setHandleSize(Size handleSize) { + this.handleSize = handleSize; + return this; + } + + public SliderWidget setBounds(float min, float max) { + this.min = min; + this.max = max; + return this; + } + + public SliderWidget setGetter(Supplier getter) { + this.getter = getter; + return this; + } + + public SliderWidget setSetter(Consumer setter) { + this.setter = setter; + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/SlotGroup.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/SlotGroup.java new file mode 100644 index 0000000..94d6029 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/SlotGroup.java @@ -0,0 +1,183 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.api.forge.IItemHandlerModifiable; +import com.gtnewhorizons.modularui.api.forge.PlayerMainInvWrapper; +import com.gtnewhorizons.modularui.api.math.Alignment; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.common.internal.wrapper.BaseSlot; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraftforge.fluids.IFluidTank; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +public class SlotGroup extends MultiChildWidget { + + public static final int PLAYER_INVENTORY_HEIGHT = 76; + + public static SlotGroup playerInventoryGroup(EntityPlayer player) { + PlayerMainInvWrapper wrapper = new PlayerMainInvWrapper(player.inventory); + SlotGroup slotGroup = new SlotGroup(); + + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + SlotWidget slot = new SlotWidget(new BaseSlot(wrapper, col + (row + 1) * 9)) + .setPos(new Pos2d(col * 18, row * 18)); + slotGroup.addSlot(slot); + } + } + + for (int i = 0; i < 9; i++) { + SlotWidget slot = new SlotWidget(new BaseSlot(wrapper, i)) + .setPos(new Pos2d(i * 18, 58)); + slotGroup.addSlot(slot); + } + return slotGroup; + } + + public static SlotGroup ofItemHandler(IItemHandlerModifiable itemHandler, int slotsWidth, boolean output, @Nullable String sortArea) { + return ofItemHandler(itemHandler, slotsWidth, 0, 0, itemHandler.getSlots() - 1, sortArea, output); + } + + public static SlotGroup ofItemHandler(IItemHandlerModifiable itemHandler, int slotsWidth, int shiftClickPriority, @Nullable String sortArea) { + return ofItemHandler(itemHandler, slotsWidth, shiftClickPriority, 0, itemHandler.getSlots() - 1, sortArea, false); + } + + public static SlotGroup ofItemHandler(IItemHandlerModifiable itemHandler, int slotsWidth, int shiftClickPriority, int startFromSlot, int endAtSlot, @Nullable String sortArea) { + return ofItemHandler(itemHandler, slotsWidth, shiftClickPriority, startFromSlot, endAtSlot, sortArea, false); + } + + public static SlotGroup ofItemHandler(IItemHandlerModifiable itemHandler, int slotsWidth, int shiftClickPriority, int startFromSlot, int endAtSlot, @Nullable String sortArea, boolean output) { + SlotGroup slotGroup = new SlotGroup(); + if (itemHandler.getSlots() >= endAtSlot) { + endAtSlot = itemHandler.getSlots() - 1; + } + startFromSlot = Math.max(startFromSlot, 0); + if (startFromSlot > endAtSlot) { + return slotGroup; + } + slotsWidth = Math.max(slotsWidth, 1); + int x = 0, y = 0; + for (int i = startFromSlot; i < endAtSlot + 1; i++) { + slotGroup.addSlot(new SlotWidget(new BaseSlot(itemHandler, i, false).setAccess(!output, true).setShiftClickPriority(shiftClickPriority)) + .setPos(new Pos2d(x * 18, y * 18))); + if (++x == slotsWidth) { + x = 0; + y++; + } + } + return slotGroup; + } + + private String section = null; + private int slotsPerRow = 9; + + @Override + public void onInit() {} + + public SlotGroup addSlot(SlotWidget slotWidget) { + addChild(slotWidget); + return this; + } + + public SlotGroup setSortable(String areaName) { + return setSortable(areaName, 9); + } + + public SlotGroup setSortable(String areaName, int rowSize) { + section = areaName; + slotsPerRow = rowSize; + return this; + } + + public static class Builder { + private final List rows = new ArrayList<>(); + private final Map> widgetCreatorMap = new HashMap<>(); + private Size cellSize = new Size(18, 18); + private Size totalSize; + private Alignment alignment = Alignment.TopLeft; + + public Builder setCellSize(Size cellSize) { + this.cellSize = cellSize; + return this; + } + + public Builder setSize(Size totalSize, Alignment contentAlignment) { + this.totalSize = totalSize; + this.alignment = contentAlignment; + return this; + } + + public Builder setSize(Size totalSize) { + return setSize(totalSize, this.alignment); + } + + public Builder row(String row) { + this.rows.add(row); + return this; + } + + public Builder where(char c, Function widgetCreator) { + this.widgetCreatorMap.put(c, widgetCreator); + return this; + } + + public Builder where(char c, IItemHandlerModifiable inventory) { + this.widgetCreatorMap.put(c, i -> new SlotWidget(inventory, i)); + return this; + } + + public Builder where(char c, IFluidTank[] inventory) { + this.widgetCreatorMap.put(c, i -> new FluidSlotWidget(inventory[i])); + return this; + } + + public Builder where(char c, List inventory) { + this.widgetCreatorMap.put(c, i -> new FluidSlotWidget(inventory.get(i))); + return this; + } + + public SlotGroup build() { + int maxRowWith = 0; + for (String row : rows) { + maxRowWith = Math.max(maxRowWith, row.length()); + } + Size contentSize = new Size(maxRowWith * cellSize.width, rows.size() * cellSize.height); + Pos2d offsetPos = Pos2d.ZERO; + if (totalSize != null) { + offsetPos = alignment.getAlignedPos(totalSize, contentSize); + } + Map charCount = new HashMap<>(); + SlotGroup slotGroup = new SlotGroup(); + + for (int i = 0; i < rows.size(); i++) { + String row = rows.get(i); + for (int j = 0; j < row.length(); j++) { + char c = row.charAt(j); + if (c == ' ') { + continue; + } + Function widgetCreator = this.widgetCreatorMap.get(c); + if (widgetCreator == null) { + ModularUI.logger.warn("Key {} was not found in Slot group.", c); + continue; + } + Widget widget = widgetCreator.apply(charCount.computeIfAbsent(c, key -> new AtomicInteger()).getAndIncrement()); + if (widget != null) { + slotGroup.addChild(widget.setPos(offsetPos.add(j * cellSize.width, i * cellSize.height))); + } + } + } + return slotGroup; + } + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/SlotWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/SlotWidget.java new file mode 100644 index 0000000..1e7c703 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/SlotWidget.java @@ -0,0 +1,438 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.ModularUITextures; +import com.gtnewhorizons.modularui.api.NumberFormat; +import com.gtnewhorizons.modularui.api.drawable.Text; +import com.gtnewhorizons.modularui.api.drawable.TextRenderer; +import com.gtnewhorizons.modularui.api.forge.IItemHandlerModifiable; +import com.gtnewhorizons.modularui.api.math.Alignment; +import com.gtnewhorizons.modularui.api.math.Color; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.nei.IGhostIngredientHandler; +import com.gtnewhorizons.modularui.api.widget.IGhostIngredientTarget; +import com.gtnewhorizons.modularui.api.widget.IIngredientProvider; +import com.gtnewhorizons.modularui.api.widget.ISyncedWidget; +import com.gtnewhorizons.modularui.api.widget.IVanillaSlot; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.common.internal.Theme; +import com.gtnewhorizons.modularui.common.internal.wrapper.BaseSlot; +import com.gtnewhorizons.modularui.common.internal.wrapper.GhostIngredientWrapper; +import com.gtnewhorizons.modularui.common.internal.wrapper.ModularGui; +import com.gtnewhorizons.modularui.mixins.GuiContainerMixin; +import com.gtnewhorizons.modularui.api.widget.*; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.resources.I18n; +import net.minecraft.inventory.Container; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketBuffer; +import net.minecraft.util.EnumChatFormatting; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL11; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class SlotWidget extends Widget implements IVanillaSlot, Interactable, ISyncedWidget, IIngredientProvider, IGhostIngredientTarget { + + public static final Size SIZE = new Size(18, 18); + + private final TextRenderer textRenderer = new TextRenderer(); + private final BaseSlot slot; + private ItemStack lastStoredPhantomItem = null; + @Nullable + private String sortAreaName = null; + + public SlotWidget(BaseSlot slot) { + this.slot = slot; + } + + public SlotWidget(IItemHandlerModifiable handler, int index) { + this(new BaseSlot(handler, index, false)); + } + + public static SlotWidget phantom(IItemHandlerModifiable handler, int index) { + return new SlotWidget(BaseSlot.phantom(handler, index)); + } + + @Override + public void onInit() { + getContext().getContainer().addSlotToContainer(this.slot); + if (getBackground() == null) { + setBackground(ModularUITextures.ITEM_SLOT); + } + if (!isClient() && this.slot.getStack() != null) { + this.lastStoredPhantomItem = this.slot.getStack().copy(); + } + } + + @Override + public void onDestroy() { + getContext().getContainer().removeSlot(this.slot); + } + + @Override + public BaseSlot getMcSlot() { + return this.slot; + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + return SIZE; + } + + @Override + public @Nullable String getBackgroundColorKey() { + return Theme.KEY_ITEM_SLOT; + } + + @Override + public void draw(float partialTicks) { + RenderHelper.enableGUIStandardItemLighting(); + drawSlot(this.slot); + RenderHelper.enableStandardItemLighting(); + GlStateManager.disableLighting(); + if (isHovering()) { + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_BLEND); + GlStateManager.colorMask(true, true, true, false); + ModularGui.drawSolidRect(1, 1, 16, 16, Theme.INSTANCE.getSlotHighlight()); + GlStateManager.colorMask(true, true, true, true); + GL11.glDisable(GL11.GL_BLEND); + } + } + + @Override + public void onRebuild() { + Pos2d pos = getAbsolutePos().subtract(getContext().getMainWindow().getPos()).add(1, 1); + if (this.slot.xDisplayPosition != pos.x || this.slot.yDisplayPosition != pos.y) { + this.slot.xDisplayPosition = pos.x; + this.slot.yDisplayPosition = pos.y; + } + } + + @Override + public void detectAndSendChanges(boolean init) { + if (init || this.slot.isNeedsSyncing()) { + getContext().syncSlotContent(this.slot); + this.slot.resetNeedsSyncing(); + } + } + + @Override + public void buildTooltip(List tooltip) { + if (isPhantom()) { + tooltip.add(Text.localised("modularui.item.phantom.control")); + } + } + + @Override + public List getExtraTooltip() { + List extraLines = new ArrayList<>(); + if (slot.getStack().stackSize >= 1000) { + extraLines.add(I18n.format("modularui.amount", slot.getStack().stackSize)); + } + if (isPhantom()) { + String[] lines = I18n.format("modularui.item.phantom.control").split("\\\\n"); + extraLines.addAll(Arrays.asList(lines)); + } + return extraLines.isEmpty() ? Collections.emptyList() : extraLines; + } + + public boolean isPhantom() { + return this.slot.isPhantom(); + } + + @Override + public Object getIngredient() { + return slot.getStack(); + } + + @Override + public SlotWidget setPos(Pos2d relativePos) { + return (SlotWidget) super.setPos(relativePos); + } + + @Override + public SlotWidget setSize(Size size) { + return (SlotWidget) super.setSize(size); + } + + public SlotWidget setShiftClickPrio(int prio) { + this.slot.setShiftClickPriority(prio); + return this; + } + + public SlotWidget disableShiftInsert() { + this.slot.disableShiftInsert(); + return this; + } + + public SlotWidget setChangeListener(Runnable runnable) { + this.slot.setChangeListener(runnable); + return this; + } + + public SlotWidget setChangeListener(Consumer changeListener) { + return setChangeListener(() -> changeListener.accept(this)); + } + + public SlotWidget setFilter(Predicate filter) { + this.slot.setFilter(filter); + return this; + } + + public SlotWidget setAccess(boolean canTake, boolean canInsert) { + this.slot.setAccess(canInsert, canTake); + return this; + } + + public SlotWidget setIgnoreStackSizeLimit(boolean ignoreStackSizeLimit) { + this.slot.setIgnoreStackSizeLimit(ignoreStackSizeLimit); + return this; + } + + public SlotWidget setSortable(String areaName) { + if (this.sortAreaName == null ^ areaName == null) { + this.sortAreaName = areaName; + } + return this; + } + + @Override + public SlotWidget setEnabled(boolean enabled) { + if (enabled != isEnabled()) { + super.setEnabled(enabled); + slot.setEnabled(enabled); + if (isClient()) { + syncToServer(4, buffer -> buffer.writeBoolean(enabled)); + } + } + return this; + } + + @Override + public void readOnClient(int id, PacketBuffer buf) { + + } + + @Override + public void readOnServer(int id, PacketBuffer buf) throws IOException { + if (id == 1) { + this.slot.xDisplayPosition = buf.readVarIntFromBuffer(); + this.slot.yDisplayPosition = buf.readVarIntFromBuffer(); + } else if (id == 2) { + phantomClick(ClickData.readPacket(buf)); + } else if (id == 3) { + phantomScroll(buf.readVarIntFromBuffer()); + } else if (id == 4) { + setEnabled(buf.readBoolean()); + } else if (id == 5) { + this.slot.putStack(buf.readItemStackFromBuffer()); + } + } + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + if (isPhantom()) { + syncToServer(2, buffer -> ClickData.create(buttonId, doubleClick).writeToPacket(buffer)); + return ClickResult.ACCEPT; + } + return ClickResult.REJECT; + } + + @Override + public boolean onMouseScroll(int direction) { + if (isPhantom()) { + if (Interactable.hasShiftDown()) { + direction *= 8; + } + final int finalDirection = direction; + syncToServer(3, buffer -> buffer.writeVarIntToBuffer(finalDirection)); + return true; + } + return false; + } + + protected void phantomClick(ClickData clickData) { + ItemStack cursorStack = getContext().getCursor().getItemStack(); + ItemStack slotStack = getMcSlot().getStack(); + ItemStack stackToPut; + if (slotStack == null) { + if (cursorStack == null) { + if (clickData.mouseButton == 1 && this.lastStoredPhantomItem != null) { + stackToPut = this.lastStoredPhantomItem.copy(); + } else { + return; + } + } else { + stackToPut = cursorStack.copy(); + } + if (clickData.mouseButton == 1) { + stackToPut.stackSize = 1; + } + slot.putStack(stackToPut); + this.lastStoredPhantomItem = stackToPut.copy(); + } else { + if (clickData.mouseButton == 0) { + if (clickData.shift) { + this.slot.putStack(null); + } else { + this.slot.incrementStackCount(-1); + } + } else if (clickData.mouseButton == 1) { + this.slot.incrementStackCount(1); + } + } + } + + protected void phantomScroll(int direction) { + ItemStack currentItem = this.slot.getStack(); + if (direction > 0 && currentItem == null && lastStoredPhantomItem != null) { + ItemStack stackToPut = this.lastStoredPhantomItem.copy(); + stackToPut.stackSize = direction; + this.slot.putStack(stackToPut); + } else { + this.slot.incrementStackCount(direction); + } + } + + @Override + public IGhostIngredientHandler.@Nullable Target getTarget(@NotNull ItemStack ingredient) { + if (!isPhantom() || !(ingredient instanceof ItemStack) || ((ItemStack) ingredient) == null) { + return null; + } + return new GhostIngredientWrapper<>(this); + } + + @Override + public void accept(@NotNull ItemStack ingredient) { + syncToServer(5, buffer -> { + try { + buffer.writeItemStackToBuffer(ingredient); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + + private GuiContainerMixin getGuiAccessor() { + return getContext().getScreen().getAccessor(); + } + + private ModularGui getScreen() { + return getContext().getScreen(); + } + + /** + * Copied from {@link net.minecraft.client.gui.inventory.GuiContainer} and removed the bad parts + */ + @SideOnly(Side.CLIENT) + private void drawSlot(Slot slotIn) { + int x = slotIn.xDisplayPosition; + int y = slotIn.yDisplayPosition; + ItemStack itemstack = slotIn.getStack(); + boolean flag = false; + boolean flag1 = slotIn == getGuiAccessor().getClickedSlot() && getGuiAccessor().getDraggedStack() != null && !getGuiAccessor().getIsRightMouseClick(); + ItemStack itemstack1 = getScreen().mc.thePlayer.inventory.getItemStack(); + int amount = -1; + String format = null; + + if (slotIn == this.getGuiAccessor().getClickedSlot() && getGuiAccessor().getDraggedStack() != null && getGuiAccessor().getIsRightMouseClick() && itemstack != null) { + itemstack = itemstack.copy(); + itemstack.stackSize = itemstack.stackSize / 2; + } else if (getScreen().isDragSplitting2() && getScreen().getDragSlots().contains(slotIn) && itemstack1 != null) { + if (getScreen().getDragSlots().size() == 1) { + return; + } + + // Container#canAddItemToSlot + if (Container.func_94527_a(slotIn, itemstack1, true) && getScreen().inventorySlots.canDragIntoSlot(slotIn)) { + itemstack = itemstack1.copy(); + flag = true; + // Container#computeStackSize + Container.func_94525_a(getScreen().getDragSlots(), getGuiAccessor().getDragSplittingLimit(), itemstack, slotIn.getStack() == null ? 0 : slotIn.getStack().stackSize); + int k = Math.min(itemstack.getMaxStackSize(), slotIn.getSlotStackLimit()); + + if (itemstack.stackSize > k) { + amount = k; + format = EnumChatFormatting.YELLOW.toString(); + itemstack.stackSize = k; + } + } else { + getScreen().getDragSlots().remove(slotIn); + getGuiAccessor().invokeUpdateDragSplitting(); + } + } + + getScreen().setZ(100f); + getScreen().getItemRenderer().zLevel = 100.0F; + + if (!flag1) { + if (flag) { + ModularGui.drawSolidRect(1, 1, 16, 16, -2130706433); + } + + if (itemstack != null) { + GlStateManager.enableRescaleNormal(); + GlStateManager.enableLighting(); + RenderHelper.enableGUIStandardItemLighting(); + GlStateManager.enableDepth(); + // render the item itself + getScreen().getItemRenderer().renderItemAndEffectIntoGUI(getScreen().getFontRenderer(), Minecraft.getMinecraft().getTextureManager(), itemstack, 1, 1); + if (amount < 0) { + amount = itemstack.stackSize; + } + // render the amount overlay + if (amount > 1 || format != null) { + String amountText = NumberFormat.format(amount, 2); + if (format != null) { + amountText = format + amountText; + } + float scale = 1f; + if (amountText.length() == 3) { + scale = 0.8f; + } else if (amountText.length() == 4) { + scale = 0.6f; + } else if (amountText.length() > 4) { + scale = 0.5f; + } + textRenderer.setShadow(true); + textRenderer.setScale(scale); + textRenderer.setColor(Color.WHITE.normal); + textRenderer.setAlignment(Alignment.BottomRight, size.width - 1, size.height - 1); + textRenderer.setPos(1, 1); + GlStateManager.disableLighting(); + GlStateManager.disableDepth(); + GlStateManager.disableBlend(); + textRenderer.draw(amountText); + GlStateManager.enableLighting(); + GlStateManager.enableDepth(); + GlStateManager.enableBlend(); + } + + int cachedCount = itemstack.stackSize; + itemstack.stackSize = 1; // required to not render the amount overlay + // render other overlays like durability bar + getScreen().getItemRenderer().renderItemOverlayIntoGUI(getScreen().getFontRenderer(), Minecraft.getMinecraft().getTextureManager(), itemstack, 1, 1, null); + itemstack.stackSize = cachedCount; + GlStateManager.disableDepth(); + } + } + + getScreen().getItemRenderer().zLevel = 0.0F; + getScreen().setZ(0f); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/SortableListItem.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/SortableListItem.java new file mode 100644 index 0000000..105234a --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/SortableListItem.java @@ -0,0 +1,195 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.ModularUITextures; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.screen.Cursor; +import com.gtnewhorizons.modularui.api.screen.ModularWindow; +import com.gtnewhorizons.modularui.api.widget.IDraggable; +import com.gtnewhorizons.modularui.api.widget.IWidgetParent; +import com.gtnewhorizons.modularui.api.widget.Widget; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class SortableListItem extends Widget implements IWidgetParent, IDraggable { + + private final Widget upButton; + private final Widget downButton; + private final Widget removeButton; + private Widget content; + private final List allChildren = new ArrayList<>(); + private int currentIndex; + private SortableListWidget listWidget; + private final T value; + private boolean moving; + private Pos2d relativeClickPos; + private boolean active = true; + + public SortableListItem(T value) { + this.value = value; + this.upButton = new ButtonWidget() + .setOnClick((clickData, widget) -> listWidget.moveElementUp(this.currentIndex)) + .setBackground(ModularUITextures.BASE_BUTTON, ModularUITextures.ARROW_UP) + .setSize(10, 10); + this.downButton = new ButtonWidget() + .setOnClick((clickData, widget) -> listWidget.moveElementDown(this.currentIndex)) + .setBackground(ModularUITextures.BASE_BUTTON, ModularUITextures.ARROW_DOWN) + .setSize(10, 10); + this.removeButton = new ButtonWidget() + .setOnClick((clickData, widget) -> listWidget.removeElement(this.currentIndex)) + .setBackground(ModularUITextures.BASE_BUTTON, ModularUITextures.CROSS) + .setSize(10, 20); + } + + protected void init(SortableListWidget listWidget) { + this.listWidget = listWidget; + } + + public void setCurrentIndex(int currentIndex) { + this.currentIndex = currentIndex; + } + + public int getCurrentIndex() { + return currentIndex; + } + + @Override + public void initChildren() { + this.content = this.listWidget.getWidgetCreator().apply(this.value); + makeChildrenList(); + } + + @Override + public void onRebuild() { + setEnabled(relativeClickPos == null); + } + + @Override + public Widget setEnabled(boolean enabled) { + enabled = this.active; + return super.setEnabled(enabled); + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + int w = content.getSize().width + (listWidget.areElementsRemovable() ? 20 : 10); + int h = Math.max(content.getSize().height, 20); + Size size = new Size(w, h); + + // need to layout children after sizing because size may be needed for positioning + if (content.getSize().height >= 20) { + this.content.setPosSilent(Pos2d.ZERO); + } else { + this.content.setPosSilent(new Pos2d(0, size.height / 2 - content.getSize().height / 2)); + } + this.upButton.setPosSilent(new Pos2d(content.getSize().width, 0)); + this.downButton.setPosSilent(new Pos2d(content.getSize().width, size.height - 10)); + this.removeButton.setSize(10, size.height); + this.removeButton.setPosSilent(new Pos2d(size.width - 10, 0)); + + return size; + } + + private void makeChildrenList() { + this.allChildren.clear(); + this.allChildren.add(content); + this.allChildren.add(upButton); + this.allChildren.add(downButton); + if (listWidget.areElementsRemovable()) { + this.allChildren.add(removeButton); + } + } + + @Override + public boolean canHover() { + return true; + } + + @Override + public void renderMovingState(float partialTicks) { + Cursor cursor = getContext().getCursor(); + GlStateManager.pushMatrix(); + GlStateManager.translate(-getAbsolutePos().x, -getAbsolutePos().y, 0); + GlStateManager.translate(cursor.getX() - relativeClickPos.x, cursor.getY() - relativeClickPos.y, 0); + drawInternal(partialTicks, true); + GlStateManager.popMatrix(); + } + + @Override + public boolean onDragStart(int button) { + setActive(false); + setEnabled(false); + relativeClickPos = getContext().getMousePos().subtract(getAbsolutePos()); + return true; + } + + @Override + public void onDragEnd(boolean successful) { + setActive(true); + setEnabled(true); + checkNeedsRebuild(); + relativeClickPos = null; + } + + @Override + public boolean canDropHere(@Nullable Widget widget, boolean isInBounds) { + if (widget != null && widget.getParent() instanceof SortableListItem) { + SortableListItem listItem = (SortableListItem) widget.getParent(); + return value.getClass().isAssignableFrom(listItem.value.getClass()) && currentIndex != listItem.currentIndex; + } + return false; + } + + @Override + public void onDrag(int mouseButton, long timeSinceLastClick) { + SortableListItem listItem = findSortableListItem(); + if (listItem != null) { + listWidget.putAtIndex(currentIndex, listItem.currentIndex); + } + } + + @Nullable + private SortableListItem findSortableListItem() { + if (!listWidget.isUnderMouse(getContext().getMousePos())) return null; + for (Object hovered : getContext().getCursor().getAllHovered()) { + if (hovered instanceof ModularWindow && hovered != getWindow()) return null; + if (hovered instanceof SortableListItem) { + if (((SortableListItem) hovered).listWidget == listWidget) { + return (SortableListItem) hovered; + } + } + } + return null; + } + + @Override + public boolean isMoving() { + return moving; + } + + @Override + public void setMoving(boolean moving) { + this.moving = moving; + } + + public T getValue() { + return value; + } + + @Override + public List getChildren() { + return this.allChildren; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/SortableListWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/SortableListWidget.java new file mode 100644 index 0000000..db168d8 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/SortableListWidget.java @@ -0,0 +1,165 @@ +package com.gtnewhorizons.modularui.common.widget; + + +import com.gtnewhorizons.modularui.ModularUI; +import com.gtnewhorizons.modularui.api.widget.Widget; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class SortableListWidget extends ListWidget { + + private final Map> widgetMap = new HashMap<>(); + private final List startValues; + private Function widgetCreator = t -> new TextWidget(t.toString()); + private Consumer> saveFunction = list -> { + }; + private Consumer onRemoveElement = t -> { + }; + private boolean elementsRemovable = false; + + public static SortableListWidget removable(Collection allValues, List startValues) { + return new SortableListWidget<>(true, allValues, startValues); + } + + private SortableListWidget(boolean removable, Collection allValues, List startValues) { + this.elementsRemovable = removable; + for (T t : allValues) { + widgetMap.put(t, null); + } + this.startValues = new ArrayList<>(startValues); + } + + public SortableListWidget(List startValues) { + this(false, startValues, startValues); + } + + @Override + public void initChildren() { + super.initChildren(); + this.children.clear(); + int i = 0; + for (T t : widgetMap.keySet()) { + SortableListItem listItem = new SortableListItem<>(t); + listItem.init(this); + listItem.setCurrentIndex(i++); + this.widgetMap.put(t, listItem); + this.children.add(listItem); + } + } + + @Override + public void onFirstRebuild() { + this.children.clear(); + for (T t : startValues) { + SortableListItem listItem = widgetMap.get(t); + if (listItem == null) { + ModularUI.logger.error("Unexpected error: Could not find sortable list item for {}!", t); + continue; + } + this.children.add(listItem); + } + assignIndexes(); + layoutChildren(0, 0); + onRebuild(); + } + + @Override + public void onPause() { + this.saveFunction.accept(createElements()); + } + + public List createElements() { + return this.children.stream().map(widget -> ((SortableListItem) widget).getValue()).collect(Collectors.toList()); + } + + protected void removeElement(int index) { + SortableListItem item = (SortableListItem) children.remove(index); + onRemoveElement.accept(item.getValue()); + assignIndexes(); + layoutChildren(0, 0); + onRebuild(); + } + + protected void moveElementUp(int index) { + if (index > 0) { + Widget widget = children.remove(index); + children.add(index - 1, widget); + assignIndexes(); + layoutChildren(0, 0); + onRebuild(); + } + } + + protected void moveElementDown(int index) { + if (index < children.size() - 1) { + Widget widget = children.remove(index); + children.add(index + 1, widget); + assignIndexes(); + layoutChildren(0, 0); + onRebuild(); + } + } + + protected void putAtIndex(int index, int toIndex) { + Widget widget = children.remove(index); + children.add(toIndex, widget); + assignIndexes(); + checkNeedsRebuild(); + } + + protected void assignIndexes() { + for (int i = 0; i < children.size(); i++) { + ((SortableListItem) children.get(i)).setCurrentIndex(i); + } + } + + public Function getWidgetCreator() { + return widgetCreator; + } + + public Consumer getOnRemoveElement() { + return onRemoveElement; + } + + public boolean areElementsRemovable() { + return elementsRemovable; + } + + public void addElement(T t) { + if (!isInitialised()) { + throw new IllegalStateException("List needs to be initialised to add elements dynamically."); + } + if (!widgetMap.containsKey(t)) { + throw new NoSuchElementException("This list widget was not initialised with the value " + t); + } + SortableListItem listItem = widgetMap.get(t); + listItem.setActive(true); + listItem.setEnabled(true); + this.children.add(listItem); + assignIndexes(); + checkNeedsRebuild(); + } + + public SortableListWidget setWidgetCreator(Function widgetCreator) { + this.widgetCreator = widgetCreator; + return this; + } + + public SortableListWidget setSaveFunction(Consumer> saveFunction) { + this.saveFunction = saveFunction; + return this; + } + + public SortableListWidget setElementsRemovable(boolean elementsRemovable) { + this.elementsRemovable = elementsRemovable; + return this; + } + + public SortableListWidget setOnRemoveElement(Consumer onRemoveElement) { + this.onRemoveElement = onRemoveElement; + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/SyncedWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/SyncedWidget.java new file mode 100644 index 0000000..b6c0a29 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/SyncedWidget.java @@ -0,0 +1,50 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.widget.ISyncedWidget; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.common.internal.JsonHelper; +import com.google.gson.JsonObject; + +/** + * An optional base class for synced widgets. + */ +public abstract class SyncedWidget extends Widget implements ISyncedWidget { + + private boolean syncsToServer = true; + private boolean syncsToClient = true; + + @Override + public void readJson(JsonObject json, String type) { + super.readJson(json, type); + this.syncsToServer = JsonHelper.getBoolean(json, true, "syncToClient", "handlesServer"); + this.syncsToClient = JsonHelper.getBoolean(json, true, "syncToServer", "handlesClient"); + } + + /** + * @return if this widget should operate on the sever side. + * For example detecting and sending changes to client. + */ + public boolean syncsToClient() { + return syncsToClient; + } + + /** + * @return if this widget should operate on the client side. + * For example, sending a changed value to the server. + */ + public boolean syncsToServer() { + return syncsToServer; + } + + /** + * Determines how this widget should sync values + * + * @param syncsToClient if this widget should sync changes to the server + * @param syncsToServer if this widget should detect changes on server and sync them to client + */ + public SyncedWidget setSynced(boolean syncsToClient, boolean syncsToServer) { + this.syncsToClient = syncsToClient; + this.syncsToServer = syncsToServer; + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/TabButton.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/TabButton.java new file mode 100644 index 0000000..cd7f0a6 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/TabButton.java @@ -0,0 +1,62 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import com.gtnewhorizons.modularui.api.widget.Widget; + +import javax.annotation.Nullable; + +/** + * Tab-styled button widget. Can be switched between other tab buttons by {@link TabContainer}. + */ +public class TabButton extends Widget implements Interactable { + + private final int page; + private TabContainer tabController; + private IDrawable[] activeBackground; + + /** + * @param page Should be unique within {@link TabContainer} + */ + public TabButton(int page) { + this.page = page; + } + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + if (page != tabController.getCurrentPage()) { + tabController.setActivePage(page); + } + return ClickResult.ACCEPT; + } + + protected void setTabController(TabContainer tabController) { + this.tabController = tabController; + } + + public int getPage() { + return page; + } + + @Nullable + @Override + public IDrawable[] getBackground() { + if (isSelected() && activeBackground != null) { + return activeBackground; + } + return super.getBackground(); + } + + public boolean isSelected() { + return page == tabController.getCurrentPage(); + } + + public TabButton setBackground(boolean active, IDrawable... drawables) { + if (active) { + this.activeBackground = drawables; + } else { + setBackground(drawables); + } + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/TabContainer.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/TabContainer.java new file mode 100644 index 0000000..d48dbb0 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/TabContainer.java @@ -0,0 +1,68 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.Widget; + +import java.util.ArrayList; +import java.util.List; + +/** + * Widget holding a list of {@link TabButton}. + * Add tab buttons with {@link #addTabButton(Widget)} and add pages + * with {@link #addPage(Widget)} in corresponding order. + * At least one child is required. + * Number of {@link TabButton}s and pages should match. + */ +public class TabContainer extends PageControlWidget { + + private final List tabButtons = new ArrayList<>(); + private final List allChildren = new ArrayList<>(); + private Size buttonSize = null; + + + @Override + public void initChildren() { + allChildren.clear(); + allChildren.addAll(getPages()); + allChildren.addAll(tabButtons); + } + + @Override + public void onInit() { + super.onInit(); + for (TabButton tabButton : tabButtons) { + tabButton.setTabController(this); + if (tabButton.getPage() < 0 || tabButton.getPage() >= getPages().size()) { + throw new IndexOutOfBoundsException(String.format("TabButton page is %s, but must be 0 - %s", tabButton.getPage(), getPages().size() - 1)); + } + if (buttonSize != null && tabButton.isAutoSized()) { + tabButton.setSize(buttonSize); + } + } + } + + @Override + public List getChildren() { + return allChildren; + } + + /** + * @param tabButton Must extend {@link TabButton} + */ + public TabContainer addTabButton(Widget tabButton) { + if (!(tabButton instanceof TabButton)) { + throw new IllegalArgumentException("Tab button must be instance of TabButton"); + } + this.tabButtons.add((TabButton) tabButton); + return this; + } + + public TabContainer setButtonSize(Size buttonSize) { + this.buttonSize = buttonSize; + return this; + } + + public TabContainer setButtonSize(int width, int height) { + return setButtonSize(new Size(width, height)); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/TextWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/TextWidget.java new file mode 100644 index 0000000..b89c886 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/TextWidget.java @@ -0,0 +1,131 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.api.drawable.Text; +import com.gtnewhorizons.modularui.api.drawable.TextRenderer; +import com.gtnewhorizons.modularui.api.math.Alignment; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.common.internal.Theme; +import com.google.gson.JsonObject; +import net.minecraft.util.EnumChatFormatting; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Supplier; + +/** + * Draws text. Accepts some text formatting rules. + * See also {@link Text} + */ +public class TextWidget extends Widget { + + private final Text text; + protected String localised; + private int maxWidth = -1; + private Alignment textAlignment = Alignment.Center; + private final TextRenderer textRenderer = new TextRenderer(); + + public TextWidget() { + this(new Text("")); + } + + public TextWidget(Text text) { + this.text = text; + } + + public TextWidget(String text) { + this(new Text(text)); + } + + /*public TextWidget(ITextComponent text) { + this(new TextSpan().addText(text)); + }*/ + + public static DynamicTextWidget dynamicText(Supplier supplier) { + return new DynamicTextWidget(supplier); + } + + public static DynamicTextWidget dynamicString(Supplier supplier) { + return new DynamicTextWidget(() -> new Text(supplier.get())); + } + + /*public static DynamicTextWidget dynamicTextComponent(Supplier supplier) { + return new DynamicTextWidget(() -> new TextSpan().addText(supplier.get())); + }*/ + + @Override + public void readJson(JsonObject json, String type) { + super.readJson(json, type); + //getText().readJson(json); + } + + @Override + public void onRebuild() { + if (localised == null) { + this.localised = getText().getFormatted(); + } + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + this.localised = getText().getFormatted(); + int width = this.maxWidth > 0 ? this.maxWidth : maxWidth - getPos().x; + textRenderer.setSimulate(true); + textRenderer.setAlignment(textAlignment, width, maxHeight); + textRenderer.draw(localised); + textRenderer.setSimulate(false); + return textRenderer.getLastSize().grow(1, 1); + } + + @Override + public void onScreenUpdate() { + if (isAutoSized()) { + String l = getText().getFormatted(); + if (!l.equals(localised)) { + checkNeedsRebuild(); + localised = l; + } + } + } + + @Override + public void draw(float partialTicks) { + Text text = getText(); + if (localised == null) { + localised = text.getFormatted(); + } + textRenderer.setPos(0, 0); + textRenderer.setShadow(text.hasShadow()); + textRenderer.setAlignment(textAlignment, size.width, size.height); + textRenderer.setColor(text.hasColor() ? text.getColor() : Theme.INSTANCE.getText()); + textRenderer.draw(localised); + } + + public Text getText() { + return text; + } + + public TextWidget setDefaultColor(int color) { + this.text.color(color); + return this; + } + + public TextWidget setDefaultColor(EnumChatFormatting color) { + this.text.format(color); + return this; + } + + public TextWidget setMaxWidth(int maxWidth) { + this.maxWidth = maxWidth; + return this; + } + + public TextWidget setTextAlignment(Alignment textAlignment) { + this.textAlignment = textAlignment; + return this; + } + + public TextWidget setScale(float scale) { + this.textRenderer.setScale(scale); + return this; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/WidgetJsonRegistry.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/WidgetJsonRegistry.java new file mode 100644 index 0000000..4b6b430 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/WidgetJsonRegistry.java @@ -0,0 +1,49 @@ +package com.gtnewhorizons.modularui.common.widget; + +import com.gtnewhorizons.modularui.ModularUI; + +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.drawable.Text; +import com.gtnewhorizons.modularui.api.drawable.UITexture; +import com.gtnewhorizons.modularui.api.widget.Widget; +import net.minecraft.entity.player.EntityPlayer; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class WidgetJsonRegistry { + + public static void init() { + registerWidget("text", TextWidget::new); + registerWidget("image", DrawableWidget::new); + registerWidget("cycle_button", CycleButtonWidget::new); + registerWidget("button", ButtonWidget::new); + registerWidgetSpecial("player_inventory", SlotGroup::playerInventoryGroup); + + IDrawable.JSON_DRAWABLE_MAP.put("text", Text::ofJson); + IDrawable.JSON_DRAWABLE_MAP.put("image", UITexture::ofJson); + } + + private static final Map REGISTRY = new HashMap<>(); + + public static void registerWidgetSpecial(String id, WidgetFactory factory) { + ModularUI.logger.info("Register type {}", id); + REGISTRY.put(id, factory); + } + + public static void registerWidget(String id, Supplier factory) { + ModularUI.logger.info("Register type {}", id); + REGISTRY.put(id, player -> factory.get()); + } + + @Nullable + public static WidgetFactory getFactory(String id) { + return REGISTRY.get(id); + } + + public interface WidgetFactory { + Widget create(EntityPlayer player); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/BaseTextFieldWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/BaseTextFieldWidget.java new file mode 100644 index 0000000..2cb8508 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/BaseTextFieldWidget.java @@ -0,0 +1,323 @@ +package com.gtnewhorizons.modularui.common.widget.textfield; + +import com.gtnewhorizons.modularui.api.widget.scroll.ScrollType; +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.drawable.GuiHelper; +import com.gtnewhorizons.modularui.api.math.Alignment; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.widget.IWidgetParent; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.api.widget.scroll.IHorizontalScrollable; +import com.gtnewhorizons.modularui.common.widget.ScrollBar; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.Util; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.input.Keyboard; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +/** + * The base of a text input widget. Handles mouse/keyboard input and rendering. + */ +public class BaseTextFieldWidget extends Widget implements IWidgetParent, Interactable, IHorizontalScrollable { + + public static final DecimalFormat format = (DecimalFormat) NumberFormat.getInstance(); + + /** + * all positive whole numbers + */ + public static final Pattern NATURAL_NUMS = Pattern.compile("[0-9]*([+\\-*/%^][0-9]*)*"); + /** + * all positive and negative numbers + */ + public static final Pattern WHOLE_NUMS = Pattern.compile("-?[0-9]*([+\\-*/%^][0-9]*)*"); + public static final Pattern DECIMALS = Pattern.compile("[0-9]*(" + getDecimalSeparator() + "[0-9]*)?([+\\-*/%^][0-9]*(" + getDecimalSeparator() + "[0-9]*)?)*"); + /** + * alphabets + */ + public static final Pattern LETTERS = Pattern.compile("[a-zA-Z]*"); + public static final Pattern ANY = Pattern.compile(".*"); + /** + * ascii letters + */ + private static final Pattern BASE_PATTERN = Pattern.compile("[A-Za-z0-9\\s_+\\-.,!@#$%^&*();\\\\/|<>\"'\\[\\]?=]"); + + protected TextFieldHandler handler = new TextFieldHandler(); + protected TextFieldRenderer renderer = new TextFieldRenderer(handler); + protected Alignment textAlignment = Alignment.TopLeft; + protected int scrollOffset = 0; + protected float scale = 1f; + private int cursorTimer; + + protected ScrollBar scrollBar; + + public BaseTextFieldWidget() { + this.handler.setRenderer(renderer); + } + + @Override + public List getChildren() { + return scrollBar == null ? Collections.emptyList() : Collections.singletonList(scrollBar); + } + + @Override + public void onScreenUpdate() { + if (isFocused() && ++cursorTimer == 10) { + renderer.toggleCursor(); + cursorTimer = 0; + } + } + + @Override + public void draw(float partialTicks) { + GuiHelper.useScissor(pos.x, pos.y, size.width, size.height, () -> { + GlStateManager.pushMatrix(); + GlStateManager.translate(1 - scrollOffset, 1, 0); + renderer.setSimulate(false); + renderer.setScale(scale); + renderer.setAlignment(textAlignment, -2, size.height); + renderer.draw(handler.getText()); + GlStateManager.popMatrix(); + }); + } + + @Override + public boolean shouldGetFocus() { + this.cursorTimer = 0; + this.renderer.setCursor(true); + return true; + } + + @Override + public boolean canHover() { + return true; + } + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + if (!isRightBelowMouse()) { + return ClickResult.IGNORE; + } + handler.setCursor(renderer.getCursorPos(handler.getText(), getContext().getCursor().getX() - pos.x + scrollOffset, getContext().getCursor().getY() - pos.y)); + return ClickResult.SUCCESS; + } + + @Override + public void onMouseDragged(int buttonId, long deltaTime) { + handler.setMainCursor(renderer.getCursorPos(handler.getText(), getContext().getCursor().getX() - pos.x + scrollOffset, getContext().getCursor().getY() - pos.y)); + } + + @Override + public boolean onMouseScroll(int direction) { + return scrollBar != null && this.scrollBar.onMouseScroll(direction); + } + + @Override + public boolean onKeyPressed(char character, int keyCode) { + switch (keyCode) { + case Keyboard.KEY_RETURN: + if (getMaxLines() > 1) { + handler.newLine(); + } else { + removeFocus(); + } + return true; + case Keyboard.KEY_ESCAPE: + removeFocus(); + return true; + case Keyboard.KEY_LEFT: { + handler.moveCursorLeft(Interactable.hasControlDown(), Interactable.hasShiftDown()); + return true; + } + case Keyboard.KEY_RIGHT: { + handler.moveCursorRight(Interactable.hasControlDown(), Interactable.hasShiftDown()); + return true; + } + case Keyboard.KEY_UP: { + handler.moveCursorUp(Interactable.hasControlDown(), Interactable.hasShiftDown()); + return true; + } + case Keyboard.KEY_DOWN: { + handler.moveCursorDown(Interactable.hasControlDown(), Interactable.hasShiftDown()); + return true; + } + case Keyboard.KEY_DELETE: + handler.delete(true); + return true; + case Keyboard.KEY_BACK: + handler.delete(); + return true; + } + + if (character == Character.MIN_VALUE) { + return false; + } + + if (isKeyComboCtrlC(keyCode)) { + // copy marked text + GuiScreen.setClipboardString(handler.getSelectedText()); + return true; + } else if (isKeyComboCtrlV(keyCode)) { + // paste copied text in marked text + handler.insert(GuiScreen.getClipboardString()); + return true; + } else if (isKeyComboCtrlX(keyCode) && handler.hasTextMarked()) { + // copy and delete copied text + GuiScreen.setClipboardString(handler.getSelectedText()); + handler.delete(); + return true; + } else if (isKeyComboCtrlA(keyCode)) { + // mark whole text + handler.markAll(); + return true; + } else if (BASE_PATTERN.matcher(String.valueOf(character)).matches()) { + // insert typed char + handler.insert(String.valueOf(character)); + return true; + } + return false; + } + + @Override + public void onRemoveFocus() { + super.onRemoveFocus(); + this.renderer.setCursor(false); + this.cursorTimer = 0; + this.scrollOffset = 0; + } + + @Override + protected @NotNull Size determineSize(int maxWidth, int maxHeight) { + return new Size(maxWidth, (int) (renderer.getFontHeight() * getMaxLines() + 0.5)); + } + + @Override + public void setHorizontalScrollOffset(int offset) { + if (this.scrollBar != null && this.scrollBar.isActive()) { + this.scrollOffset = offset; + } else { + this.scrollOffset = 0; + } + } + + @Override + public int getHorizontalScrollOffset() { + return this.scrollOffset; + } + + @Override + public int getVisibleWidth() { + return size.width; + } + + @Override + public int getActualWidth() { + return (int) Math.ceil(renderer.getLastWidth()); + } + + public int getMaxLines() { + return handler.getMaxLines(); + } + + public BaseTextFieldWidget setTextAlignment(Alignment textAlignment) { + this.textAlignment = textAlignment; + return this; + } + + public BaseTextFieldWidget setScale(float scale) { + this.scale = scale; + return this; + } + + public BaseTextFieldWidget setScrollBar() { + return setScrollBar(0); + } + + public BaseTextFieldWidget setScrollBar(int posOffset) { + return setScrollBar(ScrollBar.defaultTextScrollBar().setPosOffset(posOffset)); + } + + public BaseTextFieldWidget setScrollBar(@Nullable ScrollBar scrollBar) { + this.scrollBar = scrollBar; + this.handler.setScrollBar(scrollBar); + if (this.scrollBar != null) { + this.scrollBar.setScrollType(ScrollType.HORIZONTAL, this, null); + } + return this; + } + + public BaseTextFieldWidget setTextColor(int color) { + this.renderer.setColor(color); + return this; + } + + public static char getDecimalSeparator() { + return format.getDecimalFormatSymbols().getDecimalSeparator(); + } + + public static char getGroupSeparator() { + return format.getDecimalFormatSymbols().getGroupingSeparator(); + } + + // === from GuiScreen 1.12 === + + public static final boolean IS_RUNNING_ON_MAC = Util.getOSType() == Util.EnumOS.OSX; + + /** + * Returns true if either windows ctrl key is down or if either mac meta key is down + */ + public static boolean isCtrlKeyDown() + + { + if (IS_RUNNING_ON_MAC) + { + return Keyboard.isKeyDown(219) || Keyboard.isKeyDown(220); + } + else + { + return Keyboard.isKeyDown(29) || Keyboard.isKeyDown(157); + } + } + + /** + * Returns true if either shift key is down + */ + public static boolean isShiftKeyDown() + { + return Keyboard.isKeyDown(42) || Keyboard.isKeyDown(54); + } + + /** + * Returns true if either alt key is down + */ + public static boolean isAltKeyDown() + { + return Keyboard.isKeyDown(56) || Keyboard.isKeyDown(184); + } + + public static boolean isKeyComboCtrlX(int keyID) + { + return keyID == 45 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } + + public static boolean isKeyComboCtrlV(int keyID) + { + return keyID == 47 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } + + public static boolean isKeyComboCtrlC(int keyID) + { + return keyID == 46 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } + + public static boolean isKeyComboCtrlA(int keyID) + { + return keyID == 30 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextEditorWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextEditorWidget.java new file mode 100644 index 0000000..ea2c87e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextEditorWidget.java @@ -0,0 +1,11 @@ +package com.gtnewhorizons.modularui.common.widget.textfield; + +/** + * A non syncable, multiline text input widget. Meant for client only screens to edit large amounts of text. + */ +public class TextEditorWidget extends BaseTextFieldWidget { + + public TextEditorWidget() { + this.handler.setMaxLines(10000); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextFieldHandler.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextFieldHandler.java new file mode 100644 index 0000000..a08b1fc --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextFieldHandler.java @@ -0,0 +1,370 @@ +package com.gtnewhorizons.modularui.common.widget.textfield; + +import com.google.common.base.Joiner; +import com.gtnewhorizons.modularui.common.widget.ScrollBar; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Handles the text itself like inserting and deleting text. Also handles the cursor and marking text. + */ +public class TextFieldHandler { + + private static final Joiner JOINER = Joiner.on('\n'); + + private final List text = new ArrayList<>(); + private final Point cursor = new Point(), cursorEnd = new Point(); + private TextFieldRenderer renderer; + @Nullable + private ScrollBar scrollBar; + private boolean mainCursorStart = true; + private int maxLines = 1; + @Nullable + private Pattern pattern; + private int maxCharacters = -1; + + public void setPattern(@Nullable Pattern pattern) { + this.pattern = pattern; + } + + public void setMaxCharacters(int maxCharacters) { + this.maxCharacters = maxCharacters; + } + + public void setScrollBar(@Nullable ScrollBar scrollBar) { + this.scrollBar = scrollBar; + } + + public void setRenderer(TextFieldRenderer renderer) { + this.renderer = renderer; + } + + public void switchCursors() { + this.mainCursorStart = !this.mainCursorStart; + } + + public Point getMainCursor() { + return mainCursorStart ? cursor : cursorEnd; + } + + public Point getOffsetCursor() { + return mainCursorStart ? cursorEnd : cursor; + } + + public Point getStartCursor() { + if (!hasTextMarked()) { + return cursor; + } + return cursor.y > cursorEnd.y || (cursor.y == cursorEnd.y && cursor.x > cursorEnd.x) ? cursorEnd : cursor; + } + + public Point getEndCursor() { + if (!hasTextMarked()) { + return cursor; + } + return cursor.y > cursorEnd.y || (cursor.y == cursorEnd.y && cursor.x > cursorEnd.x) ? cursor : cursorEnd; + } + + public boolean hasTextMarked() { + return cursor.y != cursorEnd.y || cursor.x != cursorEnd.x; + } + + public void setOffsetCursor(int linePos, int charPos) { + getOffsetCursor().setLocation(charPos, linePos); + } + + public void setMainCursor(int linePos, int charPos) { + Point main = getMainCursor(); + if (main.x != charPos || main.y != linePos) { + main.setLocation(charPos, linePos); + if (!this.text.isEmpty() && this.renderer != null && this.scrollBar != null && this.scrollBar.isActive()) { + // update actual width + this.renderer.setSimulate(true); + this.renderer.draw(this.text); + this.renderer.setSimulate(false); + String line = this.text.get(main.y); + this.scrollBar.setScrollOffsetOfCursor(this.renderer.getPosOf(this.renderer.measureLines(Collections.singletonList(line)), main).x); + } + } + } + + public void setCursor(int linePos, int charPos) { + setCursor(linePos, charPos, true); + } + + public void setCursor(int linePos, int charPos, boolean applyToOffset) { + setMainCursor(linePos, charPos); + if (applyToOffset) { + setOffsetCursor(linePos, charPos); + } + } + + public void setOffsetCursor(Point cursor) { + setOffsetCursor(cursor.y, cursor.x); + } + + public void setMainCursor(Point cursor) { + setMainCursor(cursor.y, cursor.x); + } + + public void setCursor(Point cursor) { + setMainCursor(cursor); + setOffsetCursor(cursor); + } + + public void putMainCursorAtStart() { + if (hasTextMarked() && getMainCursor() != getStartCursor()) { + switchCursors(); + } + } + + public void putMainCursorAtEnd() { + if (hasTextMarked() && getMainCursor() != getEndCursor()) { + switchCursors(); + } + } + + public void moveCursorLeft(boolean ctrl, boolean shift) { + if (this.text.isEmpty()) return; + Point main = getMainCursor(); + if (main.x == 0) { + if (main.y == 0) return; + setCursor(main.y - 1, this.text.get(main.y - 1).length(), !shift); + } else { + int newPos = main.x - 1; + if (ctrl) { + String line = this.text.get(main.y); + boolean found = false; + for (int i = main.x - 1; i >= 0; i--) { + char c = line.charAt(i); + if (!Character.isLetter(c) && !Character.isDigit(c)) { + newPos = i + 1; + found = true; + break; + } + } + if (!found) { + newPos = 0; + } + } + setCursor(main.y, newPos, !shift); + } + } + + public void moveCursorRight(boolean ctrl, boolean shift) { + if (this.text.isEmpty()) return; + Point main = getMainCursor(); + String line = this.text.get(main.y); + if (main.x == line.length()) { + if (main.y == this.text.size() - 1) return; + setCursor(main.y + 1, 0, !shift); + } else { + int newPos = main.x + 1; + if (ctrl) { + boolean found = false; + for (int i = main.x + 1; i < line.length(); i++) { + char c = line.charAt(i); + if (!Character.isLetter(c) && !Character.isDigit(c)) { + newPos = i; + found = true; + break; + } + } + if (!found) { + newPos = line.length(); + } + } + setCursor(main.y, newPos, !shift); + } + } + + public void moveCursorUp(boolean ctrl, boolean shift) { + if (this.text.isEmpty()) return; + Point main = getMainCursor(); + if (main.y > 0) { + setCursor(main.y - 1, main.x, !shift); + } else { + setCursor(main.y, 0, !shift); + } + } + + public void moveCursorDown(boolean ctrl, boolean shift) { + if (this.text.isEmpty()) return; + Point main = getMainCursor(); + if (main.y < this.text.size() - 1) { + setCursor(main.y + 1, main.x, !shift); + } else { + setCursor(main.y, this.text.get(main.y).length(), !shift); + } + } + + public void markAll() { + setOffsetCursor(0, 0); + setMainCursor(this.text.size() - 1, this.text.get(this.text.size() - 1).length()); + } + + public String getTextAsString() { + return JOINER.join(this.text); + } + + public List getText() { + return this.text; + } + + public void onChanged() { + } + + public String getSelectedText() { + if (!hasTextMarked()) return ""; + Point min = getStartCursor(); + Point max = getEndCursor(); + if (min.y == max.y) { + return this.text.get(min.y).substring(min.x, max.x); + } + StringBuilder builder = new StringBuilder(); + builder.append(this.text.get(min.y).substring(min.x)); + if (max.y > min.y + 2) { + for (int i = min.y + 1; i < max.y - 1; i++) { + builder.append(this.text.get(i)); + } + } + builder.append(this.text.get(max.y), 0, max.x); + return builder.toString(); + } + + public boolean test(String text) { + return maxLines > 1 || ((pattern == null || pattern.matcher(text).matches()) && (maxCharacters < 0 || maxCharacters >= text.length())); + } + + public void insert(String text) { + insert(Arrays.asList(text.split("\n"))); + } + + public void insert(List text) { + List copy = new ArrayList<>(this.text); + Point point = insert(copy, text); + if (point == null || copy.size() > maxLines || !renderer.wouldFit(copy)) return; + this.text.clear(); + this.text.addAll(copy); + setCursor(point); + onChanged(); + } + + private Point insert(List text, List insertion) { + if (insertion.isEmpty() || (insertion.size() > 1 && text.size() + insertion.size() - 1 > this.maxLines)) { + return null; + } + int x = cursor.x, y = cursor.y; + if (hasTextMarked()) { + delete(false); + } + if (text.isEmpty()) { + if (insertion.size() == 1 && !test(insertion.get(0))) { + return null; + } + text.addAll(insertion); + return new Point(text.get(text.size() - 1).length(), text.size() - 1); + } + String lineStart = text.get(cursor.y).substring(0, cursor.x); + String lineEnd = text.get(cursor.y).substring(cursor.x); + if (insertion.size() == 1 && text.size() == 1 && !test(lineStart + insertion.get(0) + lineEnd)) { + return null; + } + text.set(cursor.y, lineStart + insertion.get(0)); + if (insertion.size() == 1) { + if (!test(insertion.get(0))) { + return null; + } + text.set(cursor.y, text.get(cursor.y) + lineEnd); + return new Point(cursor.x + insertion.get(0).length(), cursor.y); + } else { + if (insertion.size() > 1) { + text.add(cursor.y + 1, insertion.get(insertion.size() - 1) + lineEnd); + x = insertion.get(insertion.size() - 1).length(); + y += 1; + } + if (insertion.size() > 2) { + text.addAll(cursor.y + 1, text.subList(1, insertion.size() - 1)); + x = insertion.get(insertion.size() - 1).length(); + y += insertion.size() - 1; + } + return new Point(x, y); + } + } + + public void newLine() { + if (hasTextMarked()) { + delete(false); + } + String line = this.text.get(cursor.y); + this.text.set(cursor.y, line.substring(0, cursor.x)); + this.text.add(cursor.y + 1, line.substring(cursor.x)); + setCursor(cursor.y + 1, 0); + } + + public void delete() { + delete(false); + } + + public void delete(boolean inFront) { + if (hasTextMarked()) { + Point min = getStartCursor(); + Point max = getEndCursor(); + String minLine = this.text.get(min.y); + if (min.y == max.y) { + this.text.set(min.y, minLine.substring(0, min.x) + minLine.substring(max.x)); + } else { + String maxLine = this.text.get(Math.min(this.text.size() - 1, max.y)); + this.text.set(min.y, minLine.substring(0, min.x) + maxLine.substring(max.x)); + if (max.y > min.y + 1) { + this.text.subList(min.y + 1, max.y + 1).clear(); + } + } + setCursor(min.y, min.x); + } else { + String line = this.text.get(cursor.y); + if (inFront) { + if (cursor.x == line.length()) { + if (this.text.size() > cursor.y + 1) { + this.text.set(cursor.y, line + this.text.get(cursor.y + 1)); + this.text.remove(cursor.y + 1); + } + } else { + line = line.substring(0, cursor.x) + line.substring(cursor.x + 1); + this.text.set(cursor.y, line); + } + } else { + if (cursor.x == 0) { + if (cursor.y > 0) { + String lineAbove = this.text.get(cursor.y - 1); + this.text.set(cursor.y - 1, lineAbove + line); + this.text.remove(cursor.y); + setCursor(cursor.y - 1, lineAbove.length()); + } + } else { + line = line.substring(0, cursor.x - 1) + line.substring(cursor.x); + this.text.set(cursor.y, line); + setCursor(cursor.y, cursor.x - 1); + } + } + } + if (this.scrollBar != null) { + scrollBar.clampScrollOffset(); + } + onChanged(); + } + + public void setMaxLines(int maxLines) { + this.maxLines = Math.max(1, maxLines); + } + + public int getMaxLines() { + return maxLines; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextFieldRenderer.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextFieldRenderer.java new file mode 100644 index 0000000..4af30b2 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextFieldRenderer.java @@ -0,0 +1,174 @@ +package com.gtnewhorizons.modularui.common.widget.textfield; + +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.drawable.TextRenderer; +import com.gtnewhorizons.modularui.api.math.Color; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.renderer.Tessellator; +import org.apache.commons.lang3.tuple.Pair; + +import java.awt.*; +import java.awt.geom.Point2D; +import java.util.Collections; +import java.util.List; + +public class TextFieldRenderer extends TextRenderer { + + protected final TextFieldHandler handler; + protected int markedColor = 0x2F72A8; + protected boolean renderCursor = false; + + public TextFieldRenderer(TextFieldHandler handler) { + this.handler = handler; + } + + public void toggleCursor() { + this.renderCursor = !this.renderCursor; + } + + public void setCursor(boolean active) { + this.renderCursor = active; + } + + public void setMarkedColor(int markedColor) { + this.markedColor = markedColor; + } + + @Override + protected void drawMeasuredLines(List> measuredLines) { + drawCursors(measuredLines); + super.drawMeasuredLines(measuredLines); + } + + @Override + public List wrapLine(String line) { + return Collections.singletonList(line); + } + + protected void drawCursors(List> measuredLines) { + if (!simulate) { + Point2D.Float start; + if (handler.hasTextMarked()) { + start = getPosOf(measuredLines, handler.getStartCursor()); + // render Marked + Point2D.Float end = getPosOf(measuredLines, handler.getEndCursor()); + + if (start.y == end.y) { + drawMarked(start.y, start.x, end.x); + } else { + int min = handler.getStartCursor().y; + int max = handler.getEndCursor().y; + Pair line = measuredLines.get(min); + int startX = getStartX(line.getValue()); + drawMarked(start.y, start.x, startX + line.getValue()); + start.y += getFontHeight(); + if (max - min > 1) { + for (int i = min + 1; i < max; i++) { + line = measuredLines.get(i); + startX = getStartX(line.getValue()); + drawMarked(start.y, startX, startX + line.getValue()); + start.y += getFontHeight(); + } + } + line = measuredLines.get(max); + startX = getStartX(line.getValue()); + drawMarked(start.y, startX, end.x); + } + } + // draw cursor + start = getPosOf(measuredLines, handler.getMainCursor()); + if (this.renderCursor) { + drawCursor(start.x, start.y); + } + } + } + + public Point getCursorPos(List lines, int x, int y) { + if (lines.isEmpty()) { + return new Point(); + } + List> measuredLines = measureLines(lines); + y -= getStartY(measuredLines.size()) + this.y; + int index = (int) (y / (getFontHeight())); + if (index < 0) return new Point(); + if (index >= measuredLines.size()) + return new Point(measuredLines.get(measuredLines.size() - 1).getKey().length(), measuredLines.size() - 1); + Pair line = measuredLines.get(index); + x -= getStartX(line.getValue()) + this.x; + if (line.getValue() <= 0) return new Point(0, index); + if (line.getValue() < x) return new Point(line.getKey().length(), index); + float currentX = 0; + for (int i = 0; i < line.getKey().length(); i++) { + char c = line.getKey().charAt(i); + currentX += getFontRenderer().getCharWidth(c) * scale; + if (currentX >= x) { + return new Point(i, index); + } + } + return new Point(); + } + + public Point2D.Float getPosOf(List> measuredLines, Point cursorPos) { + if (measuredLines.isEmpty()) { + return new Point2D.Float(getStartX(0), getStartY(1)); + } + Pair line = measuredLines.get(cursorPos.y); + String sub = line.getKey().substring(0, Math.min(line.getKey().length(), cursorPos.x)); + return new Point2D.Float(getStartX(line.getRight()) + getFontRenderer().getStringWidth(sub) * scale, getStartY(measuredLines.size()) + cursorPos.y * getFontHeight()); + } + + @SideOnly(Side.CLIENT) + public void drawMarked(float y0, float x0, float x1) { + y0 -= 1; + float y1 = y0 + getFontHeight(); + float red = Color.getRedF(markedColor); + float green = Color.getGreenF(markedColor); + float blue = Color.getBlueF(markedColor); + float alpha = Color.getAlphaF(markedColor); + if (alpha == 0) + alpha = 1f; + Tessellator tessellator = Tessellator.instance; + GlStateManager.color(red, green, blue, alpha); + GlStateManager.disableTexture2D(); + tessellator.startDrawingQuads(); + tessellator.addVertex(x0, y1, 0.0D); + tessellator.addVertex(x1, y1, 0.0D); + tessellator.addVertex(x1, y0, 0.0D); + tessellator.addVertex(x0, y0, 0.0D); + tessellator.draw(); + GlStateManager.disableColorLogic(); + GlStateManager.enableTexture2D(); + GlStateManager.color(1, 1, 1, 1); + } + + @SideOnly(Side.CLIENT) + private void drawCursor(float x0, float y0) { + x0 = (x0 - 0.8f) / scale; + y0 = (y0 - 1) / scale; + float x1 = x0 + 0.6f; + float y1 = y0 + 9; + float red = Color.getRedF(color); + float green = Color.getGreenF(color); + float blue = Color.getBlueF(color); + float alpha = Color.getAlphaF(color); + if (alpha == 0) + alpha = 1f; + Tessellator tessellator = Tessellator.instance; + GlStateManager.disableBlend(); + GlStateManager.pushMatrix(); + GlStateManager.scale(scale, scale, 0); + GlStateManager.color(red, green, blue, alpha); + GlStateManager.disableTexture2D(); + tessellator.startDrawingQuads(); + tessellator.addVertex(x0, y1, 0.0D); + tessellator.addVertex(x1, y1, 0.0D); + tessellator.addVertex(x1, y0, 0.0D); + tessellator.addVertex(x0, y0, 0.0D); + tessellator.draw(); + GlStateManager.color(1, 1, 1, 1); + GlStateManager.enableTexture2D(); + GlStateManager.popMatrix(); + GlStateManager.enableBlend(); + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextFieldWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextFieldWidget.java new file mode 100644 index 0000000..eeada23 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextFieldWidget.java @@ -0,0 +1,276 @@ +package com.gtnewhorizons.modularui.common.widget.textfield; + +import com.gtnewhorizons.modularui.common.internal.network.NetworkUtils; +import com.gtnewhorizons.modularui.api.GlStateManager; +import com.gtnewhorizons.modularui.api.drawable.GuiHelper; +import com.gtnewhorizons.modularui.api.math.MathExpression; +import com.gtnewhorizons.modularui.api.widget.ISyncedWidget; +import net.minecraft.network.PacketBuffer; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.text.ParseException; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +/** + * Text input widget with one line only. Can be synced between client and server. Can handle text validation. + */ +public class TextFieldWidget extends BaseTextFieldWidget implements ISyncedWidget { + + private Supplier getter; + private Consumer setter; + private Function validator = val -> val; + private boolean syncsToServer = true; + private boolean syncsToClient = true; + + public static Number parse(String num) { + try { + return format.parse(num); + } catch (ParseException e) { + e.printStackTrace(); + } + return 0; + } + + @Override + public void onInit() { + } + + @Override + public void draw(float partialTicks) { + GuiHelper.useScissor(pos.x, pos.y, size.width, size.height, () -> { + GlStateManager.pushMatrix(); + GlStateManager.translate(1 - scrollOffset, 1, 0); + renderer.setSimulate(false); + renderer.setScale(scale); + renderer.setAlignment(textAlignment, scrollBar == null ? size.width - 2 : -1, size.height); + renderer.draw(handler.getText()); + GlStateManager.popMatrix(); + }); + } + + @NotNull + public String getText() { + if (handler.getText().isEmpty()) { + return ""; + } + if (handler.getText().size() > 1) { + throw new IllegalStateException("TextFieldWidget can only have one line!"); + } + return handler.getText().get(0); + } + + public void setText(@NotNull String text) { + if (handler.getText().isEmpty()) { + handler.getText().add(text); + } else { + handler.getText().set(0, text); + } + } + + @Override + public void onRemoveFocus() { + super.onRemoveFocus(); + if (handler.getText().isEmpty()) { + handler.getText().add(validator.apply("")); + } else if (handler.getText().size() == 1) { + handler.getText().set(0, validator.apply(handler.getText().get(0))); + } else { + throw new IllegalStateException("TextFieldWidget can only have one line!"); + } + this.setter.accept(getText()); + if (syncsToServer()) { + syncToServer(1, buffer -> NetworkUtils.writeStringSafe(buffer, getText())); + } + } + + @Override + public void detectAndSendChanges(boolean init) { + if (syncsToClient() && getter != null) { + String val = getter.get(); + if (init || !getText().equals(val)) { + setText(val); + syncToClient(1, buffer -> NetworkUtils.writeStringSafe(buffer, getText())); + } + } + } + + @Override + public void readOnClient(int id, PacketBuffer buf) { + if (id == 1) { + if (!isFocused()) { + try{ + setText(buf.readStringFromBuffer(Short.MAX_VALUE)); + } + catch (IOException e){ + + } + if (this.setter != null && (this.getter == null || !getText().equals(this.getter.get()))) { + this.setter.accept(getText()); + } + } + } + } + + @Override + public void readOnServer(int id, PacketBuffer buf) { + if (id == 1) { + try{ + setText(buf.readStringFromBuffer(Short.MAX_VALUE)); + } + catch (IOException e){ + + } + if (this.setter != null) { + this.setter.accept(getText()); + } + } + } + + /** + * @return if this widget should operate on the sever side. + * For example detecting and sending changes to client. + */ + public boolean syncsToClient() { + return syncsToClient; + } + + /** + * @return if this widget should operate on the client side. + * For example, sending a changed value to the server. + */ + public boolean syncsToServer() { + return syncsToServer; + } + + /** + * Determines how this widget should sync values + * + * @param syncsToClient if this widget should sync changes to the server + * @param syncsToServer if this widget should detect changes on server and sync them to client + */ + public TextFieldWidget setSynced(boolean syncsToClient, boolean syncsToServer) { + this.syncsToClient = syncsToClient; + this.syncsToServer = syncsToServer; + return this; + } + + public TextFieldWidget setMaxLength(int maxLength) { + this.handler.setMaxCharacters(maxLength); + return this; + } + + public TextFieldWidget setSetter(Consumer setter) { + this.setter = setter; + return this; + } + + public TextFieldWidget setSetterLong(Consumer setter) { + this.setter = val -> { + if (!val.isEmpty()) { + setter.accept(parse(val).longValue()); + } + }; + return this; + } + + public TextFieldWidget setSetterInt(Consumer setter) { + this.setter = val -> { + if (!val.isEmpty()) { + setter.accept(parse(val).intValue()); + } + }; + return this; + } + + public TextFieldWidget setGetter(Supplier getter) { + this.getter = getter; + return this; + } + + public TextFieldWidget setGetterLong(Supplier getter) { + this.getter = () -> String.valueOf(getter.get()); + return this; + } + + public TextFieldWidget setGetterInt(Supplier getter) { + this.getter = () -> String.valueOf(getter.get()); + return this; + } + + public TextFieldWidget setPattern(Pattern pattern) { + handler.setPattern(pattern); + return this; + } + + public TextFieldWidget setTextColor(int textColor) { + this.renderer.setColor(textColor); + return this; + } + + public TextFieldWidget setMarkedColor(int color) { + this.renderer.setMarkedColor(color); + return this; + } + + public TextFieldWidget setValidator(Function validator) { + this.validator = validator; + return this; + } + + public TextFieldWidget setNumbersLong(Function validator) { + setPattern(WHOLE_NUMS); + setValidator(val -> { + long num; + if (val.isEmpty()) { + num = 0; + } else { + num = (long) MathExpression.parseMathExpression(val); + } + return format.format(validator.apply(num)); + }); + return this; + } + + public TextFieldWidget setNumbers(Function validator) { + setPattern(WHOLE_NUMS); + return setValidator(val -> { + int num; + if (val.isEmpty()) { + num = 0; + } else { + num = (int) MathExpression.parseMathExpression(val); + } + return format.format(validator.apply(num)); + }); + } + + public TextFieldWidget setNumbersDouble(Function validator) { + setPattern(DECIMALS); + return setValidator(val -> { + double num; + if (val.isEmpty()) { + num = 0; + } else { + num = MathExpression.parseMathExpression(val); + } + return format.format(validator.apply(num)); + }); + } + + public TextFieldWidget setNumbers(Supplier min, Supplier max) { + return setNumbers(val -> Math.min(max.get(), Math.max(min.get(), val))); + } + + public TextFieldWidget setNumbersLong(Supplier min, Supplier max) { + return setNumbersLong(val -> Math.min(max.get(), Math.max(min.get(), val))); + } + + public TextFieldWidget setNumbers(int min, int max) { + return setNumbers(val -> Math.min(max, Math.max(min, val))); + } + +} diff --git a/src/main/java/com/gtnewhorizons/modularui/config/Config.java b/src/main/java/com/gtnewhorizons/modularui/config/Config.java new file mode 100644 index 0000000..59a3679 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/config/Config.java @@ -0,0 +1,87 @@ +package com.gtnewhorizons.modularui.config; + +import java.io.File; + +import net.minecraftforge.common.config.Configuration; + +public class Config { + + public static Configuration config; + + public static int openCloseDurationMs = 250; + public static boolean openCloseFade = false; + public static boolean openCloseScale = true; + public static boolean openCloseTranslateFromBottom = true; + public static boolean openCloseRotateFast = false; + public static boolean smoothProgressbar = true; + + public static boolean debug = false; + + public static final String CATEGORY_ANIMATIONS = "animations"; + public static final String CATEGORY_DEBUG = "debug"; + + public static void init(File configFile) { + config = new Configuration(configFile); + syncConfig(); + } + + public static void syncConfig() { + config.setCategoryComment(CATEGORY_ANIMATIONS, "Animations"); + config.setCategoryComment(CATEGORY_DEBUG, "Debug"); + + openCloseDurationMs = config.get( + CATEGORY_ANIMATIONS, + "openCloseDurationMs", + 250, + "How many milliseconds will it take to draw open/close animation", + 0, + 3000 + ).getInt(); + + openCloseFade = config.get( + CATEGORY_ANIMATIONS, + "openCloseFade", + false, + "Whether to draw fade in/out animation on GUI open/close" + ).getBoolean(); + + openCloseScale = config.get( + CATEGORY_ANIMATIONS, + "openCloseScale", + true, + "Whether to draw scale in/out animation on GUI open/close" + ).getBoolean(); + + openCloseTranslateFromBottom = config.get( + CATEGORY_ANIMATIONS, + "openCloseTranslateFromBottom", + true, + "Whether to draw GUI coming out of / going out to the bottom of the screen on GUI open/close" + ).getBoolean(); + + openCloseRotateFast = config.get( + CATEGORY_ANIMATIONS, + "openCloseRotateFast", + false, + "Whether to draw GUI rotating fast on GUI open/close" + ).getBoolean(); + + smoothProgressbar = config.get( + CATEGORY_ANIMATIONS, + "smoothProgressbar", + true, + "Draw progress bar smoothly" + ).getBoolean(); + + debug = config.get( + CATEGORY_DEBUG, + "debug", + false, + "Enable Debug information" + ).getBoolean(); + + if (config.hasChanged()) { + config.save(); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/config/GuiFactory.java b/src/main/java/com/gtnewhorizons/modularui/config/GuiFactory.java new file mode 100644 index 0000000..e7dc056 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/config/GuiFactory.java @@ -0,0 +1,31 @@ +package com.gtnewhorizons.modularui.config; + +import cpw.mods.fml.client.IModGuiFactory; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; + +import java.util.Set; + +@SuppressWarnings("unused") +public class GuiFactory implements IModGuiFactory { + + @Override + public void initialize(Minecraft minecraftInstance) { + + } + + @Override + public Class mainConfigGuiClass() { + return ModularUIGuiConfig.class; + } + + @Override + public Set runtimeGuiCategories() { + return null; + } + + @Override + public RuntimeOptionGuiHandler getHandlerFor(RuntimeOptionCategoryElement element) { + return null; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/config/ModularUIGuiConfig.java b/src/main/java/com/gtnewhorizons/modularui/config/ModularUIGuiConfig.java new file mode 100644 index 0000000..fbee147 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/config/ModularUIGuiConfig.java @@ -0,0 +1,36 @@ +package com.gtnewhorizons.modularui.config; + +import com.gtnewhorizons.modularui.ModularUI; +import cpw.mods.fml.client.config.GuiConfig; +import cpw.mods.fml.client.config.IConfigElement; +import net.minecraft.client.gui.GuiScreen; +import net.minecraftforge.common.config.ConfigElement; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +@SuppressWarnings("rawtypes") +public class ModularUIGuiConfig extends GuiConfig { + + public ModularUIGuiConfig(GuiScreen parentScreen) { + super( + parentScreen, + getConfigElements(), + ModularUI.MODID, + false, + false, + GuiConfig.getAbridgedConfigPath(Config.config.toString())); + } + + private static List getConfigElements() { + List list = new ArrayList<>(); + String[] categories = new String[] {Config.CATEGORY_ANIMATIONS, Config.CATEGORY_DEBUG}; + + for (String category : categories) { + list.add(new ConfigElement(Config.config.getCategory(category.toLowerCase(Locale.US)))); + } + + return list; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/integration/jei/ModularUIHandler.java b/src/main/java/com/gtnewhorizons/modularui/integration/jei/ModularUIHandler.java new file mode 100644 index 0000000..0086541 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/integration/jei/ModularUIHandler.java @@ -0,0 +1,67 @@ +//package com.gtnewhorizons.modularui.integration.jei; +// +//import com.gtnewhorizons.modularui.api.screen.ModularWindow; +//import com.gtnewhorizons.modularui.api.widget.IGhostIngredientTarget; +//import com.gtnewhorizons.modularui.api.widget.IIngredientProvider; +//import com.gtnewhorizons.modularui.api.widget.IWidgetParent; +//import com.gtnewhorizons.modularui.api.widget.Widget; +//import com.gtnewhorizons.modularui.common.internal.wrapper.ModularGui; +//import mezz.jei.api.gui.IAdvancedGuiHandler; +//import mezz.jei.api.gui.IGhostIngredientHandler; +//import mezz.jei.api.gui.IGuiProperties; +//import mezz.jei.api.gui.IGuiScreenHandler; +//import mezz.jei.gui.overlay.GuiProperties; +//import org.jetbrains.annotations.NotNull; +//import org.jetbrains.annotations.Nullable; +// +//import java.awt.*; +//import java.util.LinkedList; +//import java.util.List; +// +//public class ModularUIHandler implements IAdvancedGuiHandler, IGhostIngredientHandler, IGuiScreenHandler { +// +// @Override +// public @NotNull Class getGuiContainerClass() { +// return ModularGui.class; +// } +// +// @Nullable +// @Override +// public List getGuiExtraAreas(@NotNull ModularGui guiContainer) { +// return guiContainer.getContext().getJeiExclusionZones(); +// } +// +// @Nullable +// @Override +// public Object getIngredientUnderMouse(@NotNull ModularGui guiContainer, int mouseX, int mouseY) { +// Widget hovered = guiContainer.getContext().getCursor().getHovered(); +// return hovered instanceof IIngredientProvider ? ((IIngredientProvider) hovered).getIngredient() : null; +// } +// +// @Override +// public @NotNull List> getTargets(ModularGui gui, @NotNull I ingredient, boolean doStart) { +// LinkedList> targets = new LinkedList<>(); +// for (ModularWindow window : gui.getContext().getOpenWindowsReversed()) { +// IWidgetParent.forEachByLayer(window, true, widget -> { +// if (widget instanceof IGhostIngredientTarget) { +// Target target = ((IGhostIngredientTarget) widget).getTarget(ingredient); +// if (target != null) { +// targets.addFirst((Target) target); +// } +// } +// return false; +// }); +// } +// return targets; +// } +// +// @Override +// public void onComplete() { +// } +// +// @Nullable +// @Override +// public IGuiProperties apply(@NotNull ModularGui guiScreen) { +// return guiScreen.getContext().doShowJei() ? GuiProperties.create(guiScreen) : null; +// } +//} diff --git a/src/main/java/com/gtnewhorizons/modularui/integration/jei/ModularUIJeiPlugin.java b/src/main/java/com/gtnewhorizons/modularui/integration/jei/ModularUIJeiPlugin.java new file mode 100644 index 0000000..b951e9e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/integration/jei/ModularUIJeiPlugin.java @@ -0,0 +1,19 @@ +//package com.gtnewhorizons.modularui.integration.jei; +// +//import com.gtnewhorizons.modularui.common.internal.wrapper.ModularGui; +//import mezz.jei.api.IModPlugin; +//import mezz.jei.api.IModRegistry; +//import mezz.jei.api.JEIPlugin; +//import org.jetbrains.annotations.NotNull; +// +//@JEIPlugin +//public class ModularUIJeiPlugin implements IModPlugin { +// +// @Override +// public void register(@NotNull IModRegistry registry) { +// ModularUIHandler uiHandler = new ModularUIHandler(); +// registry.addAdvancedGuiHandlers(uiHandler); +// registry.addGhostIngredientHandler(ModularGui.class, uiHandler); +// registry.addGuiScreenHandler(ModularGui.class, uiHandler); +// } +//} diff --git a/src/main/java/com/gtnewhorizons/modularui/mixinplugin/Mixin.java b/src/main/java/com/gtnewhorizons/modularui/mixinplugin/Mixin.java new file mode 100644 index 0000000..1377520 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/mixinplugin/Mixin.java @@ -0,0 +1,49 @@ +package com.gtnewhorizons.modularui.mixinplugin; + +import static com.gtnewhorizons.modularui.mixinplugin.TargetedMod.*; + +import cpw.mods.fml.relauncher.FMLLaunchHandler; +import java.util.Arrays; +import java.util.List; + +public enum Mixin { + + // + // IMPORTANT: Do not make any references to any mod from this file. This file is loaded quite early on and if + // you refer to other mods you load them as well. The consequence is: You can't inject any previously loaded + // classes! + // Exception: Tags.java, as long as it is used for Strings only! + // + + // Replace with your own mixins: + GuiContainerMixin("GuiContainerMixin", Side.CLIENT, VANILLA); + + public final String mixinClass; + public final List targetedMods; + private final Side side; + + Mixin(String mixinClass, Side side, TargetedMod... targetedMods) { + this.mixinClass = mixinClass; + this.targetedMods = Arrays.asList(targetedMods); + this.side = side; + } + + Mixin(String mixinClass, TargetedMod... targetedMods) { + this.mixinClass = mixinClass; + this.targetedMods = Arrays.asList(targetedMods); + this.side = Side.BOTH; + } + + public boolean shouldLoad(List loadedMods) { + return (side == Side.BOTH + || side == Side.SERVER && FMLLaunchHandler.side().isServer() + || side == Side.CLIENT && FMLLaunchHandler.side().isClient()) + && loadedMods.containsAll(targetedMods); + } +} + +enum Side { + BOTH, + CLIENT, + SERVER; +} diff --git a/src/main/java/com/gtnewhorizons/modularui/mixinplugin/MixinPlugin.java b/src/main/java/com/gtnewhorizons/modularui/mixinplugin/MixinPlugin.java new file mode 100644 index 0000000..d0a4050 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/mixinplugin/MixinPlugin.java @@ -0,0 +1,110 @@ +package com.gtnewhorizons.modularui.mixinplugin; + +import static com.gtnewhorizons.modularui.mixinplugin.TargetedMod.VANILLA; +import static java.nio.file.Files.walk; + +import com.gtnewhorizons.modularui.Tags; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import net.minecraft.launchwrapper.Launch; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.spongepowered.asm.lib.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; +import ru.timeconqueror.spongemixins.MinecraftURLClassPath; + +public class MixinPlugin implements IMixinConfigPlugin { + + private static final Logger LOG = LogManager.getLogger(Tags.MODID + " mixins"); + private static final Path MODS_DIRECTORY_PATH = new File(Launch.minecraftHome, "mods/").toPath(); + + @Override + public void onLoad(String mixinPackage) {} + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + return true; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) {} + + // This method return a List of mixins. Every mixins in this list will be loaded. + @Override + public List getMixins() { + final boolean isDevelopmentEnvironment = (boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment"); + + List loadedMods = Arrays.stream(TargetedMod.values()) + .filter(mod -> mod == VANILLA || (mod.loadInDevelopment && isDevelopmentEnvironment) || loadJarOf(mod)) + .collect(Collectors.toList()); + + for (TargetedMod mod : TargetedMod.values()) { + if (loadedMods.contains(mod)) { + LOG.info("Found " + mod.modName + "! Integrating now..."); + } else { + LOG.info("Could not find " + mod.modName + "! Skipping integration...."); + } + } + + List mixins = new ArrayList<>(); + for (Mixin mixin : Mixin.values()) { + if (mixin.shouldLoad(loadedMods)) { + mixins.add(mixin.mixinClass); + LOG.debug("Loading mixin: " + mixin.mixinClass); + } + } + return mixins; + } + + private boolean loadJarOf(final TargetedMod mod) { + try { + File jar = findJarOf(mod); + if (jar == null) { + LOG.info("Jar not found for " + mod); + return false; + } + + LOG.info("Attempting to add " + jar + " to the URL Class Path"); + if (!jar.exists()) { + throw new FileNotFoundException(jar.toString()); + } + MinecraftURLClassPath.addJar(jar); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + public static File findJarOf(final TargetedMod mod) { + try { + return walk(MODS_DIRECTORY_PATH) + .filter(mod::isMatchingJar) + .map(Path::toFile) + .findFirst() + .orElse(null); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {} + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {} +} diff --git a/src/main/java/com/gtnewhorizons/modularui/mixinplugin/TargetedMod.java b/src/main/java/com/gtnewhorizons/modularui/mixinplugin/TargetedMod.java new file mode 100644 index 0000000..b9c8952 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/mixinplugin/TargetedMod.java @@ -0,0 +1,47 @@ +package com.gtnewhorizons.modularui.mixinplugin; + +import com.google.common.io.Files; +import java.nio.file.Path; + +public enum TargetedMod { + + // + // IMPORTANT: Do not make any references to any mod from this file. This file is loaded quite early on and if + // you refer to other mods you load them as well. The consequence is: You can't inject any previously loaded + // classes! + // Exception: Tags.java, as long as it is used for Strings only! + // + + // Replace with your injected mods here, but always keep VANILLA: + VANILLA("Minecraft", "unused", true), + CODECHICKENLIB("CodeChickenLib", "CodeChickenLib", true), + NOTENOUGHITEMS("NotEnoughItems", "NotEnoughItems", true); + // GREGTECH("GregTech", "gregtech", false); + + public final String modName; + public final String jarNamePrefixLowercase; + // Optional dependencies can be omitted in development. Especially skipping GT5U will drastically speed up your game + // start! + public final boolean loadInDevelopment; + + TargetedMod(String modName, String jarNamePrefix, boolean loadInDevelopment) { + this.modName = modName; + this.jarNamePrefixLowercase = jarNamePrefix.toLowerCase(); + this.loadInDevelopment = loadInDevelopment; + } + + public boolean isMatchingJar(Path path) { + final String pathString = path.toString(); + final String nameLowerCase = Files.getNameWithoutExtension(pathString).toLowerCase(); + final String fileExtension = Files.getFileExtension(pathString); + + return nameLowerCase.startsWith(jarNamePrefixLowercase) && "jar".equals(fileExtension); + } + + @Override + public String toString() { + return "TargetedMod{" + "modName='" + + modName + '\'' + ", jarNamePrefixLowercase='" + + jarNamePrefixLowercase + '\'' + '}'; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/mixins/GuiContainerMixin.java b/src/main/java/com/gtnewhorizons/modularui/mixins/GuiContainerMixin.java new file mode 100644 index 0000000..db395e8 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/mixins/GuiContainerMixin.java @@ -0,0 +1,59 @@ +package com.gtnewhorizons.modularui.mixins; + +import net.minecraft.client.gui.inventory.GuiContainer; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.util.Set; + +@Mixin(GuiContainer.class) +public interface GuiContainerMixin { + + @Accessor("theSlot") + void setHoveredSlot(Slot slot); + + @Accessor + Slot getClickedSlot(); + + @Accessor + ItemStack getDraggedStack(); + + @Accessor + boolean getIsRightMouseClick(); + + @Accessor("field_146987_F") + int getDragSplittingLimit(); + + @Invoker("func_146980_g") + void invokeUpdateDragSplitting(); + + @Accessor("field_147008_s") + Set getDragSplittingSlots(); + + @Accessor("field_147007_t") + boolean isDragSplitting(); + + @Accessor("field_146996_I") + int getDragSplittingRemnant(); + + @Accessor + ItemStack getReturningStack(); + + @Accessor + void setReturningStack(ItemStack stack); + + @Accessor + Slot getReturningStackDestSlot(); + + @Accessor("field_147011_y") + int getTouchUpX(); + + @Accessor("field_147010_z") + int getTouchUpY(); + + @Accessor + long getReturningStackTime(); +} diff --git a/src/main/java/com/gtnewhorizons/modularui/mixins/KeyBindAccess.java b/src/main/java/com/gtnewhorizons/modularui/mixins/KeyBindAccess.java new file mode 100644 index 0000000..49e2f93 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/mixins/KeyBindAccess.java @@ -0,0 +1,24 @@ +package com.gtnewhorizons.modularui.mixins; + +import net.minecraft.client.settings.KeyBinding; +import net.minecraft.util.IntHashMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Set; + +@Mixin(KeyBinding.class) +public interface KeyBindAccess { + + @Accessor + IntHashMap getHash(); + + @Accessor + void setPressed(boolean pressed); + + @Accessor + int getPressTime(); + + @Accessor + void setPressTime(int time); +} diff --git a/src/main/java/com/gtnewhorizons/modularui/mixins/KeyBindMixin.java b/src/main/java/com/gtnewhorizons/modularui/mixins/KeyBindMixin.java new file mode 100644 index 0000000..9da97ba --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/mixins/KeyBindMixin.java @@ -0,0 +1,45 @@ +//package com.gtnewhorizons.modularui.core.mixin; +// +//import com.gtnewhorizons.modularui.api.KeyBindAPI; +//import com.gtnewhorizons.modularui.common.keybind.KeyBindHandler; +//import com.gtnewhorizons.modularui.api.KeyBindAPI; +//import net.minecraft.client.settings.KeyBinding; +//import org.spongepowered.asm.mixin.Mixin; +//import org.spongepowered.asm.mixin.Overwrite; +//import org.spongepowered.asm.mixin.injection.At; +//import org.spongepowered.asm.mixin.injection.Inject; +//import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +// +//import java.util.Collection; +// +//@Mixin(KeyBinding.class) +//public class KeyBindMixin { +// +// @Inject(method = "conflicts", at = @At("HEAD"), cancellable = true, remap = false) +// public void conflicts(KeyBinding other, CallbackInfoReturnable cir) { +// if (KeyBindAPI.areCompatible((KeyBinding) (Object) this, other)) { +// cir.setReturnValue(false); +// } +// } +// +// /** +// * @author brachy84 +// */ +// @Overwrite +// public static void onTick(int keyCode) { +// if (keyCode != 0) { +// KeyBinding keyBinding = KeyBindHandler.getKeyBindingMap().lookupActive(keyCode); +// if (keyBinding != null) { +// KeyBindHandler.incrementPressTime(keyBinding); +// +// Collection compatibles = KeyBindAPI.getCompatibles(keyBinding); +// if (compatibles.isEmpty()) return; +// for (KeyBinding keyBinding1 : compatibles) { +// if (keyBinding1.isActiveAndMatches(keyCode)) { +// KeyBindHandler.incrementPressTime(keyBinding1); +// } +// } +// } +// } +// } +//} diff --git a/src/main/java/com/gtnewhorizons/modularui/mixins/SPacketSetSlotMixin.java b/src/main/java/com/gtnewhorizons/modularui/mixins/SPacketSetSlotMixin.java new file mode 100644 index 0000000..ee30996 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/mixins/SPacketSetSlotMixin.java @@ -0,0 +1,29 @@ +//package com.gtnewhorizons.modularui.core.mixin; +// +//import net.minecraft.item.ItemStack; +//import net.minecraft.network.PacketBuffer; +//import net.minecraft.network.play.server.SPacketSetSlot; +//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; +// +//import java.io.IOException; +// +//@Mixin(SPacketSetSlot.class) +//public class SPacketSetSlotMixin { +// +// @Shadow +// private ItemStack item; +// +// @Inject(method = "readPacketData", at = @At("TAIL")) +// public void readPacketData(PacketBuffer buf, CallbackInfo ci) throws IOException { +// this.item.setCount(buf.readVarIntFromBuffer()); +// } +// +// @Inject(method = "writePacketData", at = @At("TAIL")) +// public void writePacketData(PacketBuffer buf, CallbackInfo ci) throws IOException { +// buf.writeVarInt(item.getCount()); +// } +//} diff --git a/src/main/java/com/gtnewhorizons/modularui/test/SyncedTileEntityBase.java b/src/main/java/com/gtnewhorizons/modularui/test/SyncedTileEntityBase.java new file mode 100644 index 0000000..290e2df --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/test/SyncedTileEntityBase.java @@ -0,0 +1,95 @@ +package com.gtnewhorizons.modularui.test; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.network.NetworkManager; +import net.minecraft.network.PacketBuffer; +import net.minecraft.network.play.server.S35PacketUpdateTileEntity; +import net.minecraft.tileentity.TileEntity; +import net.minecraftforge.common.util.Constants.NBT; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +public abstract class SyncedTileEntityBase extends TileEntity { + + public abstract void writeInitialSyncData(PacketBuffer buf); + + public abstract void receiveInitialSyncData(PacketBuffer buf); + + public abstract void receiveCustomData(int discriminator, PacketBuffer buf); + + private static class UpdateEntry { + private final int discriminator; + private final byte[] updateData; + + public UpdateEntry(int discriminator, byte[] updateData) { + this.discriminator = discriminator; + this.updateData = updateData; + } + } + + protected final List updateEntries = new ArrayList<>(); + + public void writeCustomData(int discriminator, Consumer dataWriter) { + ByteBuf backedBuffer = Unpooled.buffer(); + dataWriter.accept(new PacketBuffer(backedBuffer)); + byte[] updateData = Arrays.copyOfRange(backedBuffer.array(), 0, backedBuffer.writerIndex()); + updateEntries.add(new UpdateEntry(discriminator, updateData)); + worldObj.setBlockMetadataWithNotify(xCoord, yCoord, zCoord, getBlockMetadata(), 0); + } + + @Override + public S35PacketUpdateTileEntity getDescriptionPacket() { + NBTTagCompound updateTag = new NBTTagCompound(); + NBTTagList tagList = new NBTTagList(); + for (UpdateEntry updateEntry : updateEntries) { + NBTTagCompound entryTag = new NBTTagCompound(); + entryTag.setInteger("i", updateEntry.discriminator); + entryTag.setByteArray("d", updateEntry.updateData); + tagList.appendTag(entryTag); + } + this.updateEntries.clear(); + updateTag.setTag("d", tagList); + return new S35PacketUpdateTileEntity(xCoord, yCoord, zCoord, 0, updateTag); + } + + @Override + public void onDataPacket(@Nonnull NetworkManager net, S35PacketUpdateTileEntity pkt) { + // #getNbtCompound + NBTTagCompound updateTag = pkt.func_148857_g(); + NBTTagList tagList = updateTag.getTagList("d", NBT.TAG_COMPOUND); + for (int i = 0; i < tagList.tagCount(); i++) { + NBTTagCompound entryTag = tagList.getCompoundTagAt(i); + int discriminator = entryTag.getInteger("i"); + byte[] updateData = entryTag.getByteArray("d"); + ByteBuf backedBuffer = Unpooled.copiedBuffer(updateData); + receiveCustomData(discriminator, new PacketBuffer(backedBuffer)); + } + } + + @Nonnull + public NBTTagCompound getUpdateTag() { + NBTTagCompound updateTag = new NBTTagCompound(); + this.writeToNBT(updateTag); + ByteBuf backedBuffer = Unpooled.buffer(); + writeInitialSyncData(new PacketBuffer(backedBuffer)); + byte[] updateData = Arrays.copyOfRange(backedBuffer.array(), 0, backedBuffer.writerIndex()); + updateTag.setByteArray("d", updateData); + return updateTag; + } + + @Override + public void readFromNBT(@Nonnull NBTTagCompound tag) { + super.readFromNBT(tag); // deserializes Forge data and capabilities + byte[] updateData = tag.getByteArray("d"); + ByteBuf backedBuffer = Unpooled.copiedBuffer(updateData); + receiveInitialSyncData(new PacketBuffer(backedBuffer)); + } + +} diff --git a/src/main/java/com/gtnewhorizons/modularui/test/TestBlock.java b/src/main/java/com/gtnewhorizons/modularui/test/TestBlock.java new file mode 100644 index 0000000..eaf4744 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/test/TestBlock.java @@ -0,0 +1,34 @@ +package com.gtnewhorizons.modularui.test; + +import com.gtnewhorizons.modularui.api.UIInfos; +import net.minecraft.block.Block; +import net.minecraft.block.ITileEntityProvider; +import net.minecraft.block.material.Material; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; + +import javax.annotation.Nullable; + +public class TestBlock extends Block implements ITileEntityProvider { + + public TestBlock(Material p_i45394_1_) { + super(p_i45394_1_); + } + + @Nullable + @Override + public TileEntity createNewTileEntity(World worldIn, int meta) { + return new TestTile(); + } + + @Override + public boolean onBlockActivated(World p_149727_1_, int p_149727_2_, int p_149727_3_, int p_149727_4_, EntityPlayer p_149727_5_, int p_149727_6_, float p_149727_7_, float p_149727_8_, float p_149727_9_) + { + if (!p_149727_1_.isRemote) { + UIInfos.TILE_MODULAR_UI.open(p_149727_5_, p_149727_1_, Vec3.createVectorHelper(p_149727_2_, p_149727_3_, p_149727_4_)); + } + return true; + } +} diff --git a/src/main/java/com/gtnewhorizons/modularui/test/TestTile.java b/src/main/java/com/gtnewhorizons/modularui/test/TestTile.java new file mode 100644 index 0000000..4cb48ee --- /dev/null +++ b/src/main/java/com/gtnewhorizons/modularui/test/TestTile.java @@ -0,0 +1,358 @@ +package com.gtnewhorizons.modularui.test; + + +import com.gtnewhorizons.modularui.ModularUI; +import com.gtnewhorizons.modularui.api.ModularUITextures; +import com.gtnewhorizons.modularui.api.drawable.AdaptableUITexture; +import com.gtnewhorizons.modularui.api.drawable.Text; +import com.gtnewhorizons.modularui.api.drawable.UITexture; +import com.gtnewhorizons.modularui.api.forge.ItemStackHandler; +import com.gtnewhorizons.modularui.api.math.Alignment; +import com.gtnewhorizons.modularui.api.math.Color; +import com.gtnewhorizons.modularui.api.math.CrossAxisAlignment; +import com.gtnewhorizons.modularui.api.math.MainAxisAlignment; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import com.gtnewhorizons.modularui.api.math.Size; +import com.gtnewhorizons.modularui.api.screen.ModularWindow; +import com.gtnewhorizons.modularui.api.screen.UIBuildContext; +import com.gtnewhorizons.modularui.common.widget.ButtonWidget; +import com.gtnewhorizons.modularui.common.widget.ChangeableWidget; +import com.gtnewhorizons.modularui.common.widget.Column; +import com.gtnewhorizons.modularui.common.widget.CycleButtonWidget; +import com.gtnewhorizons.modularui.common.widget.DrawableWidget; +import com.gtnewhorizons.modularui.common.widget.ExpandTab; +import com.gtnewhorizons.modularui.common.widget.FluidSlotWidget; +import com.gtnewhorizons.modularui.common.widget.MultiChildWidget; +import com.gtnewhorizons.modularui.common.widget.ProgressBar; +import com.gtnewhorizons.modularui.common.widget.Row; +import com.gtnewhorizons.modularui.common.widget.Scrollable; +import com.gtnewhorizons.modularui.common.widget.SliderWidget; +import com.gtnewhorizons.modularui.common.widget.SlotGroup; +import com.gtnewhorizons.modularui.common.widget.SlotWidget; +import com.gtnewhorizons.modularui.common.widget.SortableListWidget; +import com.gtnewhorizons.modularui.common.widget.TabButton; +import com.gtnewhorizons.modularui.common.widget.TabContainer; +import com.gtnewhorizons.modularui.common.widget.TextWidget; +import com.gtnewhorizons.modularui.common.widget.textfield.TextFieldWidget; +import com.gtnewhorizons.modularui.api.drawable.ItemDrawable; +import com.gtnewhorizons.modularui.api.screen.ITileWithModularUI; +import com.gtnewhorizons.modularui.api.widget.IWidgetBuilder; +import com.gtnewhorizons.modularui.api.widget.Widget; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.init.Blocks; +import net.minecraft.init.Items; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.network.PacketBuffer; +import net.minecraft.util.EnumChatFormatting; +import net.minecraftforge.fluids.FluidTank; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class TestTile extends SyncedTileEntityBase implements ITileWithModularUI { + + private int serverValue = 0; + private final FluidTank fluidTank1 = new FluidTank(10000); + private final FluidTank fluidTank2 = new FluidTank(Integer.MAX_VALUE); + private final ItemStackHandler phantomInventory = new ItemStackHandler(2) { + @Override + public int getSlotLimit(int slot) { + return Integer.MAX_VALUE; + } + }; + private final ItemStackHandler items = new ItemStackHandler(9); + private String textFieldValue = ""; + private final int duration = 60; + private int progress = 0; + private int ticks = 0; + private float sliderValue = 0; + private int serverCounter = 0; + private static final AdaptableUITexture DISPLAY = AdaptableUITexture.of("modularui:gui/background/display", 143, 75, 2); + private static final AdaptableUITexture BACKGROUND = AdaptableUITexture.of("modularui:gui/background/background", 176, 166, 3); + private static final UITexture PROGRESS_BAR = UITexture.fullImage("modularui", "gui/widgets/progress_bar_arrow"); + private static final UITexture PROGRESS_BAR_MIXER = UITexture.fullImage("modularui", "gui/widgets/progress_bar_mixer"); + + @Override + public ModularWindow createWindow(UIBuildContext buildContext) { + phantomInventory.setStackInSlot(1, new ItemStack(Items.diamond, Integer.MAX_VALUE)); + Text[] TEXT = {new Text("Blue \u00a7nUnderlined\u00a7rBlue ").color(0x3058B8), new Text("Mint").color(0x469E8F)}; + ModularWindow.Builder builder = ModularWindow.builder(new Size(176, 272)); + //.addFromJson("modularui:test", buildContext); + /*buildContext.applyToWidget("background", DrawableWidget.class, widget -> { + widget.addTooltip("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.") + .addTooltip("Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.") + .addTooltip("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet"); + });*/ + List nums = IntStream.range(1, 101).boxed().collect(Collectors.toList()); + builder.setBackground(ModularUITextures.VANILLA_BACKGROUND) + .bindPlayerInventory(buildContext.getPlayer()); + Column column = new Column(); + addInfo(column); + ChangeableWidget changeableWidget = new ChangeableWidget(this::dynamicWidget); + buildContext.addSyncedWindow(1, this::createAnotherWindow); + return builder + .widget(new TabContainer() + .setButtonSize(new Size(28, 32)) + .addTabButton(new TabButton(0) + .setBackground(false, ModularUITextures.VANILLA_TAB_TOP_START.getSubArea(0, 0, 1f, 0.5f)) + .setBackground(true, ModularUITextures.VANILLA_TAB_TOP_START.getSubArea(0, 0.5f, 1f, 1f)) + .setPos(0, -28)) + .addTabButton(new TabButton(1) + .setBackground(false, ModularUITextures.VANILLA_TAB_TOP_MIDDLE.getSubArea(0, 0, 1f, 0.5f)) + .setBackground(true, ModularUITextures.VANILLA_TAB_TOP_MIDDLE.getSubArea(0, 0.5f, 1f, 1f)) + .setPos(28, -28)) + .addTabButton(new TabButton(2) + .setBackground(false, ModularUITextures.VANILLA_TAB_TOP_MIDDLE.getSubArea(0, 0, 1f, 0.5f)) + .setBackground(true, ModularUITextures.VANILLA_TAB_TOP_MIDDLE.getSubArea(0, 0.5f, 1f, 1f)) + .setPos(56, -28)) + .addTabButton(new TabButton(3) + .setBackground(false, ModularUITextures.VANILLA_TAB_TOP_MIDDLE.getSubArea(0, 0, 1f, 0.5f)) + .setBackground(true, ModularUITextures.VANILLA_TAB_TOP_MIDDLE.getSubArea(0, 0.5f, 1f, 1f)) + .setPos(84, -28)) + .addPage(new MultiChildWidget() + .addChild(new TextWidget("Page 1")) + .addChild(new SlotWidget(phantomInventory, 0) + .setChangeListener(() -> { + serverCounter = 0; + changeableWidget.notifyChangeServer(); + }).setShiftClickPrio(0) + .setPos(10, 30)) + .addChild(SlotWidget.phantom(phantomInventory, 1) + .setShiftClickPrio(1) + .setIgnoreStackSizeLimit(true) + .setPos(28, 30)) + .addChild(changeableWidget + .setPos(12, 55)) + .addChild(SlotGroup.ofItemHandler(items, 3, false, "inv") + .setPos(12, 80)) + .setPos(10, 10) + .setDebugLabel("Page1")) + .addPage(new MultiChildWidget() + .addChild(new TextWidget("Page 2") + .setPos(10, 10)) + .addChild(column.setPos(7, 19)) + .addChild(new ButtonWidget() + .setOnClick((clickData, widget) -> { + if (!widget.isClient()) + widget.getContext().openSyncedWindow(1); + }) + .setBackground(ModularUITextures.VANILLA_BACKGROUND, new Text("Window")) + .setSize(80, 20) + .setPos(20, 100)) + .addChild(new ItemDrawable(new ItemStack(Blocks.command_block)).asWidget() + .setSize(32, 16) + .setPos(20, 80)) + .addChild(new SliderWidget() + .setBounds(0, 15) + .setGetter(() -> sliderValue) + .setSetter(val -> sliderValue = val) + .setSize(120, 20) + .setPos(7, 130)) + .addChild(TextWidget.dynamicString(() -> String.valueOf((int) (sliderValue + 0.5f))) + .setTextAlignment(Alignment.CenterLeft) + .setSize(30, 20) + .setPos(135, 130)) + .setDebugLabel("Page2")) + .addPage(new MultiChildWidget() + .addChild(new TextWidget("Page 3")) + .addChild(new CycleButtonWidget() + .setLength(3) + .setGetter(() -> serverValue) + .setSetter(val -> this.serverValue = val) + .setTexture(UITexture.fullImage("modularui", "gui/widgets/cycle_button_demo")) + .addTooltip(0, "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.") + .addTooltip(1, "Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.") + .addTooltip(2, "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet") + .setPos(new Pos2d(68, 0))) + .addChild(new TextFieldWidget() + .setGetter(() -> textFieldValue) + .setSetter(val -> textFieldValue = val) + .setTextColor(Color.WHITE.dark(1)) + .setTextAlignment(Alignment.Center) + .setScrollBar() + .setBackground(DISPLAY.withOffset(-2, -2, 4, 4)) + .setSize(92, 20) + .setPos(20, 25)) + .addChild(new ProgressBar() + .setProgress(() -> progress * 1f / duration) + .setDirection(ProgressBar.Direction.LEFT) + .setTexture(PROGRESS_BAR_MIXER, 20) + .setPos(7, 85)) + .addChild(new ProgressBar() + .setProgress(() -> progress * 1f / duration) + .setDirection(ProgressBar.Direction.RIGHT) + .setTexture(PROGRESS_BAR_MIXER, 20) + .setPos(30, 85)) + .addChild(new ProgressBar() + .setProgress(() -> progress * 1f / duration) + .setDirection(ProgressBar.Direction.UP) + .setTexture(PROGRESS_BAR_MIXER, 20) + .setPos(53, 85)) + .addChild(new ProgressBar() + .setProgress(() -> progress * 1f / duration) + .setDirection(ProgressBar.Direction.DOWN) + .setTexture(PROGRESS_BAR_MIXER, 20) + .setPos(76, 85)) + .addChild(new ProgressBar() + .setProgress(() -> progress * 1f / duration) + .setDirection(ProgressBar.Direction.CIRCULAR_CW) + .setTexture(PROGRESS_BAR_MIXER, 20) + .setPos(99, 85)) + .addChild(FluidSlotWidget.phantom(fluidTank2, true).setPos(38, 47)) + .addChild(new FluidSlotWidget(fluidTank1).setPos(20, 47)) + .addChild(new ButtonWidget() + .setOnClick((clickData, widget) -> { + if (++serverValue == 3) { + serverValue = 0; + } + }) + .setSynced(true, false) + .setBackground(DISPLAY, new Text("jTest Textg")) + .setSize(80, 20) + .setPos(10, 65)) + .addChild(new TextWidget(new Text("modularui.test").localise()).setPos(10, 110)) + .addChild(new Row() + .setAlignment(MainAxisAlignment.SPACE_BETWEEN, CrossAxisAlignment.CENTER) + .widget(new TextWidget(new Text("Some Text"))) + .widget(new ButtonWidget().setBackground(DISPLAY)) + .widget(new TextWidget(new Text("More Text"))) + .setMaxWidth(156) + .setPos(0, 130)) + .setPos(10, 10)) + .addPage(new MultiChildWidget() + .addChild(new Scrollable() + .setHorizontalScroll() + .setVerticalScroll() + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(0, 0)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(20, 20)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(40, 40)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(60, 60)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(80, 80)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(100, 100)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(120, 120)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(140, 140)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(160, 160)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(180, 180)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(200, 200)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(220, 220)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(240, 240)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(260, 260)) + .widget(ModularUITextures.ICON_INFO.asWidget().setSize(20, 20).setPos(280, 280)) + .widget(new TextFieldWidget() + .setGetter(() -> textFieldValue) + .setSetter(val -> textFieldValue = val) + .setNumbers(val -> val) + .setTextColor(Color.WHITE.dark(1)) + .setTextAlignment(Alignment.CenterLeft) + .setScrollBar() + .setBackground(DISPLAY.withOffset(-2, -2, 4, 4)) + .setSize(92, 20) + .setPos(20, 25)) + .setSize(156, 150)) + .setPos(10, 10))) + .widget(new ExpandTab() + .setNormalTexture(ModularUITextures.ICON_INFO.withFixedSize(14, 14, 3, 3)) + .widget(new DrawableWidget() + .setDrawable(ModularUITextures.ICON_INFO) + .setSize(14, 14) + .setPos(3, 3)) + .widget(new SortableListWidget<>(nums) + .setWidgetCreator(integer -> new TextWidget(integer.toString()).setSize(20, 20).addTooltip(integer.toString())) + .setSize(50, 135) + .setPos(5, 20)) + .setExpandedSize(60, 160) + .setBackground(BACKGROUND) + .setSize(20, 20) + .setPos(177, 5) + .respectAreaInNEI()) + .build(); + } + + public ModularWindow createAnotherWindow(EntityPlayer player) { + return ModularWindow.builder(100, 100) + .setBackground(ModularUITextures.VANILLA_BACKGROUND) + .widget(ButtonWidget.closeWindowButton(true).setPos(85, 5)) + .widget(SlotWidget.phantom(phantomInventory, 0) + .setShiftClickPrio(0) + .setPos(30, 30)) + .build(); + } + + public > void addInfo(T builder) { + builder.widget(new TextWidget(new Text("Probably a Machine Name").color(0x13610C))) + .widget(new TextWidget("Invalid Structure or whatever") + .addTooltip("This has a tooltip")); + if (true) { + builder.widget(new TextWidget("Maintanance Problems")); + } + builder.widget(new Row() + .widget(new TextWidget("Here you can click a button")) + .widget(new ButtonWidget() + .setOnClick(((clickData, widget) -> ModularUI.logger.info("Clicked Button"))) + .setSize(20, 9) + .setBackground(new Text("[O]")))); + } + + public Widget dynamicWidget() { + ItemStack stack = phantomInventory.getStackInSlot(0); + if (stack == null) { + return null; + } + MultiChildWidget widget = new MultiChildWidget(); + widget.addChild(new TextWidget(new Text("Item: " + stack.getDisplayName()).format(EnumChatFormatting.BLUE))) + .addChild(new CycleButtonWidget() + .setGetter(() -> serverCounter) + .setSetter(value -> serverCounter = value) + .setLength(10) + .setTextureGetter(value -> new Text(value + "")) + .setPos(5, 11)); + + return widget; + } + + @Override + public void writeInitialSyncData(PacketBuffer buf) { + buf.writeVarIntToBuffer(serverValue); + } + + @Override + public void receiveInitialSyncData(PacketBuffer buf) { + serverValue = buf.readVarIntFromBuffer(); + } + + @Override + public void receiveCustomData(int discriminator, PacketBuffer buf) { + + } + + @Override + public void writeToNBT(NBTTagCompound nbt) { + super.writeToNBT(nbt); + nbt.setInteger("Val", serverValue); + } + + @Override + public void readFromNBT(@NotNull NBTTagCompound nbt) { + super.readFromNBT(nbt); + this.serverValue = nbt.getInteger("Val"); + } + + @Override + public void updateEntity() { + if (!worldObj.isRemote) { + ticks++; + if (ticks % 20 == 0) { + if (++serverCounter == 10) { + serverCounter = 0; + } + } + } else { + if (++progress == duration) { + progress = 0; + } + } + } +} diff --git a/src/main/resources/assets/modularui/lang/en_US.lang b/src/main/resources/assets/modularui/lang/en_US.lang new file mode 100644 index 0000000..8b3ea1c --- /dev/null +++ b/src/main/resources/assets/modularui/lang/en_US.lang @@ -0,0 +1,14 @@ +modularui.test=Test Text\n2. Line +modularui.tooltip.shift=§bHold Shift +modularui.increment.tooltip=§7Click to increase the number by §6%d§7\nHold shift[§6%d§7], ctrl[§6%d§7] or shift+ctrl[§6%d§7] +modularui.decrement.tooltip=§7Click to decrease the number by §6%d§7\nHold shift[§6%d§7], ctrl[§6%d§7] or shift+ctrl[§6%d§7] +modularui.amount=§9Amount: %,d +modularui.fluid.empty=Empty +modularui.fluid.amount=§9Amount: %,d/%,d L +modularui.fluid.phantom.amount=§9Amount: %,d L +modularui.fluid.phantom.control=§7Scroll wheel up increases amount, down decreases.\nShift[§6x10§7],Ctrl[§ex100§7],Shift+Ctrl[§ax1000§7]\nRight click increases amount, left click decreases.\nShift + left click to clear. +modularui.item.phantom.control=§7Scroll wheel up increases amount, down decreases.\n§7Shift[§6x10§7],Ctrl[§ex100§7],Shift+Ctrl[§ax1000§7]\n§7Right click increases amount, left click decreases.\n§7Shift + left click to clear. + +modularui.fluid.click_to_fill=§bLeft §7Click with a Fluid Container to §bfill §7the tank (Shift-click for a full stack). +modularui.fluid.click_combined=§cRight §7or §bLeft §7Click with a Fluid Container to §cempty §7or §bfill §7the tank (Shift-click for a full stack). +modularui.fluid.click_to_empty=§cRight §7Click with a Fluid Container to §cempty §7the tank (Shift-click for a full stack). \ No newline at end of file diff --git a/src/main/resources/assets/modularui/textures/gui/background/background.png b/src/main/resources/assets/modularui/textures/gui/background/background.png new file mode 100644 index 0000000..6cd28d5 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/background/background.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/background/display.png b/src/main/resources/assets/modularui/textures/gui/background/display.png new file mode 100644 index 0000000..1a36ea4 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/background/display.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/background/vanilla_background.png b/src/main/resources/assets/modularui/textures/gui/background/vanilla_background.png new file mode 100644 index 0000000..f77849e Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/background/vanilla_background.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/icons/arrow_down.png b/src/main/resources/assets/modularui/textures/gui/icons/arrow_down.png new file mode 100644 index 0000000..4dde7fb Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/icons/arrow_down.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/icons/arrow_left.png b/src/main/resources/assets/modularui/textures/gui/icons/arrow_left.png new file mode 100644 index 0000000..7150d0f Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/icons/arrow_left.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/icons/arrow_right.png b/src/main/resources/assets/modularui/textures/gui/icons/arrow_right.png new file mode 100644 index 0000000..96653c8 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/icons/arrow_right.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/icons/arrow_up.png b/src/main/resources/assets/modularui/textures/gui/icons/arrow_up.png new file mode 100644 index 0000000..57f92d9 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/icons/arrow_up.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/icons/cross.png b/src/main/resources/assets/modularui/textures/gui/icons/cross.png new file mode 100644 index 0000000..6b97136 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/icons/cross.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/slot/fluid.png b/src/main/resources/assets/modularui/textures/gui/slot/fluid.png new file mode 100644 index 0000000..ea7b041 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/slot/fluid.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/slot/fluid_tank.png b/src/main/resources/assets/modularui/textures/gui/slot/fluid_tank.png new file mode 100644 index 0000000..f70c1e7 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/slot/fluid_tank.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/slot/item.png b/src/main/resources/assets/modularui/textures/gui/slot/item.png new file mode 100644 index 0000000..7e29f15 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/slot/item.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/tab/tabs_bottom.png b/src/main/resources/assets/modularui/textures/gui/tab/tabs_bottom.png new file mode 100644 index 0000000..b4cee09 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/tab/tabs_bottom.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/tab/tabs_left.png b/src/main/resources/assets/modularui/textures/gui/tab/tabs_left.png new file mode 100644 index 0000000..3e83347 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/tab/tabs_left.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/tab/tabs_right.png b/src/main/resources/assets/modularui/textures/gui/tab/tabs_right.png new file mode 100644 index 0000000..8dfef9e Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/tab/tabs_right.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/tab/tabs_top.png b/src/main/resources/assets/modularui/textures/gui/tab/tabs_top.png new file mode 100644 index 0000000..18481f4 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/tab/tabs_top.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/widgets/base_button.png b/src/main/resources/assets/modularui/textures/gui/widgets/base_button.png new file mode 100644 index 0000000..336f63d Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/widgets/base_button.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/widgets/cycle_button_demo.png b/src/main/resources/assets/modularui/textures/gui/widgets/cycle_button_demo.png new file mode 100644 index 0000000..86f6a27 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/widgets/cycle_button_demo.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/widgets/information.png b/src/main/resources/assets/modularui/textures/gui/widgets/information.png new file mode 100644 index 0000000..01281dd Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/widgets/information.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/widgets/progress_bar_arrow.png b/src/main/resources/assets/modularui/textures/gui/widgets/progress_bar_arrow.png new file mode 100644 index 0000000..2fdb582 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/widgets/progress_bar_arrow.png differ diff --git a/src/main/resources/assets/modularui/textures/gui/widgets/progress_bar_mixer.png b/src/main/resources/assets/modularui/textures/gui/widgets/progress_bar_mixer.png new file mode 100644 index 0000000..61536db Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/widgets/progress_bar_mixer.png differ diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info new file mode 100644 index 0000000..5c6f3f8 --- /dev/null +++ b/src/main/resources/mcmod.info @@ -0,0 +1,21 @@ +{ + "modListVersion": 2, + "modList": [{ + "modid": "${modId}", + "name": "${modName}", + "description": "WIP", + "version": "${modVersion}", + "mcversion": "${minecraftVersion}", + "url": "none", + "updateUrl": "", + "authorList": ["Quarri6343, miozune"], + "credits": "", + "logoFile": "", + "screenshots": [], + "parent": "", + "requiredMods": [], + "dependencies": [], + "dependants": [], + "useDependencyInformation": true + }] +}