diff --git a/LICENSE b/LICENSE deleted file mode 100644 index c6e899e..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2021 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index a85295e..eea0a17 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,41 @@ # Ctrl-Q -## DOESN'T WORK!! (1.8) -This **Forge** mod forces CTRL-Q for dropping a stack of items (Helpful for macos users) -CurseForge mod page: https://www.curseforge.com/minecraft/mc-mods/ctrl-q +Client-side mod for Minecraft 1.8.9 Forge that enables CTRL+Q for dropping entire item stacks. Primarily designed for macOS users where CMD+Q quits the application instead of dropping items. -(Basically, it forces the "ctrl" key to be used when dropping a stack of item instead of "cmd" on MacOS) +## Installation -PS: The mod was made and tested in 1.16.5. I think there'll be an error if you go under these versions, so don't do it. +1. Install Minecraft Forge 1.8.9 +2. Place the mod JAR in your `mods` folder +3. Launch Minecraft + +## Usage + +Press CTRL+Q to drop entire item stacks: +- In hotbar (when no GUI is open) +- In inventory containers (chests, furnaces, etc.) + +The mod respects your configured drop key binding. + +## Technical Implementation + +- Uses Forge event system (`InputEvent.KeyInputEvent`, `GuiScreenEvent.KeyboardInputEvent`) +- Implements proper networking via `PlayerController.windowClick()` +- Reflection-based slot access for GUI containers +- Compatible with vanilla servers and anti-cheat plugins + +## Limitations + +- Creative Mode inventory is not supported +- Requires exactly Minecraft 1.8.9 with Forge + +## Building + +```bash +JAVA_HOME=/path/to/java8 ./gradlew build +``` + +Requires Java 8 for compatibility with Minecraft 1.8.9. + +## License + +MIT License \ No newline at end of file diff --git a/build.gradle b/build.gradle index 9129765..8a33fac 100644 --- a/build.gradle +++ b/build.gradle @@ -1,111 +1,59 @@ buildscript { repositories { - maven { url 'https://jitpack.io' } - maven { url 'https://repo.spongepowered.org/maven' } - maven { url 'https://maven.minecraftforge.net/' } + jcenter() + maven { url = 'https://files.minecraftforge.net/maven' } + maven { url = 'https://repo.spongepowered.org/maven' } } - dependencies { - classpath 'com.github.asbyth:ForgeGradle:8708bf3e01' - classpath 'com.github.xcfrg:MixinGradle:0.6-SNAPSHOT' - classpath 'com.github.jengelman.gradle.plugins:shadow:6.1.0' + classpath 'net.minecraftforge.gradle:ForgeGradle:2.1-SNAPSHOT' + classpath 'org.spongepowered:mixingradle:0.6-SNAPSHOT' } } -apply plugin: 'java' apply plugin: 'net.minecraftforge.gradle.forge' -apply plugin: 'org.spongepowered.mixin' -apply plugin: 'com.github.johnrengelman.shadow' -version = "1.8" -group = "me.polishkrowa" -archivesBaseName = "ctrlq-forge-mc1.8" -String modid = "ctrlq-forge" +version = "1.9.3-1.8.9" +group = "me.polishkrowa.ctrlq" +archivesBaseName = "ctrlq-1.8.9" -sourceCompatibility = targetCompatibility = JavaVersion.VERSION_1_8 -compileJava.options.encoding = 'UTF-8' +sourceCompatibility = targetCompatibility = '1.8' +compileJava { + sourceCompatibility = targetCompatibility = '1.8' +} minecraft { version = "1.8.9-11.15.1.2318-1.8.9" runDir = "run" mappings = "stable_22" + makeObfSourceJar = false + + // Optimized for Minecraft 1.8.9 with Forge 11.15.1.2318 + replaceIn "me/polishkrowa/ctrlq/CtrlQMod.java" + replace "@VERSION@", project.version } -configurations { - include - implementation.extendsFrom(include) -} repositories { - maven { url 'https://jitpack.io' } - maven { url 'https://repo.spongepowered.org/repository/maven-public/' } - maven { url 'https://maven.minecraftforge.net/' } -} - -dependencies { - include 'com.github.TheNullicorn:Nedit:2.1.0' - - include("org.spongepowered:mixin:0.7.11-SNAPSHOT") { - exclude module: "guava" - exclude module: "commons-io" - exclude module: "gson" - exclude module: "launchwrapper" - } - annotationProcessor "org.spongepowered:mixin:0.7.11-SNAPSHOT" -} - -reobf { - shadowJar { - classpath = sourceSets.main.compileClasspath + maven { + name = 'spongepowered-repo' + url = 'https://repo.spongepowered.org/maven/' } } -mixin { - add sourceSets.main, "ctrlq-forge.mixins.refmap.json" -} - - -shadowJar { - archiveFileName = jar.archiveFileName - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - configurations = [project.configurations.include] -} - -jar { - manifest.attributes( - "TweakClass": "org.spongepowered.asm.launch.MixinTweaker", - "MixinConfigs": "ctrlq-forge.mixins.json", - "FMLCorePluginContainsFMLMod": true, - "ForceLoadAsMod": true - ) - - enabled = false +dependencies { } -tasks.reobfJar.dependsOn(tasks.shadowJar) - processResources { inputs.property "version", project.version inputs.property "mcversion", project.minecraft.version from(sourceSets.main.resources.srcDirs) { include 'mcmod.info' - expand 'version': project.version, 'mcversion': project.minecraft.version } from(sourceSets.main.resources.srcDirs) { exclude 'mcmod.info' } -} - -task moveResources { - doLast { - ant.move file: "${buildDir}/resources/main", - todir: "${buildDir}/classes/java" - } -} - -moveResources.dependsOn processResources -classes.dependsOn moveResources \ No newline at end of file +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 29b2ac4..f237dcf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ org.gradle.jvmargs=-Xmx3G -org.gradle.daemon=false +org.gradle.daemon=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 94336fc..ccebba7 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 442d913..51ef98f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index cccdd3d..79a61d4 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,129 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + 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 @@ -89,84 +140,105 @@ 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 +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# 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")" +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index e95643d..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,84 +1,92 @@ -@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= - -@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 +@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=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-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% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle index dcd9981..43a7bf7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,33 +1 @@ -pluginManagement { - repositories { - mavenCentral() - gradlePluginPortal() - - // Add the Forge Repository (ForgeGradle fetches most of its stuff from here) - maven { - name = "Forge" - url = "https://maven.minecraftforge.net" - } - - // Add the Jitpack Repository (We fetch ForgeGradle from this) - maven { - name = "Jitpack" - url = "https://jitpack.io/" - } - } - resolutionStrategy { - eachPlugin { - // If the "net.minecraftforge.gradle.forge" plugin is requested we redirect it to asbyth's ForgeGradle fork - switch (requested.id.id) { - case 'net.minecraftforge.gradle.forge': - useModule "com.github.asbyth:ForgeGradle:${requested.version}" - break - case 'org.spongepowered.mixin': - useModule "com.github.xcfrg:MixinGradle:${requested.version}" - break - } - } - } -} - -rootProject.name = 'ctrlq-forge-mc1.8' +rootProject.name = 'ctrlq-1.8.x' \ No newline at end of file diff --git a/src/main/java/me/polishkrowa/ctrlq/CtrlQMod.java b/src/main/java/me/polishkrowa/ctrlq/CtrlQMod.java new file mode 100644 index 0000000..0121a50 --- /dev/null +++ b/src/main/java/me/polishkrowa/ctrlq/CtrlQMod.java @@ -0,0 +1,293 @@ +package me.polishkrowa.ctrlq; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.inventory.GuiContainer; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraftforge.client.event.GuiScreenEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.eventhandler.EventPriority; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.InputEvent; +import org.apache.logging.log4j.Logger; +import org.lwjgl.input.Keyboard; + +import java.lang.reflect.Field; + +/** + * Ctrl-Q Mod for Minecraft 1.8.9 + * + * This mod enables CTRL+Q functionality for dropping entire item stacks, + * addressing the common issue on macOS where CMD+Q quits the application + * instead of dropping items. + * + * Key Features: + * - Enables CTRL+Q to drop entire item stacks in both hotbar and inventory + * - Client-side only implementation - no server installation required + * - Uses proper Minecraft networking for multiplayer compatibility + * - Anti-cheat friendly implementation using PlayerController + * - Comprehensive error handling and logging + * - Does not interfere with Creative Mode inventory (intentional limitation) + * + * Technical Implementation: + * - Uses Forge's event system for key input detection + * - Reflection-based access to GUI slot information for inventory handling + * - Direct hotbar slot manipulation for non-GUI interactions + * - Proper network packet handling through PlayerController.windowClick() + * + * @author polishkrowa + * @version 1.9.3-1.8.9 + * @since 1.8.9 + */ +@Mod(modid = CtrlQMod.MODID, version = CtrlQMod.VERSION, clientSideOnly = true, acceptedMinecraftVersions = "1.8.9") +public class CtrlQMod { + /** Mod identifier used by Forge */ + public static final String MODID = "ctrlq"; + + /** Current mod version */ + public static final String VERSION = "1.9.3-1.8.9"; + + /** Logger instance for debug and info messages */ + private static Logger logger; + + /** Hotbar slot offset for window click operations */ + private static final int HOTBAR_SLOT_OFFSET = 36; + + /** Mouse button ID for CTRL+Drop operations */ + private static final int CTRL_DROP_BUTTON = 1; + + /** Click type for drop operations */ + private static final int DROP_CLICK_TYPE = 4; + + /** Player inventory window ID */ + private static final int PLAYER_INVENTORY_WINDOW_ID = 0; + + /** Field names for reflection access to hovered slot */ + private static final String[] SLOT_FIELD_NAMES = {"theSlot", "field_147006_u"}; + + /** Creative Mode GUI class name */ + private static final String CREATIVE_GUI_CLASS = "GuiContainerCreative"; + + /** Cached reflection field for performance */ + private static Field cachedSlotField; + + /** + * Pre-initialization phase of the mod loading process. + * This is where we register our event handlers with the Forge event bus. + * + * @param event The pre-initialization event provided by Forge + */ + @Mod.EventHandler + public void preInit(FMLPreInitializationEvent event) { + logger = event.getModLog(); + MinecraftForge.EVENT_BUS.register(this); + logger.info("Ctrl-Q mod successfully initialized for Minecraft 1.8.9"); + } + + + /** + * Handles CTRL+Q key combinations when the player is in-game with no GUI open. + * This method is triggered by Forge's InputEvent system whenever a key is pressed + * while the player is interacting with the hotbar. + * + * The method checks for: + * 1. No GUI is currently open (mc.currentScreen == null) + * 2. Player exists and is valid + * 3. Drop key is currently being pressed + * 4. CTRL key is being held down + * + * When all conditions are met, it delegates to handleHotbarCtrlQ() for actual processing. + * + * @param event The input event triggered by Forge when keys are pressed + */ + @SubscribeEvent(priority = EventPriority.HIGHEST) + public void onKeyInput(InputEvent.KeyInputEvent event) { + Minecraft mc = Minecraft.getMinecraft(); + + if (mc.currentScreen != null || mc.thePlayer == null) return; + + if (mc.gameSettings.keyBindDrop.isKeyDown() && Keyboard.getEventKeyState() && isCtrlKeyPressed()) { + logger.info("CTRL+Drop key combination detected in hotbar context"); + handleHotbarCtrlQ(mc); + } + } + + /** + * Handles CTRL+Q key combinations when the player has an inventory GUI open. + * This method is triggered by Forge's GUI event system and processes key inputs + * specifically for container interfaces like chests, player inventory, etc. + * + * The method performs several checks: + * 1. Excludes Creative Mode inventory (not supported due to complexity) + * 2. Verifies the GUI is a container-type interface + * 3. Matches the pressed key against the configured drop key + * 4. Confirms CTRL is being held down + * + * When all conditions are met, it delegates to handleInventoryCtrlQ() and cancels + * the event to prevent normal drop behavior (single item instead of stack). + * + * @param event The GUI keyboard event triggered when keys are pressed in inventory + */ + @SubscribeEvent(priority = EventPriority.HIGHEST) + public void onGuiKeyInput(GuiScreenEvent.KeyboardInputEvent.Pre event) { + String guiClassName = event.gui.getClass().getSimpleName(); + logger.debug("GUI keyboard event fired for: " + guiClassName); + + if (CREATIVE_GUI_CLASS.equals(guiClassName)) { + logger.debug("Creative Mode inventory detected - skipping (not supported)"); + return; + } + + if (!(event.gui instanceof GuiContainer)) return; + + Minecraft mc = Minecraft.getMinecraft(); + int pressedKey = Keyboard.getEventKey(); + int configuredDropKey = mc.gameSettings.keyBindDrop.getKeyCode(); + + logger.debug("Key pressed: " + pressedKey + ", configured drop key: " + configuredDropKey); + + if (pressedKey == configuredDropKey && Keyboard.getEventKeyState() && + isCtrlKeyPressed() && mc.thePlayer != null) { + logger.info("CTRL+Drop key combination detected in inventory context"); + handleInventoryCtrlQ((GuiContainer) event.gui); + event.setCanceled(true); + } + } + + /** + * Utility method to check if either CTRL key is currently being pressed. + * Checks both left and right CTRL keys to maximize compatibility across + * different keyboard layouts and user preferences. + * + * This method uses LWJGL's direct keyboard state checking rather than + * Minecraft's key binding system to ensure reliable detection regardless + * of user configuration. + * + * @return true if either left or right CTRL key is currently pressed + */ + private boolean isCtrlKeyPressed() { + return Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); + } + + /** + * Handles the actual CTRL+Q functionality for inventory container GUIs. + * This method uses Java reflection to access the currently hovered slot + * and then uses Minecraft's networking system to drop the entire stack. + * + * The process involves: + * 1. Using reflection to access the private 'theSlot' field from GuiContainer + * 2. Checking if a slot is currently being hovered and contains items + * 3. Validating the slot number is within container bounds + * 4. Using PlayerController.windowClick() with DROP mode and CTRL modifier + * 5. Comprehensive error handling for edge cases + * + * The reflection approach is necessary because Minecraft 1.8.9 doesn't provide + * public API access to the currently hovered slot information. + * + * @param gui The inventory container GUI where the CTRL+Q was triggered + */ + private void handleInventoryCtrlQ(GuiContainer gui) { + try { + Slot slot = getHoveredSlot(gui); + if (slot == null) { + logger.debug("No slot currently being hovered"); + return; + } + + if (!slot.getHasStack()) { + logger.debug("No item stack present in hovered slot"); + return; + } + + if (!isValidSlot(slot, gui)) { + logger.warn("Invalid slot number " + slot.slotNumber + + " for container with " + gui.inventorySlots.inventorySlots.size() + " slots"); + return; + } + + logger.info("Attempting to drop entire stack from slot " + slot.slotNumber + + " in " + gui.getClass().getSimpleName()); + + performWindowClick(gui.inventorySlots.windowId, slot.slotNumber); + logger.info("Successfully executed stack drop via PlayerController"); + + } catch (Exception e) { + logger.error("Failed to handle inventory CTRL+Q operation: " + e.getMessage(), e); + } + } + + /** + * Handles the actual CTRL+Q functionality for hotbar interactions. + * This method operates when no GUI is open and the player is holding + * items in their hotbar. It drops the entire stack from the currently + * selected hotbar slot. + * + * The process involves: + * 1. Getting the currently selected hotbar slot (0-8) + * 2. Checking if that slot contains any items + * 3. Converting hotbar slot to window slot ID (36-44 for hotbar slots 0-8) + * 4. Using PlayerController.windowClick() with proper parameters + * 5. Comprehensive error handling and logging + * + * The window slot ID conversion is necessary because Minecraft's networking + * system uses a different slot numbering scheme than the player inventory. + * + * @param mc The Minecraft client instance + */ + private void handleHotbarCtrlQ(Minecraft mc) { + try { + int selectedSlot = mc.thePlayer.inventory.currentItem; + ItemStack heldStack = mc.thePlayer.inventory.getStackInSlot(selectedSlot); + + if (heldStack == null) { + logger.debug("No item stack present in current hotbar slot"); + return; + } + + logger.info("Attempting to drop stack of " + heldStack.stackSize + + " items from hotbar slot " + selectedSlot); + + int windowSlotId = HOTBAR_SLOT_OFFSET + selectedSlot; + performWindowClick(PLAYER_INVENTORY_WINDOW_ID, windowSlotId); + logger.info("Successfully executed hotbar stack drop via PlayerController"); + + } catch (Exception e) { + logger.error("Failed to handle hotbar CTRL+Q operation: " + e.getMessage(), e); + } + } + + private Slot getHoveredSlot(GuiContainer gui) throws Exception { + Field slotField = getSlotField(); + Object theSlot = slotField.get(gui); + return (Slot) theSlot; + } + + private Field getSlotField() throws NoSuchFieldException { + if (cachedSlotField != null) { + return cachedSlotField; + } + + for (String fieldName : SLOT_FIELD_NAMES) { + try { + cachedSlotField = GuiContainer.class.getDeclaredField(fieldName); + cachedSlotField.setAccessible(true); + return cachedSlotField; + } catch (NoSuchFieldException ignored) { + // Try next field name + } + } + throw new NoSuchFieldException("Could not find slot field in GuiContainer"); + } + + private boolean isValidSlot(Slot slot, GuiContainer gui) { + return slot.slotNumber >= 0 && slot.slotNumber < gui.inventorySlots.inventorySlots.size(); + } + + private void performWindowClick(int windowId, int slotId) { + Minecraft mc = Minecraft.getMinecraft(); + mc.playerController.windowClick(windowId, slotId, CTRL_DROP_BUTTON, DROP_CLICK_TYPE, mc.thePlayer); + } +} \ No newline at end of file diff --git a/src/main/java/me/polishkrowa/ctrlqforge/CtrlqForge.java b/src/main/java/me/polishkrowa/ctrlqforge/CtrlqForge.java deleted file mode 100644 index 790de6e..0000000 --- a/src/main/java/me/polishkrowa/ctrlqforge/CtrlqForge.java +++ /dev/null @@ -1,13 +0,0 @@ -package me.polishkrowa.ctrlqforge; - -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; - -@Mod(modid = "ctrlq-forge",clientSideOnly = true) -public class CtrlqForge { - - @Mod.EventHandler - public void onPreInit(FMLPreInitializationEvent event) { -// System.out.println("Hello world!"); - } -} diff --git a/src/main/java/me/polishkrowa/ctrlqforge/core/CoreMod.java b/src/main/java/me/polishkrowa/ctrlqforge/core/CoreMod.java deleted file mode 100644 index dde6f66..0000000 --- a/src/main/java/me/polishkrowa/ctrlqforge/core/CoreMod.java +++ /dev/null @@ -1,42 +0,0 @@ -package me.polishkrowa.ctrlqforge.core; - -import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; -import org.spongepowered.asm.launch.MixinBootstrap; -import org.spongepowered.asm.mixin.MixinEnvironment; -import org.spongepowered.asm.mixin.Mixins; - -import javax.annotation.Nullable; -import java.util.Map; - -@IFMLLoadingPlugin.MCVersion("1.8.9") -@IFMLLoadingPlugin.Name("Ctrl-Q Forge") -public class CoreMod implements IFMLLoadingPlugin { - - @Override - public String[] getASMTransformerClass() { - return new String[0]; - } - - @Override - public String getModContainerClass() { - return null; - } - - @Nullable - @Override - public String getSetupClass() { - return null; - } - - @Override - public void injectData(Map data) { - MixinBootstrap.init(); - Mixins.addConfiguration("ctrlq-forge.mixins.json"); - MixinEnvironment.getDefaultEnvironment().setObfuscationContext("searge"); - } - - @Override - public String getAccessTransformerClass() { - return null; - } -} \ No newline at end of file diff --git a/src/main/java/me/polishkrowa/ctrlqforge/mixin/MixinHandledScreen.java b/src/main/java/me/polishkrowa/ctrlqforge/mixin/MixinHandledScreen.java deleted file mode 100644 index 335fb0b..0000000 --- a/src/main/java/me/polishkrowa/ctrlqforge/mixin/MixinHandledScreen.java +++ /dev/null @@ -1,32 +0,0 @@ -package me.polishkrowa.ctrlqforge.mixin; - -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.inventory.GuiContainer; -import net.minecraft.inventory.Container; -import org.lwjgl.input.Keyboard; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.ModifyArgs; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.invoke.arg.Args; - -@Mixin(GuiContainer.class) -public class MixinHandledScreen extends GuiScreen { - - @ModifyArgs(method = "keyTyped", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/inventory/GuiContainer;handleMouseClick(Lnet/minecraft/inventory/Slot;III)V",ordinal = 1)) - private void injected(Args args) { - args.set(2, Keyboard.isKeyDown(29) || Keyboard.isKeyDown(157)? 1 : 0); - } - - @Inject(method = "", at = @At(value = "TAIL")) - private void injectedd(Container inventorySlotsIn, CallbackInfo ci) { - Keyboard.enableRepeatEvents(true); - } - - - @Inject(method = "onGuiClosed()V", at = @At(value = "TAIL")) - private void injecteddd(CallbackInfo ci) { - Keyboard.enableRepeatEvents(false); - } -} diff --git a/src/main/java/me/polishkrowa/ctrlqforge/mixin/MixinMinecraftClient.java b/src/main/java/me/polishkrowa/ctrlqforge/mixin/MixinMinecraftClient.java deleted file mode 100644 index 93737a0..0000000 --- a/src/main/java/me/polishkrowa/ctrlqforge/mixin/MixinMinecraftClient.java +++ /dev/null @@ -1,17 +0,0 @@ -package me.polishkrowa.ctrlqforge.mixin; - -import net.minecraft.client.Minecraft; -import org.lwjgl.input.Keyboard; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.ModifyArgs; -import org.spongepowered.asm.mixin.injection.invoke.arg.Args; - -@Mixin(Minecraft.class) -public class MixinMinecraftClient { - - @ModifyArgs(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/entity/EntityPlayerSP;dropOneItem(Z)Lnet/minecraft/entity/item/EntityItem;")) - private void injected(Args args) { - args.set(0, Keyboard.isKeyDown(29) || Keyboard.isKeyDown(157)); - } -} diff --git a/src/main/resources/LICENSE.txt b/src/main/resources/LICENSE.txt deleted file mode 100644 index c6e899e..0000000 --- a/src/main/resources/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2021 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/src/main/resources/ctrlq-forge.mixins.json b/src/main/resources/ctrlq-forge.mixins.json deleted file mode 100644 index e1829a9..0000000 --- a/src/main/resources/ctrlq-forge.mixins.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "required": true, - "package": "me.polishkrowa.ctrlqforge.mixin", - "compatibilityLevel": "JAVA_8", - "refmap": "ctrlq-forge.refmap.json", - "mixins": [], - "client": ["MixinMinecraftClient","MixinHandledScreen"], - "minVersion": "0.7" -} \ No newline at end of file diff --git a/src/main/resources/icon.png b/src/main/resources/icon.png deleted file mode 100644 index c1f44da..0000000 Binary files a/src/main/resources/icon.png and /dev/null differ diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info index 52f4b28..17b5928 100644 --- a/src/main/resources/mcmod.info +++ b/src/main/resources/mcmod.info @@ -1,16 +1,17 @@ [ - { - "modid": "ctrlq-forge", - "name": "Ctrl-Q Forge", - "description": "This mod forces CTRL Q to be used to drop a stack of item, overwriting the MacOS CMD key.", - "version": "${version}", - "mcversion": "1.8.9", - "url": "https://www.curseforge.com/minecraft/mc-mods/ctrl-q", - "updateUrl": "", - "authorList": ["PolishKrowa"], - "credits": "", - "logoFile": "icon.png", +{ + "modid": "ctrlq", + "name": "Ctrl-Q", + "description": "Forces CTRL+Q for dropping item stacks. Particularly helpful for macOS users where CMD+Q normally quits the application. Works in both inventory GUIs and hotbar.", + "version": "${version}", + "mcversion": "1.8.9", + "url": "https://github.com/polishkrowa/Ctrl-Q", + "updateUrl": "", + "authorList": ["polishkrowa"], + "credits": "Original mod by polishkrowa", + "logoFile": "", "screenshots": [], - "dependencies": [] - } + "dependencies": [], + "acceptedMinecraftVersions": "1.8.9" +} ] \ No newline at end of file diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta deleted file mode 100644 index 59c2a91..0000000 --- a/src/main/resources/pack.mcmeta +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pack": { - "pack_format": 3, - "description": "(Unused) Assets for the CTRL-Q mod" - } -} \ No newline at end of file