aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore22
-rw-r--r--README.md10
-rw-r--r--build.gradle85
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 52271 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rw-r--r--gradlew164
-rw-r--r--gradlew.bat90
-rw-r--r--src/main/java/eu/olli/cowmoonication/Cowmoonication.java63
-rw-r--r--src/main/java/eu/olli/cowmoonication/Friends.java55
-rw-r--r--src/main/java/eu/olli/cowmoonication/Utils.java57
-rw-r--r--src/main/java/eu/olli/cowmoonication/command/MooCommand.java115
-rw-r--r--src/main/java/eu/olli/cowmoonication/config/MooConfig.java113
-rw-r--r--src/main/java/eu/olli/cowmoonication/config/MooGuiConfig.java39
-rw-r--r--src/main/java/eu/olli/cowmoonication/config/MooGuiFactory.java29
-rw-r--r--src/main/java/eu/olli/cowmoonication/listener/ChatListener.java124
-rw-r--r--src/main/resources/assets/cowmoonication/lang/en_us.lang1
-rw-r--r--src/main/resources/mcmod.info13
17 files changed, 985 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2c770e0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+# eclipse
+bin
+*.launch
+.settings
+.metadata
+.classpath
+.project
+
+# idea
+out
+*.ipr
+*.iws
+*.iml
+.idea
+
+# gradle
+build
+.gradle
+
+# other
+eclipse
+run
diff --git a/README.md b/README.md
index 9e2dee8..e279e57 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,10 @@
# Cowmoonication
-A client-side only Forge mod by providing various things related to communication.
+A client-side only Forge mod by [Cow](https://namemc.com/profile/Cow) providing various things related to communication.
+
+Completed features:
+*Base command is `/moo`*
+* Toggle to hide all join/leave notifications
+* 'Best friends' list to limit the amount of join and leave notifications
+* Auto-replace `/r` with `/msg <latest username>`
+* Copy chat components via <kbd>ALT</kbd> + <kbd>right click</kbd>
+* Change guiScale to any value
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..3d002c6
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,85 @@
+
+// For those who want the bleeding edge
+buildscript {
+ repositories {
+ jcenter()
+ maven {
+ name = "forge"
+ url = "http://files.minecraftforge.net/maven"
+ }
+ }
+ dependencies {
+ classpath 'net.minecraftforge.gradle:ForgeGradle:2.1-SNAPSHOT'
+ }
+}
+apply plugin: 'net.minecraftforge.gradle.forge'
+
+/*
+// for people who want stable - not yet functional for MC 1.8.8 - we require the forgegradle 2.1 snapshot
+plugins {
+ id "net.minecraftforge.gradle.forge" version "2.0.2"
+}
+*/
+version = "0.1"
+group= "eu.olli.cowmoonication" // http://maven.apache.org/guides/mini/guide-naming-conventions.html
+archivesBaseName = "Cowmoonication"
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+minecraft {
+ version = "1.8.9-11.15.1.1722"
+ runDir = "run"
+
+ // the mappings can be changed at any time, and must be in the following format.
+ // snapshot_YYYYMMDD snapshot are built nightly.
+ // stable_# stables are built at the discretion of the MCP team.
+ // Use non-default mappings at your own risk. they may not allways work.
+ // simply re-run your setup task after changing the mappings to update your workspace.
+ mappings = "stable_20"
+ // makeObfSourceJar = false // an Srg named sources jar is made by default. uncomment this to disable.
+}
+
+dependencies {
+ // you may put jars on which you depend on in ./libs
+ // or you may define them like so..
+ //compile "some.group:artifact:version:classifier"
+ //compile "some.group:artifact:version"
+
+ // real examples
+ //compile 'com.mod-buildcraft:buildcraft:6.0.8:dev' // adds buildcraft to the dev env
+ //compile 'com.googlecode.efficient-java-matrix-library:ejml:0.24' // adds ejml to the dev env
+
+ // the 'provided' configuration is for optional dependencies that exist at compile-time but might not at runtime.
+ //provided 'com.mod-buildcraft:buildcraft:6.0.8:dev'
+
+ // the deobf configurations: 'deobfCompile' and 'deobfProvided' are the same as the normal compile and provided,
+ // except that these dependencies get remapped to your current MCP mappings
+ //deobfCompile 'com.mod-buildcraft:buildcraft:6.0.8:dev'
+ //deobfProvided 'com.mod-buildcraft:buildcraft:6.0.8:dev'
+
+ // for more info...
+ // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html
+ // http://www.gradle.org/docs/current/userguide/dependency_management.html
+
+}
+
+processResources
+{
+ // this will ensure that this task is redone when the versions change.
+ inputs.property "version", project.version
+ inputs.property "mcversion", project.minecraft.version
+
+ // replace stuff in mcmod.info, nothing else
+ from(sourceSets.main.resources.srcDirs) {
+ include 'mcmod.info'
+
+ // replace version and mcversion
+ expand 'version':project.version, 'mcversion':project.minecraft.version
+ }
+
+ // copy everything else, thats not the mcmod.info
+ from(sourceSets.main.resources.srcDirs) {
+ exclude 'mcmod.info'
+ }
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..30d399d
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..fb87029
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Feb 26 22:53:05 CET 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..91a7e26
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# 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
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# 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\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+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" ] ; 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, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # 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
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@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
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@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 Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_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=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+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/src/main/java/eu/olli/cowmoonication/Cowmoonication.java b/src/main/java/eu/olli/cowmoonication/Cowmoonication.java
new file mode 100644
index 0000000..d07876c
--- /dev/null
+++ b/src/main/java/eu/olli/cowmoonication/Cowmoonication.java
@@ -0,0 +1,63 @@
+package eu.olli.cowmoonication;
+
+import eu.olli.cowmoonication.command.MooCommand;
+import eu.olli.cowmoonication.config.MooConfig;
+import eu.olli.cowmoonication.listener.ChatListener;
+import net.minecraftforge.client.ClientCommandHandler;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.common.config.Configuration;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.Mod.EventHandler;
+import net.minecraftforge.fml.common.event.FMLInitializationEvent;
+import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
+import org.apache.logging.log4j.Logger;
+
+import java.io.File;
+
+@Mod(modid = Cowmoonication.MODID, version = Cowmoonication.VERSION, clientSideOnly = true, guiFactory = "eu.olli." + Cowmoonication.MODID + ".config.MooGuiFactory")
+public class Cowmoonication {
+ public static final String MODID = "cowmoonication";
+ public static final String VERSION = "1.0";
+ private MooConfig config;
+ private Friends friends;
+ private Utils utils;
+ private Logger logger;
+
+ @Mod.EventHandler
+ public void preInit(FMLPreInitializationEvent e) {
+ logger = e.getModLog();
+
+ File modDir = new File(e.getModConfigurationDirectory(), MODID + File.separatorChar);
+ if (!modDir.exists()) {
+ modDir.mkdirs();
+ }
+
+ friends = new Friends(this);
+ config = new MooConfig(new Configuration(new File(modDir, MODID + ".cfg")), this);
+ }
+
+ @EventHandler
+ public void init(FMLInitializationEvent e) {
+ utils = new Utils(this);
+
+ MinecraftForge.EVENT_BUS.register(new ChatListener(this));
+ ClientCommandHandler.instance.registerCommand(new MooCommand(this));
+ }
+
+ public MooConfig getConfig() {
+ return config;
+ }
+
+ public Friends getFriends() {
+ return friends;
+ }
+
+ public Utils getUtils() {
+ return utils;
+ }
+
+ public Logger getLogger() {
+ return logger;
+ }
+
+}
diff --git a/src/main/java/eu/olli/cowmoonication/Friends.java b/src/main/java/eu/olli/cowmoonication/Friends.java
new file mode 100644
index 0000000..0cbe517
--- /dev/null
+++ b/src/main/java/eu/olli/cowmoonication/Friends.java
@@ -0,0 +1,55 @@
+package eu.olli.cowmoonication;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.TreeSet;
+
+public class Friends {
+ private final Cowmoonication main;
+ private Set<String> bestFriends = new HashSet<>();
+
+ public Friends(Cowmoonication main) {
+ this.main = main;
+ }
+
+ public boolean addBestFriend(String name, boolean save) {
+ if (name.isEmpty()) {
+ return false;
+ }
+ boolean added = bestFriends.add(name);
+ if (added && save) {
+ saveBestFriends();
+ }
+ return added;
+ }
+
+ public boolean addBestFriend(String name) {
+ return addBestFriend(name, false);
+ }
+
+ public boolean removeBestFriend(String name) {
+ boolean removed = bestFriends.remove(name);
+ if (removed) {
+ saveBestFriends();
+ }
+ return removed;
+ }
+
+ public boolean isBestFriend(String playerName) {
+ return bestFriends.contains(playerName);
+ }
+
+ private void saveBestFriends() {
+
+ }
+
+ public Set<String> getBestFriends() {
+ return new TreeSet<>(bestFriends);
+ }
+
+ public void syncFriends(String[] bestFriends) {
+ this.bestFriends = new HashSet<>();
+ Collections.addAll(this.bestFriends, bestFriends);
+ }
+}
diff --git a/src/main/java/eu/olli/cowmoonication/Utils.java b/src/main/java/eu/olli/cowmoonication/Utils.java
new file mode 100644
index 0000000..ac3167c
--- /dev/null
+++ b/src/main/java/eu/olli/cowmoonication/Utils.java
@@ -0,0 +1,57 @@
+package eu.olli.cowmoonication;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.util.ChatComponentText;
+import net.minecraft.util.IChatComponent;
+import net.minecraftforge.client.event.ClientChatReceivedEvent;
+import net.minecraftforge.common.MinecraftForge;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Utils {
+ public static final Pattern VALID_USERNAME = Pattern.compile("^[\\w]{1,16}$");
+ private static final Pattern USELESS_JSON_CONTENT_PATTERN = Pattern.compile("\"[A-Za-z]+\":false,?");
+ private final Cowmoonication main;
+ private String[] aboveChatMessage;
+ private long aboveChatMessageExpiration;
+
+ public Utils(Cowmoonication main) {
+ this.main = main;
+ }
+
+ public void sendMessage(String text) {
+ sendMessage(new ChatComponentText(text));
+ }
+
+ public void sendMessage(IChatComponent chatComponent) {
+ ClientChatReceivedEvent event = new ClientChatReceivedEvent((byte) 1, chatComponent);
+ MinecraftForge.EVENT_BUS.post(event);
+ if (!event.isCanceled()) {
+ Minecraft.getMinecraft().thePlayer.addChatMessage(event.message);
+ }
+ }
+
+ public void sendAboveChatMessage(String... text) {
+ aboveChatMessage = text;
+ aboveChatMessageExpiration = Minecraft.getSystemTime() + 5000;
+ }
+
+ public String[] getAboveChatMessage() {
+ if (aboveChatMessageExpiration < Minecraft.getSystemTime()) {
+ // message expired
+ aboveChatMessage = null;
+ }
+ return aboveChatMessage;
+ }
+
+ public boolean isValidMcName(String username) {
+ return VALID_USERNAME.matcher(username).matches();
+ }
+
+ public String cleanChatComponent(IChatComponent chatComponent) {
+ String component = IChatComponent.Serializer.componentToJson(chatComponent);
+ Matcher jsonMatcher = USELESS_JSON_CONTENT_PATTERN.matcher(component);
+ return jsonMatcher.replaceAll("");
+ }
+}
diff --git a/src/main/java/eu/olli/cowmoonication/command/MooCommand.java b/src/main/java/eu/olli/cowmoonication/command/MooCommand.java
new file mode 100644
index 0000000..ca4722c
--- /dev/null
+++ b/src/main/java/eu/olli/cowmoonication/command/MooCommand.java
@@ -0,0 +1,115 @@
+package eu.olli.cowmoonication.command;
+
+import eu.olli.cowmoonication.Cowmoonication;
+import eu.olli.cowmoonication.config.MooConfig;
+import net.minecraft.client.Minecraft;
+import net.minecraft.command.CommandBase;
+import net.minecraft.command.CommandException;
+import net.minecraft.command.ICommandSender;
+import net.minecraft.util.BlockPos;
+import net.minecraft.util.ChatComponentTranslation;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.MathHelper;
+
+import java.util.List;
+import java.util.Set;
+
+public class MooCommand extends CommandBase {
+ private final Cowmoonication main;
+
+ public MooCommand(Cowmoonication main) {
+ this.main = main;
+ }
+
+ @Override
+ public void processCommand(ICommandSender sender, String[] args) throws CommandException {
+ if (args.length == 0) {
+ main.getUtils().sendMessage(new ChatComponentTranslation(getCommandUsage(sender)));
+ return;
+ }
+ if (args.length == 2 && args[0].equalsIgnoreCase("add")) {
+ main.getUtils().sendMessage(EnumChatFormatting.RED + "Edit the best friends list via ESC > Mod Options > Cowmoonication > Config > bestFriends.");
+ // TODO replace with a proper command
+ // handleBestFriendAdd(args[1]);
+ } else if (args.length == 2 && args[0].equalsIgnoreCase("remove") && main.getUtils().isValidMcName(args[1])) {
+ main.getUtils().sendMessage(EnumChatFormatting.RED + "Edit the best friends list via ESC > Mod Options > Cowmoonication > Config > bestFriends.");
+ // TODO replace with a proper command
+ // handleBestFriendRemove(args[1]);
+ } else if (args[0].equalsIgnoreCase("list")) {
+ handleListBestFriends();
+ } else if (args[0].equalsIgnoreCase("toggle")) {
+ main.getConfig().toggleNotifications();
+ main.getUtils().sendMessage(EnumChatFormatting.GREEN + "Switched all non-best friend login/logout notifications " + (MooConfig.filterFriendNotifications ? EnumChatFormatting.DARK_GREEN + "off" : EnumChatFormatting.DARK_RED + "on"));
+ } else if (args[0].equalsIgnoreCase("guiscale")) {
+ int currentGuiScale = (Minecraft.getMinecraft()).gameSettings.guiScale;
+ if (args.length == 1) {
+ main.getUtils().sendMessage(EnumChatFormatting.GREEN + "Current GUI scale: " + EnumChatFormatting.DARK_GREEN + currentGuiScale);
+ } else {
+ int scale = Math.min(10, MathHelper.parseIntWithDefault(args[1], 6));
+ Minecraft.getMinecraft().gameSettings.guiScale = scale;
+ main.getUtils().sendMessage(EnumChatFormatting.GREEN + "New GUI scale: " + EnumChatFormatting.DARK_GREEN + scale + EnumChatFormatting.GREEN + " (previous: " + EnumChatFormatting.DARK_GREEN + currentGuiScale + EnumChatFormatting.GREEN + ")");
+ }
+ } else {
+ main.getUtils().sendMessage(new ChatComponentTranslation(getCommandUsage(sender)));
+ }
+ }
+
+ private void handleBestFriendAdd(String username) {
+ if (!main.getUtils().isValidMcName(username)) {
+ main.getUtils().sendMessage(EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + "? This... doesn't look like a valid username.");
+ return;
+ }
+
+ // TODO Add check if 'best friend' is on normal friend list
+ boolean added = main.getFriends().addBestFriend(username, true);
+ if (added) {
+ main.getUtils().sendMessage(EnumChatFormatting.GREEN + "Added " + EnumChatFormatting.DARK_GREEN + username + EnumChatFormatting.GREEN + " as best friend.");
+ } else {
+ main.getUtils().sendMessage(EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + " is a best friend already.");
+ }
+ }
+
+ private void handleBestFriendRemove(String username) {
+ if (!main.getUtils().isValidMcName(username)) {
+ main.getUtils().sendMessage(EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + "? This... doesn't look like a valid username.");
+ return;
+ }
+
+ boolean removed = main.getFriends().removeBestFriend(username);
+ if (removed) {
+ main.getUtils().sendMessage(EnumChatFormatting.GREEN + "Removed " + EnumChatFormatting.DARK_GREEN + username + EnumChatFormatting.GREEN + " from best friends list.");
+ } else {
+ main.getUtils().sendMessage(EnumChatFormatting.DARK_RED + username + EnumChatFormatting.RED + " isn't a best friend.");
+ }
+ }
+
+ private void handleListBestFriends() {
+ Set<String> bestFriends = main.getFriends().getBestFriends();
+
+ // TODO show fancy gui with list of best friends (maybe just the mod's settings?)
+ main.getUtils().sendMessage(EnumChatFormatting.GREEN + "Best friends: " + String.join(", ", bestFriends));
+ }
+
+ @Override
+ public String getCommandName() {
+ return "moo";
+ }
+
+ @Override
+ public String getCommandUsage(ICommandSender sender) {
+ return Cowmoonication.MODID + ":command.moo.usage";
+ }
+
+ @Override
+ public int getRequiredPermissionLevel() {
+ return 0;
+ }
+
+ @Override
+ public List<String> addTabCompletionOptions(ICommandSender sender, String[] args, BlockPos pos) {
+ if (args.length == 1) {
+ return getListOfStringsMatchingLastWord(args, "add", "remove", "list", "toggle", "guiscale");
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/eu/olli/cowmoonication/config/MooConfig.java b/src/main/java/eu/olli/cowmoonication/config/MooConfig.java
new file mode 100644
index 0000000..6165e54
--- /dev/null
+++ b/src/main/java/eu/olli/cowmoonication/config/MooConfig.java
@@ -0,0 +1,113 @@
+package eu.olli.cowmoonication.config;
+
+import eu.olli.cowmoonication.Cowmoonication;
+import eu.olli.cowmoonication.Utils;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.common.config.Configuration;
+import net.minecraftforge.common.config.Property;
+import net.minecraftforge.fml.client.event.ConfigChangedEvent;
+import net.minecraftforge.fml.common.eventhandler.EventPriority;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MooConfig {
+ public static boolean filterFriendNotifications;
+ private static String[] bestFriends;
+ private static Configuration cfg = null;
+ private final Cowmoonication main;
+
+ public MooConfig(Configuration configuration, Cowmoonication main) {
+ this.main = main;
+ cfg = configuration;
+ initConfig();
+ }
+
+ public static Configuration getConfig() {
+ return cfg;
+ }
+
+ private void initConfig() {
+ syncFromFile();
+ main.getFriends().syncFriends(bestFriends);
+ MinecraftForge.EVENT_BUS.register(new ConfigEventHandler());
+ }
+
+ /**
+ * Load the configuration values from the configuration file
+ */
+ private void syncFromFile() {
+ syncConfig(true, true);
+ }
+
+ /**
+ * Save the GUI-altered values to disk
+ */
+ private void syncFromGUI() {
+ syncConfig(false, true);
+ }
+
+ /**
+ * Save the Configuration variables (fields) to disk
+ */
+ private void syncFromFields() {
+ syncConfig(false, false);
+ }
+
+ /**
+ * Synchronise the three copies of the data
+ * 1) loadConfigFromFile && readFieldsFromConfig -> initialise everything from the disk file
+ * 2) !loadConfigFromFile && readFieldsFromConfig -> copy everything from the config file (altered by GUI)
+ * 3) !loadConfigFromFile && !readFieldsFromConfig -> copy everything from the native fields
+ *
+ * @param loadConfigFromFile if true, load the config field from the configuration file on disk
+ * @param readFieldsFromConfig if true, reload the member variables from the config field
+ */
+ private void syncConfig(boolean loadConfigFromFile, boolean readFieldsFromConfig) {
+ if (loadConfigFromFile) {
+ cfg.load();
+ }
+
+ final boolean FILTER_FRIEND_NOTIFICATIONS = true;
+ Property propFilterFriendNotify = cfg.get(Configuration.CATEGORY_CLIENT, "filterFriendNotifications", FILTER_FRIEND_NOTIFICATIONS, "Set to false to receive all login/logout messages, set to true to only get notifications of 'best friends' joining/leaving");
+
+ final String[] BEST_FRIENDS_DEFAULT_VALUE = new String[]{"Cow"};
+ Property propBestFriends = cfg.get(Configuration.CATEGORY_CLIENT, "bestFriends", BEST_FRIENDS_DEFAULT_VALUE, "List of best friends: receive login/logout notifications from them");
+ propBestFriends.setValidationPattern(Utils.VALID_USERNAME);
+
+ List<String> propOrderGeneral = new ArrayList<>();
+ propOrderGeneral.add(propFilterFriendNotify.getName());
+ propOrderGeneral.add(propBestFriends.getName());
+ cfg.setCategoryPropertyOrder(Configuration.CATEGORY_CLIENT, propOrderGeneral);
+
+ if (readFieldsFromConfig) {
+ filterFriendNotifications = propFilterFriendNotify.getBoolean(FILTER_FRIEND_NOTIFICATIONS);
+ bestFriends = propBestFriends.getStringList();
+ }
+
+ propFilterFriendNotify.set(filterFriendNotifications);
+ propBestFriends.set(bestFriends);
+
+ if (cfg.hasChanged()) {
+ cfg.save();
+ }
+ if (propBestFriends.hasChanged()) {
+ main.getFriends().syncFriends(bestFriends);
+ }
+ }
+
+ public void toggleNotifications() {
+ filterFriendNotifications = !filterFriendNotifications;
+ syncFromFields();
+ }
+
+ public class ConfigEventHandler {
+ @SubscribeEvent(priority = EventPriority.NORMAL)
+ public void onEvent(ConfigChangedEvent.OnConfigChangedEvent e) {
+ if (Cowmoonication.MODID.equals(e.modID)) {
+ syncFromGUI();
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/olli/cowmoonication/config/MooGuiConfig.java b/src/main/java/eu/olli/cowmoonication/config/MooGuiConfig.java
new file mode 100644
index 0000000..e7b7862
--- /dev/null
+++ b/src/main/java/eu/olli/cowmoonication/config/MooGuiConfig.java
@@ -0,0 +1,39 @@
+package eu.olli.cowmoonication.config;
+
+import eu.olli.cowmoonication.Cowmoonication;
+import net.minecraft.client.gui.GuiButton;
+import net.minecraft.client.gui.GuiScreen;
+import net.minecraftforge.common.config.ConfigElement;
+import net.minecraftforge.common.config.Configuration;
+import net.minecraftforge.fml.client.config.GuiConfig;
+
+public class MooGuiConfig extends GuiConfig {
+ public MooGuiConfig(GuiScreen parent) {
+ super(parent,
+ new ConfigElement(MooConfig.getConfig().getCategory(Configuration.CATEGORY_CLIENT)).getChildElements(),
+ Cowmoonication.MODID,
+ false,
+ false,
+ "Configuration for Cowmoonication");
+ titleLine2 = MooConfig.getConfig().getConfigFile().getAbsolutePath();
+ }
+
+ @Override
+ public void initGui() {
+ super.initGui();
+ // optional: add buttons and initialize fields
+ }
+
+
+ @Override
+ public void drawScreen(int mouseX, int mouseY, float partialTicks) {
+ super.drawScreen(mouseX, mouseY, partialTicks);
+ // optional: create animations, draw additional elements, etc.
+ }
+
+ @Override
+ protected void actionPerformed(GuiButton button) {
+ super.actionPerformed(button);
+ // optional: process any additional buttons added in initGui
+ }
+}
diff --git a/src/main/java/eu/olli/cowmoonication/config/MooGuiFactory.java b/src/main/java/eu/olli/cowmoonication/config/MooGuiFactory.java
new file mode 100644
index 0000000..dbdb139
--- /dev/null
+++ b/src/main/java/eu/olli/cowmoonication/config/MooGuiFactory.java
@@ -0,0 +1,29 @@
+package eu.olli.cowmoonication.config;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiScreen;
+import net.minecraftforge.fml.client.IModGuiFactory;
+
+import java.util.Set;
+
+public class MooGuiFactory implements IModGuiFactory {
+ @Override
+ public void initialize(Minecraft minecraftInstance) {
+
+ }
+
+ @Override
+ public Class<? extends GuiScreen> mainConfigGuiClass() {
+ return MooGuiConfig.class;
+ }
+
+ @Override
+ public Set<RuntimeOptionCategoryElement> runtimeGuiCategories() {
+ return null;
+ }
+
+ @Override
+ public RuntimeOptionGuiHandler getHandlerFor(RuntimeOptionCategoryElement element) {
+ return null;
+ }
+}
diff --git a/src/main/java/eu/olli/cowmoonication/listener/ChatListener.java b/src/main/java/eu/olli/cowmoonication/listener/ChatListener.java
new file mode 100644
index 0000000..83b3890
--- /dev/null
+++ b/src/main/java/eu/olli/cowmoonication/listener/ChatListener.java
@@ -0,0 +1,124 @@
+package eu.olli.cowmoonication.listener;
+
+import eu.olli.cowmoonication.Cowmoonication;
+import eu.olli.cowmoonication.config.MooConfig;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiChat;
+import net.minecraft.client.gui.GuiNewChat;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.IChatComponent;
+import net.minecraftforge.client.event.ClientChatReceivedEvent;
+import net.minecraftforge.client.event.GuiOpenEvent;
+import net.minecraftforge.client.event.GuiScreenEvent;
+import net.minecraftforge.client.event.RenderGameOverlayEvent;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import org.apache.commons.lang3.CharUtils;
+import org.lwjgl.input.Keyboard;
+import org.lwjgl.input.Mouse;
+
+import java.awt.*;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.StringSelection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ChatListener {
+ private static final Pattern PRIVATE_MESSAGE_RECEIVED_PATTERN = Pattern.compile("^From (?:\\[.*?] )?(\\w+): ");
+ private final Cowmoonication main;
+ private String lastTypedChars = "";
+ private String lastPMSender;
+
+ public ChatListener(Cowmoonication main) {
+ this.main = main;
+ }
+
+ @SubscribeEvent
+ public void onLogInOutMessage(ClientChatReceivedEvent e) {
+ if (e.type != 2 && MooConfig.filterFriendNotifications) { // normal chat or system msg
+ String text = e.message.getUnformattedText();
+ if (text.endsWith(" joined.") || text.endsWith(" left.") // Hypixel
+ || text.endsWith(" joined the game") || text.endsWith(" left the game.")) { // Spigot
+ // TODO maybe check which server thePlayer is on and check for logout pattern accordingly
+ int nameEnd = text.indexOf(" joined");
+ if (nameEnd == -1) {
+ nameEnd = text.indexOf(" left");
+ }
+ boolean isBestFriend = main.getFriends().isBestFriend(text.substring(0, nameEnd));
+ if (!isBestFriend) {
+ e.setCanceled(true);
+ }
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onClickOnChat(GuiScreenEvent.MouseInputEvent.Pre e) {
+ if (e.gui instanceof GuiChat) {
+ if (!Mouse.getEventButtonState() && Mouse.getEventButton() == 1 && Keyboard.isKeyDown(Keyboard.KEY_LMENU)) { // alt key pressed and right mouse button being released
+ IChatComponent chatComponent = Minecraft.getMinecraft().ingameGUI.getChatGUI().getChatComponent(Mouse.getX(), Mouse.getY());
+ if (chatComponent != null) {
+ String chatData = main.getUtils().cleanChatComponent(chatComponent);
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ clipboard.setContents(new StringSelection(chatData), null);
+ main.getUtils().sendAboveChatMessage(EnumChatFormatting.YELLOW + "Copied chat component to clipboard:", "" + EnumChatFormatting.BOLD + EnumChatFormatting.GOLD + "\u276E" + EnumChatFormatting.RESET + chatComponent.getUnformattedText() + EnumChatFormatting.BOLD + EnumChatFormatting.GOLD + "\u276F");
+ }
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onReplyToMsg(GuiScreenEvent.KeyboardInputEvent.Pre e) {
+ // TODO Switch to more reliable way: GuiTextField#writeText on GuiChat#inputField (protected field) via reflections [using "Open Command"-key isn't detected currently]
+ if (lastPMSender != null && e.gui instanceof GuiChat && lastTypedChars.length() < 3 && Keyboard.getEventKeyState()) {
+ char eventCharacter = Keyboard.getEventCharacter();
+ if (!CharUtils.isAsciiControl(eventCharacter)) {
+ lastTypedChars += eventCharacter;
+ if (lastTypedChars.equalsIgnoreCase("/r ")) {
+ // replace /r with /msg <last user>
+ main.getUtils().sendAboveChatMessage("Sending message to " + lastPMSender + "!");
+ Minecraft.getMinecraft().displayGuiScreen(new GuiChat("/msg " + lastPMSender + " "));
+ }
+ } else if (Keyboard.getEventKey() == Keyboard.KEY_BACK) { // Backspace
+ lastTypedChars = lastTypedChars.substring(0, Math.max(lastTypedChars.length() - 1, 0));
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onChatOpen(GuiOpenEvent e) {
+ if (e.gui instanceof GuiChat) {
+ lastTypedChars = "";
+ }
+ }
+
+ @SubscribeEvent
+ public void onPrivateMsgReceive(ClientChatReceivedEvent e) {
+ if (e.type != 2) {
+ Matcher matcher = PRIVATE_MESSAGE_RECEIVED_PATTERN.matcher(e.message.getUnformattedText());
+ if (matcher.find()) {
+ this.lastPMSender = matcher.group(1);
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onRenderChatGui(RenderGameOverlayEvent.Chat e) {
+ if (e.type == RenderGameOverlayEvent.ElementType.CHAT) {
+ // render message above chat box
+ String[] aboveChatMessage = main.getUtils().getAboveChatMessage();
+ if (aboveChatMessage != null) {
+ float chatHeightFocused = Minecraft.getMinecraft().gameSettings.chatHeightFocused;
+ float chatScale = Minecraft.getMinecraft().gameSettings.chatScale;
+ int chatBoxHeight = (int) (GuiNewChat.calculateChatboxHeight(chatHeightFocused) * chatScale);
+
+ int defaultTextY = e.resolution.getScaledHeight() - chatBoxHeight - 30;
+
+ for (int i = 0; i < aboveChatMessage.length; i++) {
+ String msg = aboveChatMessage[i];
+ int textY = defaultTextY - (aboveChatMessage.length - i) * (Minecraft.getMinecraft().fontRendererObj.FONT_HEIGHT + 1);
+ Minecraft.getMinecraft().fontRendererObj.drawStringWithShadow(msg, 2, textY, 0xffffff);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/resources/assets/cowmoonication/lang/en_us.lang b/src/main/resources/assets/cowmoonication/lang/en_us.lang
new file mode 100644
index 0000000..b7e77d4
--- /dev/null
+++ b/src/main/resources/assets/cowmoonication/lang/en_us.lang
@@ -0,0 +1 @@
+cowmoonication:command.moo.usage=/moo
diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info
new file mode 100644
index 0000000..e6c8f6e
--- /dev/null
+++ b/src/main/resources/mcmod.info
@@ -0,0 +1,13 @@
+[{
+ "modid": "cowmoonication",
+ "name": "Cowmoonication",
+ "description": "Adding various things related to communication.",
+ "version": "${version}",
+ "mcversion": "${mcversion}",
+ "url": "https://github.com/cow-mc/Cowmoonication",
+ "updateUrl": "",
+ "authorList": ["Cow"],
+ "logoFile": "",
+ "screenshots": [],
+ "dependencies": []
+}]