From 68c628a08b5bd42900407594d720c8ea349995f7 Mon Sep 17 00:00:00 2001 From: BlockyTheDev <86119630+BlockyTheDev@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:48:40 +0200 Subject: [PATCH] Release v1.0.0-1.8.9 Co-authored-by: MrMystery-Official <78692743+MrMystery-Official@users.noreply.github.com> --- .gitattributes | 7 + .github/CODEOWNERS | 2 + .github/workflows/build.yml | 37 ++ .gitignore | 100 ++++ HEADER | 13 + LICENSE | 202 +++++++ NOTICE | 14 + README.md | 44 ++ build.gradle | 116 ++++ gradle.properties | 23 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 56177 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++++++ gradlew.bat | 84 +++ settings.gradle | 35 ++ .../forgemod/CommunityRadarMod.java | 131 +++++ .../forgemod/command/RadarCommand.java | 550 ++++++++++++++++++ .../event/ClientChatReceivedListener.java | 78 +++ .../ClientConnectionDisconnectListener.java | 77 +++ .../forgemod/event/KeyInputListener.java | 59 ++ .../event/PlayerNameFormatListener.java | 52 ++ .../forgemod/radarlistmanager/RadarList.java | 195 +++++++ .../radarlistmanager/RadarListEntry.java | 117 ++++ .../radarlistmanager/RadarListManager.java | 322 ++++++++++ .../radarlistmanager/RadarListVisibility.java | 26 + .../adapters/GsonLocalDateTimeAdapter.java | 45 ++ .../GsonRadarListPlayerMapAdapter.java | 54 ++ .../forgemod/util/Messages.java | 106 ++++ .../forgemod/util/RadarMessage.java | 106 ++++ .../communityradargg/forgemod/util/Utils.java | 260 +++++++++ src/main/resources/mcmod.info | 19 + 31 files changed, 3052 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 HEADER create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/io/github/communityradargg/forgemod/CommunityRadarMod.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/command/RadarCommand.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/event/ClientChatReceivedListener.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/event/ClientConnectionDisconnectListener.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/event/KeyInputListener.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/event/PlayerNameFormatListener.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarList.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarListEntry.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarListManager.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarListVisibility.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/radarlistmanager/adapters/GsonLocalDateTimeAdapter.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/radarlistmanager/adapters/GsonRadarListPlayerMapAdapter.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/util/Messages.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/util/RadarMessage.java create mode 100644 src/main/java/io/github/communityradargg/forgemod/util/Utils.java create mode 100644 src/main/resources/mcmod.info diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..29bde31 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +* text=auto + +*.sh text eol=lf +gradlew text eol=lf +*.bat text eol=crlf + +*.jar binary \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..2e0e0f7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +* @MrMystery-Official +* @BlockyTheDev diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..65b66fd --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,37 @@ +name: Build + +on: + workflow_dispatch: + push: + branches: ["main"] + +jobs: + build: + if: github.repository_owner == 'CommunityRadarGG' + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Validate Gradle Wrapper + uses: gradle/actions/wrapper-validation@v3 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 8 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Clean Build + run: ./gradlew clean build + - name: Extract Version from Gradle + run: | + version=$(./gradlew properties -q | grep "^version:" | awk '{print $2}') + echo "VERSION=$version" >> $GITHUB_ENV + echo $version + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: communityradar-${{ env.VERSION }} + path: build/libs/*-${{ env.VERSION }}.jar \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ec0e3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,100 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Log file +*.log + +# BlueJ files +*.ctxt + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Cache of project +.gradletasknamecache + +**/build/ + +# Common working directory +run/ \ No newline at end of file diff --git a/HEADER b/HEADER new file mode 100644 index 0000000..2652fe7 --- /dev/null +++ b/HEADER @@ -0,0 +1,13 @@ +Copyright ${year} ${name} + +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 + + http://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. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..a0e3d32 --- /dev/null +++ b/NOTICE @@ -0,0 +1,14 @@ +CommunityRadar - Forge +Copyright 2024 - present CommunityRadarGG + +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 + + http://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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..15d6156 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +CommunityRadar - Official - Forge Mod 1.8.9 +========================== +[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4.svg)](#code-of-conduct) +
+[Website](https://community-radar.de/) | +[Discord](https://discord.community-radar.de/) + +CommunityRadar is a free and open-source collection of mods and addons for Minecraft for managing ingame scammers and trusted players on GrieferGames. + +## Issues +If you notice any bugs or missing features, you can let us know by opening a ticket or by creating a feedback or bug-report post on our Discord. + +## License +This project is subject to the [Apache License v2.0](https://www.apache.org/licenses/LICENSE-2.0). +This does only apply for source code located directly in this repository. +Dependencies and used tools may have other licenses, which is not covered by this license. +Dependency licenses can be seen in the `LICENSES` folder if present. + +# General Information +## Code of Conduct +Please view our Code of Conduct [here](https://github.com/CommunityRadarGG/.github/blob/main/CODE_OF_CONDUCT.md). + +## Contributing +We appreciate contributions. So if you want to support us, +feel free to make changes to the source code and submit a pull request. +Please follow the [guidelines](https://github.com/CommunityRadarGG/.github/blob/main/CONTRIBUTING.md). + +## Security +If you find a security issue please don't report it in a public issue. +Please use our form [here](https://github.com/CommunityRadarGG/.github/security/policy/). + +# Documentation +## Commands +- `/radar [help]` --> Shows all available commands and subcommands. +- `/radar lists` --> Shows all available lists. +- `/radar list add ` --> Creates a list with the given namespace and prefix. +- `/radar list prefix ` --> Changes the prefix of the given list. +- `/radar list delete ` --> Deletes a list by the given name. +- `/radar list show ` --> Shows all players on the given list. +- `/radar check ` --> Checks if the given player is on a list. +- `/radar check *` --> Checks, which players who are on a list are online. +- `/radar player add ` --> Adds a player to a private list. +- `/radar player remove ` --> Removes a given player from a private list. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..87940a6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,116 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +plugins { + id "java" + id "idea" + id "net.minecraftforge.gradle.forge" + id 'org.cadixdev.licenser' version '0.6.1' +} + +repositories { + mavenCentral() + maven { url = "https://jitpack.io/" } +} + +archivesBaseName = project.archives_base_name +version = project.mod_version +group = project.maven_group + +compileJava { + sourceCompatibility = targetCompatibility = "1.8" + options.encoding = "UTF-8" +} + + +minecraft { + version = "1.8.9-11.15.1.2318-1.8.9" + runDir = "run" + mappings = "stable_22" + makeObfSourceJar = false +} + +configurations { + include + implementation.extendsFrom(include) + + configurations { + include + implementation.extendsFrom(include) + runtimeOnly.canBeResolved = true + } +} + +dependencies { + include fileTree(include: ["*.jar"], dir: "libs") + implementation group: 'org.jetbrains', name: 'annotations', version: '15.0' +} + +processResources { + inputs.property "version", project.version + inputs.property "mcversion", project.minecraft.version + + filesMatching("mcmod.info") { + expand "version": project.version, "mcversion": project.minecraft.version + } +} + +tasks.register('moveResources') { + doLast { + ant.move file: "${buildDir}/resources/main", + todir: "${buildDir}/classes/java" + } +} + +moveResources.dependsOn(processResources) +classes.dependsOn(moveResources) + + +tasks.register('deobfJar', Jar) { + from sourceSets.main.output + archiveClassifier = 'deobf' +} + +tasks.register('srcJar', Jar) { + from sourceSets.main.allSource + archiveClassifier = 'sources' +} + +artifacts { + archives deobfJar + archives srcJar +} + +jar { + from("LICENSE") { + rename { + "${it}_${project.archives_base_name}" + } + } +} + +license { + setNewLine(false) + header(project.file("HEADER")) + properties { + year = "2024 - present" + name = "CommunityRadarGG " + } + tasks { + gradle { + files.from('build.gradle', 'settings.gradle', 'gradle.properties') + } + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..951fdc2 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# +# Copyright 2024 - present CommunityRadarGG +# +# 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 +# +# http://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. +# +org.gradle.jvmargs=-Xmx2048m + +# When increasing the version, also increase it in the 'CommunityRadarMod' class +mod_version=1.0.0-1.8.9 +maven_group=io.github.communityradargg +archives_base_name=communityradar + +forgegradle_version = a3d86a59c0 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..94336fcae912db8a11d55634156fa011f4686124 GIT binary patch literal 56177 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 i;x|p)^~-yfPOPa3(|vBayXKz \(.*\)$'` + 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="" + +# 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, 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\"" + 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..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..a82acc8 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,35 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + maven { url = "https://maven.minecraftforge.net/" } + maven { url = "https://jitpack.io/" } + } + + resolutionStrategy { + eachPlugin { + switch (requested.id.id) { + case "net.minecraftforge.gradle.forge": + useModule("com.github.ccbluex:ForgeGradle:${forgegradle_version}") + break + } + } + } +} + +rootProject.name = 'communityradar-forge' \ No newline at end of file diff --git a/src/main/java/io/github/communityradargg/forgemod/CommunityRadarMod.java b/src/main/java/io/github/communityradargg/forgemod/CommunityRadarMod.java new file mode 100644 index 0000000..03c6e10 --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/CommunityRadarMod.java @@ -0,0 +1,131 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod; + +import io.github.communityradargg.forgemod.command.RadarCommand; +import io.github.communityradargg.forgemod.event.ClientChatReceivedListener; +import io.github.communityradargg.forgemod.event.ClientConnectionDisconnectListener; +import io.github.communityradargg.forgemod.event.KeyInputListener; +import io.github.communityradargg.forgemod.event.PlayerNameFormatListener; +import io.github.communityradargg.forgemod.radarlistmanager.RadarListManager; +import net.minecraftforge.client.ClientCommandHandler; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.Mod.EventHandler; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.nio.file.Paths; + +/** + * This class represents the main class of the mod. + */ +@Mod(modid = CommunityRadarMod.MODID, version = CommunityRadarMod.VERSION) +public class CommunityRadarMod { + /** The id of the mod. */ + public static final String MODID = "communityradargg"; + /** The version of the mod. */ + public static final String VERSION = "1.0.0-1.8.9"; + private static final Logger logger = LogManager.getLogger(CommunityRadarMod.class); + private static RadarListManager listManager; + private boolean onGrieferGames = false; + + /** + * The listener for the {@link FMLInitializationEvent} event. + * + * @param event The event. + */ + @EventHandler + @SuppressWarnings("unused") // called by the mod loader + public void init(final FMLInitializationEvent event) { + logger.info("Starting the mod '" + MODID + "' with the version '" + VERSION + "'!"); + final File directoryPath = Paths.get(new File("") + .getAbsolutePath(),"labymod-neo", "configs", "communityradar") + .toFile(); + if (!directoryPath.exists() && !directoryPath.mkdirs()) { + logger.error("Could not create directory: {}", directoryPath); + } + + listManager = new RadarListManager(directoryPath.getAbsolutePath() + "/"); + registerPublicLists(); + // Needs to be after loading public lists + listManager.loadPrivateLists(); + registerEvents(); + registerCommands(); + logger.info("Successfully started the mod '" + MODID + "'!"); + } + + /** + * Registers the events. + */ + private void registerEvents() { + MinecraftForge.EVENT_BUS.register(new ClientChatReceivedListener(this)); + MinecraftForge.EVENT_BUS.register(new PlayerNameFormatListener(this)); + MinecraftForge.EVENT_BUS.register(new KeyInputListener(this)); + MinecraftForge.EVENT_BUS.register(new ClientConnectionDisconnectListener(this)); + } + + /** + * Registers the commands. + */ + private void registerCommands() { + ClientCommandHandler.instance.registerCommand(new RadarCommand()); + } + + /** + * Registers the public lists. + */ + private void registerPublicLists() { + if (!listManager.registerPublicList("scammer", "&7[&cScammer&7]", "https://lists.community-radar.de/versions/v1/scammer.json")) { + logger.error("Could not register public list 'scammers'!"); + } + + if (!listManager.registerPublicList("trusted", "&7[&aTrusted&7]", "https://lists.community-radar.de/versions/v1/trusted.json")) { + logger.error("Could not register public list 'verbvllert_trusted'!"); + } + } + + /** + * Gets the {@link RadarListManager} instance. + * + * @return Returns the radar list manager instance. + */ + public static @NotNull RadarListManager getListManager() { + return listManager; + } + + /** + * Gets the GrieferGames connection state. + * + * @return Returns the GrieferGames connection state. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean isOnGrieferGames() { + return onGrieferGames; + } + + /** + * Sets the GrieferGames connection state. + * + * @param onGrieferGames The GrieferGames connection state to set. + */ + public void setOnGrieferGames(final boolean onGrieferGames) { + this.onGrieferGames = onGrieferGames; + } +} diff --git a/src/main/java/io/github/communityradargg/forgemod/command/RadarCommand.java b/src/main/java/io/github/communityradargg/forgemod/command/RadarCommand.java new file mode 100644 index 0000000..b0387a2 --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/command/RadarCommand.java @@ -0,0 +1,550 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.command; + +import io.github.communityradargg.forgemod.CommunityRadarMod; +import io.github.communityradargg.forgemod.radarlistmanager.RadarList; +import io.github.communityradargg.forgemod.radarlistmanager.RadarListManager; +import io.github.communityradargg.forgemod.radarlistmanager.RadarListVisibility; +import io.github.communityradargg.forgemod.radarlistmanager.RadarListEntry; +import io.github.communityradargg.forgemod.util.Messages; +import io.github.communityradargg.forgemod.util.RadarMessage; +import io.github.communityradargg.forgemod.util.Utils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.network.NetworkPlayerInfo; +import net.minecraft.command.CommandBase; +import net.minecraft.command.ICommandSender; +import net.minecraft.entity.player.EntityPlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +/** + * The class containing all logic for the radar command. + */ +public class RadarCommand extends CommandBase { + /** {@inheritDoc} */ + @Override + public @NotNull String getCommandName() { + return "radar"; + } + + /** {@inheritDoc} */ + @Override + public String getCommandUsage(final @NotNull ICommandSender sender) { + return "/radar"; + } + + /** {@inheritDoc} */ + @Override + public List getCommandAliases() { + return Arrays.asList("communityradar", "scammer", "trustedmm", "mm"); + } + + /** {@inheritDoc} */ + @Override + public int getRequiredPermissionLevel() { + return 0; + } + + /** {@inheritDoc} */ + @Override + public boolean canCommandSenderUseCommand(final ICommandSender sender) { + return sender instanceof EntityPlayer; + } + + /** {@inheritDoc} */ + @Override + public void processCommand(final ICommandSender sender, final String[] args) { + if (!(sender instanceof EntityPlayer)) { + sender.addChatMessage(new RadarMessage.RadarMessageBuilder(Messages.NOT_PLAYER) + .build().toChatComponentText()); + return; + } + + final EntityPlayer player = (EntityPlayer) sender; + if (args.length == 0) { + handleHelpSubcommand(player); + return; + } + + switch (args[0].toUpperCase(Locale.ENGLISH)) { + case "CHECK": + handleCheckSubcommand(player, args); + break; + case "LIST": + handleListSubcommand(player, args); + break; + case "PLAYER": + handlePlayerSubcommand(player, args); + break; + case "LISTS": + handleListsSubcommand(player); + break; + default: + handleHelpSubcommand(player); + break; + } + } + + /** + * Handles the player subcommand. + * + * @param player The player, which executed the subcommand. + * @param args The arguments passed to the main command. + */ + private void handlePlayerSubcommand(final @NotNull EntityPlayer player, final @NotNull String[] args) { + if (args.length < 2) { + // missing arguments + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.MISSING_ARGS) + .build().toChatComponentText()); + return; + } + + switch (args[1].toUpperCase(Locale.ENGLISH)) { + case "ADD": + handlePlayerAddSubcommand(player, args); + break; + case "REMOVE": + handlePlayerRemoveSubcommand(player, args); + break; + default: + handleHelpSubcommand(player); + break; + } + } + + /** + * Handles the player - add subcommand. + * + * @param player The player, which executed the subcommand. + * @param args The arguments passed to the main command. + */ + private void handlePlayerAddSubcommand(final @NotNull EntityPlayer player, final @NotNull String[] args) { + if (args.length < 5) { + // missing arguments + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.MISSING_ARGS) + .build().toChatComponentText()); + return; + } + + final RadarListManager listManager = CommunityRadarMod.getListManager(); + final Optional listOptional = listManager.getRadarList(args[2]); + if (!listOptional.isPresent()) { + // list not existing + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Player.ADD_FAILED) + .build().toChatComponentText()); + return; + } + + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.INPUT_PROCESSING) + .build().toChatComponentText()); + Utils.getUUID(args[3]).thenAccept(uuidOptional -> { + if (!uuidOptional.isPresent()) { + // player uuid could not be fetched + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(args[3].startsWith("!") ? Messages.Player.NAME_INVALID_BEDROCK : Messages.Player.NAME_INVALID) + .build().toChatComponentText()); + return; + } + + final UUID uuid = uuidOptional.get(); + if (listOptional.get().isInList(uuid)) { + // player already on list + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Player.ADD_IN_LIST) + .build().toChatComponentText()); + return; + } + + final StringBuilder notes = new StringBuilder(); + for (int i = 4; i < args.length; i++) { + notes.append(args[i]).append(" "); + } + + if (!CommunityRadarMod.getListManager().addRadarListEntry(args[2], uuid, args[3], notes.substring(0, notes.length() - 1))) { + // list is not private + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Player.ADD_FAILED) + .build().toChatComponentText()); + return; + } + + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Player.ADD_SUCCESS) + .build().toChatComponentText()); + Utils.updatePlayerByUuid(uuid, listManager.getExistingPrefixes()); + }); + } + + /** + * Handles the player - remove subcommand. + * + * @param player The player, which executed the subcommand. + * @param args The arguments passed to the main command. + */ + private void handlePlayerRemoveSubcommand(final @NotNull EntityPlayer player, final @NotNull String[] args) { + if (args.length != 4) { + // missing arguments + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.MISSING_ARGS) + .build().toChatComponentText()); + return; + } + + final RadarListManager listManager = CommunityRadarMod.getListManager(); + final Optional listOptional = listManager.getRadarList(args[2]); + if (!listOptional.isPresent()) { + // list is not existing + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Player.REMOVE_FAILED) + .build().toChatComponentText()); + return; + } + + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.INPUT_PROCESSING) + .build().toChatComponentText()); + final RadarList list = listOptional.get(); + Utils.getUUID(args[3]).thenAccept(uuidOptional -> { + if (!uuidOptional.isPresent()) { + // player uuid could not be fetched + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(args[3].startsWith("!") ? Messages.Player.NAME_INVALID_BEDROCK : Messages.Player.NAME_INVALID) + .build().toChatComponentText()); + return; + } + + final UUID uuid = uuidOptional.get(); + if (!list.isInList(uuid)) { + // player uuid not on list + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Player.REMOVE_NOT_IN_LIST) + .build().toChatComponentText()); + return; + } + + list.getPlayerMap().remove(uuid); + Utils.updatePlayerByUuid(uuid, listManager.getExistingPrefixes()); + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Player.REMOVE_SUCCESS) + .build().toChatComponentText()); + }); + } + + /** + * Handles the list subcommand. + * + * @param player The player, which executed the subcommand. + * @param args The arguments passed to the main command. + */ + private void handleListSubcommand(final @NotNull EntityPlayer player, final @NotNull String[] args) { + if (args.length < 2) { + // missing arguments + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.MISSING_ARGS) + .build().toChatComponentText()); + return; + } + + switch (args[1].toUpperCase(Locale.ENGLISH)) { + case "ADD": + handleListAddSubcommand(player, args); + break; + case "PREFIX": + handleListPrefixSubcommand(player, args); + break; + case "DELETE": + handleListDeleteSubcommand(player, args); + break; + case "SHOW": + handleListShowSubcommand(player, args); + break; + default: + handleHelpSubcommand(player); + break; + } + } + + /** + * Handles the list - add subcommand. + * + * @param player The player, which executed the subcommand. + * @param args The arguments passed to the main command. + */ + private void handleListAddSubcommand(final @NotNull EntityPlayer player, final @NotNull String[] args) { + if (args.length != 4) { + // missing arguments + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.MISSING_ARGS) + .build().toChatComponentText()); + return; + } + + if (CommunityRadarMod.getListManager().getRadarList(args[2]).isPresent()) { + // list already existing + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.List.CREATE_FAILED) + .build().toChatComponentText()); + return; + } + + if (!CommunityRadarMod.getListManager().registerPrivateList(args[2], args[3])) { + // list could not be registered + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.List.CREATE_FAILED) + .build().toChatComponentText()); + return; + } + + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.List.CREATE_SUCCESS) + .build().toChatComponentText()); + } + + /** + * Handles the list - delete subcommand. + * + * @param player The player, which executed the subcommand. + * @param args The arguments passed to the main command. + */ + private void handleListDeleteSubcommand(final @NotNull EntityPlayer player, final @NotNull String[] args) { + if (args.length != 3) { + // missing arguments + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.MISSING_ARGS) + .build().toChatComponentText()); + return; + } + + final RadarListManager listManager = CommunityRadarMod.getListManager(); + final Set oldPrefixes = listManager.getExistingPrefixes(); + final Set oldUuids = listManager.getRadarList(args[2]) + .map(radarList -> radarList.getPlayerMap().keySet()) + .orElse(Collections.emptySet()); + + if (!listManager.unregisterList(args[2])) { + // list is not existing, list is not private, file cannot be deleted + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.List.DELETE_FAILED) + .build().toChatComponentText()); + return; + } + + oldUuids.forEach(uuid -> Utils.updatePlayerByUuid(uuid, oldPrefixes)); + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.List.DELETE_SUCCESS) + .build().toChatComponentText()); + } + + /** + * Handles the list - show subcommand. + * + * @param player The player, which executed the subcommand. + * @param args The arguments passed to the main command. + */ + private void handleListShowSubcommand(final @NotNull EntityPlayer player, final @NotNull String[] args) { + if (args.length != 3) { + // missing arguments + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.MISSING_ARGS) + .build().toChatComponentText()); + return; + } + + final Optional listOptional = CommunityRadarMod.getListManager().getRadarList(args[2]); + if (!listOptional.isPresent()) { + // list is not existing + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.List.SHOW_FAILED) + .build().toChatComponentText()); + return; + } + + final RadarList list = listOptional.get(); + if (list.getPlayerMap().isEmpty()) { + // list is empty + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.List.SHOW_EMPTY) + .build().toChatComponentText()); + return; + } + + final StringBuilder players = new StringBuilder(); + list.getPlayerMap().values().forEach(value -> players.append(value.name()).append(", ")); + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.List.SHOW_SUCCESS) + .replace("{list}", list.getNamespace()) + .replaceWithColorCodes("{prefix}", listOptional.get().getPrefix()) + .replace("{players}", players.substring(0, players.length() - 2)) + .build().toChatComponentText()); + } + + /** + * Handles the list - prefix subcommand. + * + * @param player The player, which executed the subcommand. + * @param args The arguments passed to the main command. + */ + private void handleListPrefixSubcommand(final @NotNull EntityPlayer player, final @NotNull String[] args) { + if (args.length != 4) { + // missing arguments + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.MISSING_ARGS) + .build().toChatComponentText()); + return; + } + + final RadarListManager listManager = CommunityRadarMod.getListManager(); + final Optional listOptional = listManager.getRadarList(args[2]); + if (!listOptional.isPresent()) { + // list is not existing + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.List.PREFIX_FAILED) + .build().toChatComponentText()); + return; + } + + final RadarList list = listOptional.get(); + final Set oldPrefixes = listManager.getExistingPrefixes(); + list.setPrefix(args[3]); + list.saveList(); + list.getPlayerMap().keySet().forEach(uuid -> Utils.updatePlayerByUuid(uuid, oldPrefixes)); + + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.List.PREFIX_SUCCESS) + .replaceWithColorCodes("{prefix}", args[3]) + .build().toChatComponentText()); + } + + /** + * Handles the check subcommand. + * + * @param player The player, which executed the subcommand. + * @param args The arguments passed to the main command. + */ + private void handleCheckSubcommand(final @NotNull EntityPlayer player, final @NotNull String[] args) { + if (args.length != 2) { + // missing arguments + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.MISSING_ARGS) + .build().toChatComponentText()); + return; + } + + if (args[1].equalsIgnoreCase("*")) { + // check all argument + handleCheckAllSubcommand(player); + return; + } + handleCheckPlayerSubcommand(player, args); + } + + /** + * Handles the check - player subcommand. + * + * @param player The player, which executed the subcommand. + * @param args The arguments passed to the main command. + */ + private void handleCheckPlayerSubcommand(final @NotNull EntityPlayer player, final @NotNull String[] args) { + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.INPUT_PROCESSING) + .build().toChatComponentText()); + Utils.getUUID(args[1]).thenAccept(checkPlayerOptional -> { + if (!checkPlayerOptional.isPresent()) { + // player uuid could not be fetched + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Check.FAILED) + .build().toChatComponentText()); + return; + } + + final Optional entryOptional = CommunityRadarMod.getListManager().getRadarListEntry(checkPlayerOptional.get()); + if (!entryOptional.isPresent()) { + // player uuid is on no list + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Check.FAILED) + .build().toChatComponentText()); + return; + } + + final RadarListEntry entry = entryOptional.get(); + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Check.FOUND + "\n" + Messages.Check.CHECK_ENTRY) + .replaceWithColorCodes("{prefix}", CommunityRadarMod.getListManager().getPrefix(entry.uuid())) + .replace("{name}", entry.name()) + .replace("{cause}", entry.cause()) + .replace("{entryCreationDate}", Utils.formatDateTime(entry.entryCreationDate())) + .replace("{entryUpdateDate}", Utils.formatDateTime(entry.entryUpdateDate())) + .build().toChatComponentText()); + }); + } + + /** + * Handles the check - all subcommand. + * + * @param player The player, which executed the subcommand. + */ + private void handleCheckAllSubcommand(final @NotNull EntityPlayer player) { + boolean anyPlayerFound = false; + for (final NetworkPlayerInfo networkPlayer : Minecraft.getMinecraft().getNetHandler().getPlayerInfoMap()) { + if (networkPlayer.getGameProfile().getId() == null) { + continue; + } + + final Optional listEntryOptional = CommunityRadarMod.getListManager() + .getRadarListEntry(networkPlayer.getGameProfile().getId()); + if (!listEntryOptional.isPresent()) { + // player uuid is on no list + continue; + } + + if (!anyPlayerFound) { + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Check.EVERYONE) + .build().toChatComponentText()); + anyPlayerFound = true; + } + + final RadarListEntry entry = listEntryOptional.get(); + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Check.CHECK_ENTRY) + .replaceWithColorCodes("{prefix}", CommunityRadarMod.getListManager().getPrefix(entry.uuid())) + .replace("{name}", entry.name()) + .replace("{cause}", entry.cause()) + .replace("{entryCreationDate}", Utils.formatDateTime(entry.entryCreationDate())) + .replace("{entryUpdateDate}", Utils.formatDateTime(entry.entryUpdateDate())) + .build().toChatComponentText()); + } + + if (!anyPlayerFound) { + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Check.NOT_FOUND) + .build().toChatComponentText()); + } + } + + /** + * Handles the help subcommand. + * + * @param player The player, which executed the subcommand. + */ + private void handleHelpSubcommand(final @NotNull EntityPlayer player) { + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.HELP) + .replace("{code_version}", CommunityRadarMod.VERSION) + .excludePrefix() + .build().toChatComponentText()); + } + + /** + * Handles the lists subcommand. + * + * @param player The player, which executed the subcommand. + */ + private void handleListsSubcommand(final @NotNull EntityPlayer player) { + final StringBuilder listsText = new StringBuilder(); + for (final String namespace : CommunityRadarMod.getListManager().getNamespaces()) { + CommunityRadarMod.getListManager().getRadarList(namespace) + .ifPresent(radarList -> listsText.append("§e").append(namespace).append(" §7(§c") + .append(radarList.getRadarListVisibility() == RadarListVisibility.PRIVATE ? Messages.Lists.PRIVATE : Messages.Lists.PUBLIC) + .append("§7)").append(", ")); + } + + if (listsText.length() > 0) { + // players on the list + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Lists.FOUND) + .replace("{lists}", listsText.substring(0, listsText.length() - 2)) + .build().toChatComponentText()); + } else { + // list is empty + player.addChatComponentMessage(new RadarMessage.RadarMessageBuilder(Messages.Lists.EMPTY) + .build().toChatComponentText()); + } + } +} diff --git a/src/main/java/io/github/communityradargg/forgemod/event/ClientChatReceivedListener.java b/src/main/java/io/github/communityradargg/forgemod/event/ClientChatReceivedListener.java new file mode 100644 index 0000000..18ed163 --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/event/ClientChatReceivedListener.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.event; + +import io.github.communityradargg.forgemod.CommunityRadarMod; +import io.github.communityradargg.forgemod.util.Utils; +import net.minecraft.util.ChatComponentText; +import net.minecraftforge.client.event.ClientChatReceivedEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A class containing a listener for client chat receiving. + */ +public class ClientChatReceivedListener { + /** + * A pattern matching private messages (in and out) and payments (in and out) as well as global and plot chat messages with the player name (nicked, bedrock and java) as only group. + */ + private static final Pattern pattern = Pattern.compile("[A-Za-z\\-+]+\\s\\u2503\\s(~?!?\\w{1,16})"); + private final CommunityRadarMod communityRadarMod; + + /** + * Constructs the class {@link ClientChatReceivedListener}. + * + * @param communityRadarMod An instance of the {@link CommunityRadarMod} class. + */ + public ClientChatReceivedListener(final @NotNull CommunityRadarMod communityRadarMod) { + this.communityRadarMod = communityRadarMod; + } + + /** + * The listener for the {@link ClientChatReceivedEvent} event. + * + * @param event The event. + */ + @SubscribeEvent + @SuppressWarnings("unused") // event listener + public void onClientChatReceived(final ClientChatReceivedEvent event) { + if (!communityRadarMod.isOnGrieferGames()) { + return; + } + + final Matcher matcher = pattern.matcher(event.message.getUnformattedText()); + if (!matcher.find()) { + return; + } + + final String playerName = matcher.group(1); + if (playerName.startsWith("~")) { + // nicked player + return; + } + + Utils.getUUID(playerName).thenAccept(uuid -> { + if (uuid.isPresent() && CommunityRadarMod.getListManager().isInList(uuid.get())) { + event.message = new ChatComponentText(CommunityRadarMod.getListManager().getPrefix(uuid.get()).replace("&", "§")) + .appendText(" §r") + .appendText(event.message.getFormattedText()); + } + }); + } +} diff --git a/src/main/java/io/github/communityradargg/forgemod/event/ClientConnectionDisconnectListener.java b/src/main/java/io/github/communityradargg/forgemod/event/ClientConnectionDisconnectListener.java new file mode 100644 index 0000000..265cbf5 --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/event/ClientConnectionDisconnectListener.java @@ -0,0 +1,77 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.event; + +import io.github.communityradargg.forgemod.CommunityRadarMod; +import io.github.communityradargg.forgemod.util.Utils; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.network.FMLNetworkEvent; +import org.jetbrains.annotations.NotNull; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +/** + * A class containing listeners for player connect and disconnect from and to servers. + */ +public class ClientConnectionDisconnectListener { + private final CommunityRadarMod communityRadarMod; + + /** + * Constructs the class {@link ClientConnectionDisconnectListener}. + * + * @param communityRadarMod An instance of the {@link CommunityRadarMod} class. + */ + public ClientConnectionDisconnectListener(final @NotNull CommunityRadarMod communityRadarMod) { + this.communityRadarMod = communityRadarMod; + } + + /** + * The listener for the {@link FMLNetworkEvent.ClientConnectedToServerEvent} event. + * + * @param event The event. + */ + @SubscribeEvent + @SuppressWarnings("unused") // called by forge + public void onFMLNetworkClientConnectedToServer(final FMLNetworkEvent.ClientConnectedToServerEvent event) { + if (event.isLocal) { + return; + } + + final SocketAddress socketAddress = event.manager.getRemoteAddress(); + if (!(socketAddress instanceof InetSocketAddress)) { + return; + } + + final String hostname = ((InetSocketAddress) socketAddress).getHostName(); + if (Utils.isGrieferGamesHostName(hostname)) { + communityRadarMod.setOnGrieferGames(true); + return; + } + communityRadarMod.setOnGrieferGames(false); + } + + /** + * The listener for the {@link FMLNetworkEvent.ClientDisconnectionFromServerEvent} event. + * + * @param event The event. + */ + @SubscribeEvent + @SuppressWarnings("unused") // called by forge + public void onFMLNetworkClientDisconnectionFromServer(final FMLNetworkEvent.ClientDisconnectionFromServerEvent event) { + communityRadarMod.setOnGrieferGames(false); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/communityradargg/forgemod/event/KeyInputListener.java b/src/main/java/io/github/communityradargg/forgemod/event/KeyInputListener.java new file mode 100644 index 0000000..d4b034b --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/event/KeyInputListener.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.event; + +import io.github.communityradargg.forgemod.CommunityRadarMod; +import io.github.communityradargg.forgemod.util.Utils; +import net.minecraft.client.Minecraft; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.InputEvent; +import org.jetbrains.annotations.NotNull; + +/** + * A class containing a listener for key input. + */ +public class KeyInputListener { + private final CommunityRadarMod communityRadarMod; + + /** + * Constructs the class {@link KeyInputListener}. + * + * @param communityRadarMod An instance of the {@link CommunityRadarMod} class. + */ + public KeyInputListener(final @NotNull CommunityRadarMod communityRadarMod) { + this.communityRadarMod = communityRadarMod; + } + + /** + * The listener for the {@link InputEvent.KeyInputEvent} event. + * + * @param event The event. + */ + @SubscribeEvent + @SuppressWarnings("unused") // called by mod loader on event + public void onKeyInput(final InputEvent.KeyInputEvent event) { + if (!communityRadarMod.isOnGrieferGames()) { + return; + } + + final Minecraft mc = Minecraft.getMinecraft(); + if (!mc.gameSettings.keyBindPlayerList.isPressed()) { + return; + } + + Utils.updatePrefixes(CommunityRadarMod.getListManager().getExistingPrefixes()); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/communityradargg/forgemod/event/PlayerNameFormatListener.java b/src/main/java/io/github/communityradargg/forgemod/event/PlayerNameFormatListener.java new file mode 100644 index 0000000..d608d6e --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/event/PlayerNameFormatListener.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.event; + +import io.github.communityradargg.forgemod.CommunityRadarMod; +import io.github.communityradargg.forgemod.util.Utils; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import org.jetbrains.annotations.NotNull; + +/** + * A class containing a listener for player name formatting. + */ +public class PlayerNameFormatListener { + private final CommunityRadarMod communityRadarMod; + + /** + * Constructs the class {@link PlayerNameFormatListener}. + * + * @param communityRadarMod An instance of the {@link CommunityRadarMod} class. + */ + public PlayerNameFormatListener(final @NotNull CommunityRadarMod communityRadarMod) { + this.communityRadarMod = communityRadarMod; + } + + /** + * The listener for the {@link PlayerEvent.NameFormat} event. + * + * @param event The event. + */ + @SubscribeEvent + @SuppressWarnings("unused") // called by forge event system + public void onPlayerNameFormat(final PlayerEvent.NameFormat event) { + if (!communityRadarMod.isOnGrieferGames()) { + return; + } + Utils.updatePlayerNameTag(event.entityPlayer, CommunityRadarMod.getListManager().getExistingPrefixes()); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarList.java b/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarList.java new file mode 100644 index 0000000..976401e --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarList.java @@ -0,0 +1,195 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.radarlistmanager; + +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import io.github.communityradargg.forgemod.CommunityRadarMod; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +/** + * A class representing a radar list. + */ +public class RadarList { + private static final Logger logger = LogManager.getLogger(RadarList.class); + @SerializedName("VERSION") + @SuppressWarnings("unused") // needed in future + private final int version = 1; + @SerializedName("namespace") + private final String namespace; + @SerializedName("url") + private final String url; + @SerializedName("playerMap") + private final Map playerMap; + @SerializedName("visibility") + private final RadarListVisibility visibility; + @SerializedName("prefix") + private String prefix; + + /** + * Constructs a {@link RadarList}. + * + * @param namespace The namespace for the list. + * @param prefix The prefix for the list. + * @param url The url for the list. + * @param visibility The visibility of the list. + */ + public RadarList(final @NotNull String namespace, final @NotNull String prefix, final @NotNull String url, final @NotNull RadarListVisibility visibility) { + this.namespace = namespace; + this.prefix = prefix; + this.visibility = visibility; + this.playerMap = new HashMap<>(); + this.url = url; + load(); + } + + /** + * Checks, whether a given uuid is in the list. + * + * @param uuid The uuid to check. + * @return Returns, whether the given uuid is in the list. + */ + public boolean isInList(final @NotNull UUID uuid) { + return playerMap.get(uuid) != null; + } + + /** + * Gets a radar list entry by a given uuid. + * + * @param uuid The uuid to get the entry for. + * @return Returns an optional with the found entry. + */ + public @NotNull Optional getRadarListEntry(final @NotNull UUID uuid) { + return Optional.ofNullable(playerMap.get(uuid)); + } + + /** + * Gets the namespace of the list. + * + * @return Returns the namespace. + */ + public @NotNull String getNamespace() { + return namespace; + } + + /** + * Gets the prefix of the list. + * + * @return Returns the prefix. + */ + public @NotNull String getPrefix() { + return prefix; + } + + /** + * Sets the prefix of the list. + * + * @param prefix The prefix to set. + */ + public void setPrefix(final @NotNull String prefix) { + this.prefix = prefix; + } + + /** + * Gets the visibility of the list. + * + * @return Returns the visibility. + */ + public @NotNull RadarListVisibility getRadarListVisibility() { + return visibility; + } + + /** + * Gets the url of the list. + * + * @return Returns the url. + */ + public @NotNull String getUrl() { + return url; + } + + /** + * Gets the player map of the list. + * + * @return Returns the player map. + */ + public @NotNull Map getPlayerMap() { + return playerMap; + } + + /** + * Adds a radar list entry to the list if it is private. + * + * @param radarListEntry The entry to add. + */ + public void addRadarListEntry(final @NotNull RadarListEntry radarListEntry) { + if (visibility == RadarListVisibility.PRIVATE) { + playerMap.put(radarListEntry.uuid(), radarListEntry); + saveList(); + } + } + + /** + * Loads a radar list entry. + * + * @param radarListEntry The entry to load. + */ + private void loadRadarListEntry(final @NotNull RadarListEntry radarListEntry) { + playerMap.put(radarListEntry.uuid(), radarListEntry); + } + + /** + * Saves a list to the disk if it is private. + */ + public void saveList() { + if (visibility == RadarListVisibility.PRIVATE) { + CommunityRadarMod.getListManager().saveRadarList(this); + } + } + + /** + * Loads a public list. + */ + private void loadPublicList() { + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(new URL(this.url).openStream()))) { + final List players = RadarListManager.getGson().fromJson(reader, new TypeToken>() {}.getType()); + players.forEach(this::loadRadarListEntry); + } catch (final IOException e) { + logger.error("Could not load public list", e); + } + } + + /** + * Loads a list if it is public. + */ + public void load() { + if (visibility == RadarListVisibility.PUBLIC) { + loadPublicList(); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarListEntry.java b/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarListEntry.java new file mode 100644 index 0000000..83f50c6 --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarListEntry.java @@ -0,0 +1,117 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.radarlistmanager; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * A class representing an entry in a radar list. + */ +public class RadarListEntry { + @SerializedName("uuid") + private final UUID uuid; + + @SerializedName("name") + private final String name; + + @SerializedName("cause") + private final String cause; + + @SerializedName("entryCreatedAt") + private final LocalDateTime entryCreationDate; + + @SerializedName("entryUpdatedAt") + private final LocalDateTime entryUpdateDate; + + @SerializedName("expiryDays") + private final int expiryDays; + + /** + * Constructs a {@link RadarListEntry}. + * + * @param uuid The player uuid of the entry. + * @param name The player name of the entry + * @param cause The cause of the entry. + * @param entryCreationDate The date when the entry was created the first time. + */ + public RadarListEntry(final @NotNull UUID uuid, final @NotNull String name, final @NotNull String cause, final @NotNull LocalDateTime entryCreationDate) { + this.uuid = uuid; + this.name = name; + this.cause = cause; + this.entryCreationDate = entryCreationDate; + this.entryUpdateDate = entryCreationDate; + this.expiryDays = -1; + } + + /** + * Gets the player uuid of the entry. + * + * @return Returns the player uuid of the entry. + */ + public @NotNull UUID uuid() { + return uuid; + } + + /** + * Gets the player name of the entry. + * + * @return Returns the player name of the entry. + */ + public @NotNull String name() { + return name; + } + + /** + * Gets the cause of the entry. + * + * @return Returns the cause of the entry. + */ + public @NotNull String cause() { + return cause; + } + + /** + * Gets the creation datetime of the entry. + * + * @return Returns the creation datetime of the entry. + */ + public LocalDateTime entryCreationDate() { + return entryCreationDate; + } + + /** + * Gets the update datetime of the entry. + * + * @return Returns the update datetime of the entry. + */ + public LocalDateTime entryUpdateDate() { + return entryUpdateDate; + } + + /** + * Gets the expiry days of the entry. + * + * @return Returns the expiry days of the entry. + */ + @SuppressWarnings("unused") // included - json has this field + public int expiryDays() { + return expiryDays; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarListManager.java b/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarListManager.java new file mode 100644 index 0000000..4b267f9 --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarListManager.java @@ -0,0 +1,322 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.radarlistmanager; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import io.github.communityradargg.forgemod.radarlistmanager.adapters.GsonLocalDateTimeAdapter; +import io.github.communityradargg.forgemod.radarlistmanager.adapters.GsonRadarListPlayerMapAdapter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A class containing the methods to manage lists. + */ +public class RadarListManager { + private static final Logger logger = LogManager.getLogger(RadarListManager.class); + private static final Gson gson = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(LocalDateTime.class, new GsonLocalDateTimeAdapter()) + .registerTypeAdapter(Map.class, new GsonRadarListPlayerMapAdapter()) + .create(); + private final List lists; + private final String directoryPath; + + /** + * Constructs a {@link RadarListManager} + * + * @param directoryPath The directory path of the list the manager manages. + */ + public RadarListManager(final @NotNull String directoryPath) { + this.lists = new ArrayList<>(); + this.directoryPath = directoryPath; + } + + /** + * Checks if a given uuid is in a list. + * + * @param uuid The uuid to check. + * @return Returns, whether the uuid is in a list. + */ + public boolean isInList(final @NotNull UUID uuid) { + return lists.stream() + .anyMatch(list -> list.isInList(uuid)); + } + + /** + * Gets the first prefix linked to a given uuid. + * + * @param uuid The uuid to get the prefix for. + * @return Returns the prefix. + */ + public @NotNull String getPrefix(final @NotNull UUID uuid) { + return lists.stream() + .filter(list -> list.isInList(uuid)) + .map(RadarList::getPrefix) + .findFirst() + .orElse(""); + } + + /** + * Gets all existing namespaces. + * + * @return Returns a set with all existing namespaces. + */ + public @NotNull Set getNamespaces() { + return lists.stream() + .map(RadarList::getNamespace) + .collect(Collectors.toSet()); + } + + /** + * Gets an optional with a {@link RadarListEntry} by a given uuid. + * + * @param uuid The uuid to get the entry for. + * @return Returns an optional with the found entry. + */ + public @NotNull Optional getRadarListEntry(final @NotNull UUID uuid) { + return lists.stream() + .filter(list -> list.isInList(uuid)) + .findFirst() + .flatMap(list -> list.getRadarListEntry(uuid)); + } + + /** + * Gets an optional with a {@link RadarList} by a given namespace. + * + * @param namespace The namespace to get the list for. + * @return Returns an optional with the found list. + */ + public @NotNull Optional getRadarList(final @NotNull String namespace) { + return lists.stream() + .filter(list -> list.getNamespace().equalsIgnoreCase(namespace)) + .findFirst(); + } + + /** + * Adds a player entry to a list. + * + * @param namespace The namespace of the list. + * @param uuid The player uuid for the entry. + * @param name The player name for the entry. + * @param cause The cause for the entry. + * @return Returns, whether the entry was successfully added. + */ + public boolean addRadarListEntry(final @NotNull String namespace, final @NotNull UUID uuid, final @NotNull String name, final @NotNull String cause) { + if (getRadarListEntry(uuid).isPresent()) { + return false; + } + + final Optional listOptional = lists.stream() + .filter(list -> list.getNamespace().equalsIgnoreCase(namespace)) + .findFirst(); + + if (listOptional.isPresent()) { + final RadarList list = listOptional.get(); + + if (list.getRadarListVisibility() == RadarListVisibility.PRIVATE) { + list.addRadarListEntry(new RadarListEntry(uuid, name, cause, LocalDateTime.now())); + return true; + } + } + return false; + } + + /** + * Saves a radar list to disk if it is a private one. + * + * @param list The list to save. + */ + public void saveRadarList(final @NotNull RadarList list) { + if (list.getRadarListVisibility() != RadarListVisibility.PRIVATE) { + return; + } + + try (final FileWriter writer = new FileWriter(directoryPath + list.getNamespace() + ".json")) { + writer.write(gson.toJson(list)); + } catch (final IOException e) { + logger.error("Could not save list", e); + } + } + + /** + * Registers a private list. + * + * @param namespace The namespace of the list. + * @param prefix The prefix of the list. + * @return Returns, whether the list was successfully registered. + */ + public boolean registerPrivateList(final @NotNull String namespace, final @NotNull String prefix) { + final boolean namespaceExists = getNamespaces().stream() + .anyMatch(namespace::equalsIgnoreCase); + if (namespaceExists) { + return false; + } + + lists.add(new RadarList(namespace, prefix, directoryPath + namespace + ".json", RadarListVisibility.PRIVATE)); + + final Optional listOptional = getRadarList(namespace); + if (!listOptional.isPresent()) { + return false; + } + + saveRadarList(listOptional.get()); + return true; + } + + /** + * Registers a public list. + * + * @param namespace The namespace of the list. + * @param prefix The prefix of the list. + * @return Returns, whether the list was successfully registered. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") // better understanding of code logic + public boolean registerPublicList(final @NotNull String namespace, final @NotNull String prefix, final @NotNull String url) { + final boolean namespaceExists = getNamespaces().stream() + .anyMatch(namespace::equalsIgnoreCase); + + if (namespaceExists) { + return false; + } + + lists.add(new RadarList(namespace, prefix, url, RadarListVisibility.PUBLIC)); + return true; + } + + /** + * Adds a radar list if it is not null. + * + * @param list The nullable radar list. + */ + private void addRadarList(final @Nullable RadarList list) { + if (list != null) { + lists.add(list); + } + } + + /** + * Unregisters a list by its namespace. + * + * @param namespace The namespace of the list. + * @return Returns, whether the list was successfully unregistered. + */ + public boolean unregisterList(final @NotNull String namespace) { + final Optional listOptional = getRadarList(namespace); + if (!listOptional.isPresent()) { + return false; + } + + final RadarList list = listOptional.get(); + if (list.getRadarListVisibility() == RadarListVisibility.PUBLIC) { + return false; + } + + final File file = new File(list.getUrl()); + if (file.exists() && !file.delete()) { + return false; + } + + lists.remove(list); + return true; + } + + /** + * Loads the private lists. + */ + public void loadPrivateLists() { + getJsonUrls(directoryPath) + .forEach(jsonUrl -> loadRadarListFromFile(jsonUrl) + .ifPresent(this::addRadarList)); + } + + /** + * Loads a radar list from a file. + * + * @param filePath The path to the file. + * @return Returns an optional with the loaded radar list. + */ + private @NotNull Optional loadRadarListFromFile(final @NotNull String filePath) { + try (final FileReader reader = new FileReader(filePath)) { + return Optional.of(gson.fromJson(reader, new TypeToken() {}.getType())); + } catch (final IOException | IllegalStateException | JsonIOException | JsonSyntaxException e) { + logger.error("Could not load list from file", e); + } + return Optional.empty(); + } + + /** + * Gets the json urls for the directory paths. + * + * @param directoryPath The directory path. + * @return Returns a set with all json urls. + */ + private @NotNull Set getJsonUrls(final @NotNull String directoryPath) { + try (final Stream paths = Files.walk(Paths.get(directoryPath))) { + return paths + .filter(Files::isRegularFile) + .map(Path::toString) + .filter(string -> string.endsWith(".json")) + .collect(Collectors.toSet()); + } catch (final IOException e) { + logger.error("Could not get json urls", e); + } + return new HashSet<>(); + } + + /** + * Gets the {@link Gson} instance with project relevant settings. + * + * @return Returns the pre-configured {@link Gson} instance. + */ + public static @NotNull Gson getGson() { + return gson; + } + + /** + * Gets all existing prefixes. + * + * @return Returns a set with all existing prefixes. + */ + public @NotNull Set getExistingPrefixes() { + return lists.stream() + .map(RadarList::getPrefix) + .collect(Collectors.toSet()); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarListVisibility.java b/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarListVisibility.java new file mode 100644 index 0000000..95737d8 --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/RadarListVisibility.java @@ -0,0 +1,26 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.radarlistmanager; + +/** + * An enum representing the visibility state of a radar list. + */ +public enum RadarListVisibility { + /** The state when the list is a public one. */ + PUBLIC, + /** The state when the list is a private one. */ + PRIVATE +} \ No newline at end of file diff --git a/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/adapters/GsonLocalDateTimeAdapter.java b/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/adapters/GsonLocalDateTimeAdapter.java new file mode 100644 index 0000000..9b11ea6 --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/adapters/GsonLocalDateTimeAdapter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.radarlistmanager.adapters; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * A class with an adapter for serialization and deserialization of the class {@link LocalDateTime} for the GSON library. + */ +public class GsonLocalDateTimeAdapter implements JsonSerializer, JsonDeserializer { + /** {@inheritDoc} */ + @Override + public LocalDateTime deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException { + return LocalDateTime.parse(json.getAsString(), DateTimeFormatter.ISO_DATE_TIME); + } + + /** {@inheritDoc} */ + @Override + public JsonElement serialize(final LocalDateTime localDateTime, final Type typeOfSrc, final JsonSerializationContext context) { + return new JsonPrimitive(localDateTime.format(DateTimeFormatter.ISO_DATE_TIME)); + } +} diff --git a/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/adapters/GsonRadarListPlayerMapAdapter.java b/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/adapters/GsonRadarListPlayerMapAdapter.java new file mode 100644 index 0000000..98d91dc --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/radarlistmanager/adapters/GsonRadarListPlayerMapAdapter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.radarlistmanager.adapters; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import io.github.communityradargg.forgemod.radarlistmanager.RadarListEntry; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * A class with an adapter for serialization and deserialization of following structure {@code Map} for the GSON library. + */ +public class GsonRadarListPlayerMapAdapter implements JsonSerializer>, JsonDeserializer> { + /** {@inheritDoc} */ + @Override + public Map deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException { + final JsonArray playerMapJsonArray = json.getAsJsonArray(); + final Map playerMap = new HashMap<>(); + + playerMapJsonArray.forEach(jsonElement -> { + final RadarListEntry entry = context.deserialize(jsonElement, RadarListEntry.class); + playerMap.put(entry.uuid(), entry); + }); + return playerMap; + } + + /** {@inheritDoc} */ + @Override + public JsonElement serialize(final Map playerMap, final Type typeOfSrc, final JsonSerializationContext context) { + return context.serialize(playerMap.values()); + } +} diff --git a/src/main/java/io/github/communityradargg/forgemod/util/Messages.java b/src/main/java/io/github/communityradargg/forgemod/util/Messages.java new file mode 100644 index 0000000..f308a60 --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/util/Messages.java @@ -0,0 +1,106 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.util; + +/** + * A class containing all texts. + */ +public class Messages { + public static final String PREFIX = "§8[§cCommunityRadar§8]§r "; + public static final String MISSING_ARGS = "§cNicht genug Argumente. Gib '/radar' für den korrekten Syntax ein."; + public static final String NOT_PLAYER = "§cDieser Befehl kann nur von Spielern ausgeführt werden."; + public static final String INPUT_PROCESSING = "§7Deine Eingabe wird verarbeitet. Dies kann einige Augenblicke benötigen."; + + public static final String HELP = + "§7§l--------- §eRadar-Hilfe §7§l---------§r\n" + + "§e/radar lists §7-> Zeigt die vorhandenen Listen an.\n" + + "§e/radar list add §7-> Erstellt eine neue Liste.\n" + + "§e/radar list prefix §7-> Ändert den Präfix einer Liste.\n" + + "§e/radar list delete §7-> Löscht eine Liste.\n" + + "§e/radar list show §7-> Zeigt alle Spieler eine Liste an.\n" + + "§e/radar check §7-> Prüft ob sich ein Spieler auf einer Liste befindet.\n" + + "§e/radar check * §7-> Prüft ob sich einer der Spieler in der Welt auf einer Liste befindet.\n" + + "§e/radar player add §7-> Fügt einen Spieler zu einer Liste hinzu.\n" + + "§e/radar player remove §7-> Entfernt einen Spieler von einer Liste.\n" + + "§e/radar help §7-> Zeigt diese Hilfeübersicht an.\n" + + "§eEntwickler §7-> MrMystery, BlockyTheDev\n" + + "§eVersion §7-> §e{code_version}\n" + + "§eWebsite, Downloads & Tutorials §7-> https://community-radar.de/\n" + + "§eDiscord §7-> https://discord.community-radar.de/\n" + + "§eQuellcode §7-> https://github.com/CommunityRadarGG/Mod_Forge_1.8.9/\n" + + "§7§l--------- §eRadar-Hilfe §7§l---------§r"; + + /** + * Translations related to the listen subcommand. + */ + public static class Lists { + public static final String FOUND = "§7Listen: §e{lists}"; + public static final String EMPTY = "§7Es wurden§c keine §7Listen gefunden!"; + public static final String PRIVATE = "PRIVAT"; + public static final String PUBLIC = "ÖFFENTLICH"; + } + + /** + * Translations related to the list subcommand. + */ + public static class List { + public static final String CREATE_SUCCESS = "§7Die Liste wurde§a erstellt§7!"; + public static final String CREATE_FAILED = "§cFehler beim Erstellen der Liste. Existiert bereits eine Liste mit diesem Namen?"; + + public static final String DELETE_SUCCESS = "§7Diese Liste wurde§c gelöscht§7!"; + public static final String DELETE_FAILED = "§cFehler beim Löschen der Liste. Ist der Name korrekt und handelt es sich um eine private Liste?"; + + public static final String SHOW_SUCCESS = "§7Liste: §e{list}§7, Präfix: §e{prefix}§7, Spieler: §e{players}"; + public static final String SHOW_FAILED = "§cFehler beim Anzeigen der Liste. Ist der Name korrekt?"; + public static final String SHOW_EMPTY = "§7Es befindet sich kein Spieler auf dieser Liste."; + + public static final String PREFIX_SUCCESS = "§7Der Präfix wurde zu §e{prefix} §7geändert."; + public static final String PREFIX_FAILED = "§cFehler beim Ändern des Präfixes."; + } + + /** + * Translations related to the check command. + */ + public static class Check { + public static final String EVERYONE = "§7Online Spieler in einer Liste:"; + public static final String NOT_FOUND = "§cEs ist kein Spieler online, welcher in einer Liste eingetragen ist."; + public static final String FAILED = "§7Der angegebene Spieler wurde auf§c keiner §7Liste gefunden."; + + public static final String FOUND = "§7Der Spieler wurde in einer Liste gefunden:"; + public static final String CHECK_ENTRY = + "§7Präfix: §e{prefix}\n" + + "§7Name: §e{name}\n" + + "§7Grund: §e{cause}\n" + + "§7Hinzugefügt: §e{entryCreationDate}\n" + + "§7Letzte Aktualisierung: §e{entryUpdateDate}\n"; + } + + /** + * Translations related to the player command. + */ + public static class Player { + public static final String NAME_INVALID = "§cDer Spieler konnte nicht gefunden werden. Ist der Name korrekt?"; + public static final String NAME_INVALID_BEDROCK = "§cDer Spieler konnte nicht gefunden werden. Da es sich um einen Spieler der Bedrock Version handelt, muss dieser in derselben Welt sein wie du, um zur Liste hinzugefügt werden zu können."; + + public static final String ADD_SUCCESS = "§7Der Spieler wurde zur Liste§a hinzugefügt§7. Handelt es sich um einen Fall für die öffentliche Liste, dann erstelle einen Beitrag im GrieferGames-Forum oder besuche unseren Discord."; + public static final String ADD_FAILED = "§cDer Spieler konnte nicht hinzugefügt werden. Hast du eine private Liste verwendet? Um eine private Liste zu erstellen, nutze den Befehl '/radar list add '."; + public static final String ADD_IN_LIST = "§7Der Spieler befindet sich bereits auf einer Liste."; + + public static final String REMOVE_SUCCESS = "§7Der Spieler wurde aus der Liste§c entfernt§7."; + public static final String REMOVE_FAILED = "§cDer Spieler konnte nicht entfernt werden. Hast du eine private Liste verwendet?"; + public static final String REMOVE_NOT_IN_LIST = "§7Der Spieler befindet sich auf§c keiner Liste§7."; + } +} diff --git a/src/main/java/io/github/communityradargg/forgemod/util/RadarMessage.java b/src/main/java/io/github/communityradargg/forgemod/util/RadarMessage.java new file mode 100644 index 0000000..de77a08 --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/util/RadarMessage.java @@ -0,0 +1,106 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.util; + +import net.minecraft.util.ChatComponentText; +import org.jetbrains.annotations.NotNull; + +/** + * A class representing a message that can be shown to a player. + */ +public class RadarMessage { + private final String text; + + /** + * Constructs a {@link RadarMessage}. + * + * @param text The text for the message. + * @param includePrefix Whether a prefix should be included in the message. + */ + private RadarMessage(final @NotNull String text, final boolean includePrefix) { + this.text = (includePrefix ? Messages.PREFIX : "") + text; + } + + /** + * Converts this class instance to a {@link ChatComponentText}. + * + * @return Returns the text converted to a {@link ChatComponentText}. + */ + public @NotNull ChatComponentText toChatComponentText() { + return new ChatComponentText(this.text); + } + + /** + * A class that serves as a builder for the class {@link RadarMessage}. + */ + public static class RadarMessageBuilder { + private String text; + private boolean includePrefix; + + /** + * Constructs a {@link RadarMessageBuilder}. + * + * @param text The text for the builder. + */ + public RadarMessageBuilder(final @NotNull String text) { + this.text = text; + this.includePrefix = true; + } + + /** + * Replaces old text with a new one in the text stored in this builder. + * + * @param oldText The old text to replace. + * @param newText The replacement text. + * @return Returns the builder after replacing the text. + */ + public @NotNull RadarMessageBuilder replace(final @NotNull String oldText, final @NotNull String newText) { + this.text = this.text.replace(oldText, newText); + return this; + } + + /** + * Replaces old text with a new one in the text stored in this builder by considering color codes. + * + * @param oldText The old text to replace. + * @param newText The replacement text. + * @return Returns the builder after replacing the text and color codes. + */ + public @NotNull RadarMessageBuilder replaceWithColorCodes(final @NotNull String oldText, final @NotNull String newText) { + this.text = this.text.replace(oldText, newText.replace("&", "§")); + return this; + } + + /** + * Sets the prefix exclude state in the builder. + * + * @return Returns the builder after setting the prefix exclude state. + */ + public @NotNull RadarMessageBuilder excludePrefix() { + this.includePrefix = false; + return this; + } + + /** + * Builds a {@link RadarMessage} out of the builder. + * + * @return Returns the build {@link RadarMessage}. + */ + public @NotNull RadarMessage build() { + return new RadarMessage(text, includePrefix); + } + } +} diff --git a/src/main/java/io/github/communityradargg/forgemod/util/Utils.java b/src/main/java/io/github/communityradargg/forgemod/util/Utils.java new file mode 100644 index 0000000..e178992 --- /dev/null +++ b/src/main/java/io/github/communityradargg/forgemod/util/Utils.java @@ -0,0 +1,260 @@ +/* + * Copyright 2024 - present CommunityRadarGG + * + * 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 + * + * http://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. + */ +package io.github.communityradargg.forgemod.util; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import io.github.communityradargg.forgemod.CommunityRadarMod; +import net.minecraft.client.Minecraft; +import net.minecraft.client.network.NetworkPlayerInfo; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.IChatComponent; +import net.minecraft.world.World; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; + +/** + * A class with some util methods. + */ +public class Utils { + private static final Logger logger = LogManager.getLogger(Utils.class); + private static final String MOJANG_API_NAME_TO_UUID = "https://api.mojang.com/users/profiles/minecraft/"; + private static final Pattern UUID_MOJANG_API_PATTERN = Pattern.compile("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})"); + private static final DateTimeFormatter readableDateTimeFormatter = DateTimeFormatter.ofPattern("d.M.yyyy H:m:ss"); + private static final Map uuidNameCache = new HashMap<>(); + + /** + * Tries to get the uuid to the player name from the world. + * + * @param playerName The player name to get the corresponding uuid. + * @return Returns a CompletableFuture with an optional with the player uuid. + */ + public static @NotNull CompletableFuture> getUUID(final @NotNull String playerName) { + // user has to be in a world + if (Minecraft.getMinecraft().theWorld == null) { + return CompletableFuture.completedFuture(Optional.empty()); + } + + // If the UUID has been cached, returning from the map. + if (uuidNameCache.containsKey(playerName)) { + return CompletableFuture.completedFuture(Optional.of(uuidNameCache.get(playerName))); + } + + // Checking if there is a player with same name in the loaded world. If so, returning UUID from EntityPlayer. + for (final NetworkPlayerInfo networkPlayerInfo : Minecraft.getMinecraft().getNetHandler().getPlayerInfoMap()) { + if (networkPlayerInfo.getGameProfile().getName().equalsIgnoreCase(playerName)) { + uuidNameCache.put(playerName, networkPlayerInfo.getGameProfile().getId()); + return CompletableFuture.completedFuture(Optional.of(networkPlayerInfo.getGameProfile().getId())); + } + } + + if (playerName.startsWith("!") || playerName.startsWith("~")) { + return CompletableFuture.completedFuture(Optional.empty()); + } + + // If no player with same name is in the world, try fetching the UUID from the Mojang-API. + return requestUuidForName(playerName); + } + + /** + * Requests an uuid to a player name, from the Mojang API. + * + * @param playerName The player name to get the uuid for. + * @return Returns a CompletableFuture with an optional with the requested uuid, it will be empty if an error occurred on requesting. + */ + private static @NotNull CompletableFuture> requestUuidForName(final @NotNull String playerName) { + final String urlText = MOJANG_API_NAME_TO_UUID + playerName; + return CompletableFuture.supplyAsync(() -> { + HttpURLConnection connection = null; + try { + final URL url = new URL(urlText); + connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(3000); + connection.setReadTimeout(3000); + connection.setRequestMethod("GET"); + connection.setRequestProperty("User-Agent", CommunityRadarMod.MODID + "/" + CommunityRadarMod.VERSION); + + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { + logger.warn("Requesting data from '{}' resulted in following status code: {}", urlText, connection.getResponseCode()); + return Optional.empty(); + } + + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + final JsonObject json = new Gson().fromJson(reader, JsonObject.class); + if (json == null || !json.has("id") || !json.has("name")) { + connection.disconnect(); + return Optional.empty(); + } + + final UUID uuid = UUID.fromString(UUID_MOJANG_API_PATTERN.matcher(json.get("id").getAsString()).replaceAll("$1-$2-$3-$4-$5")); + uuidNameCache.put(playerName, uuid); + connection.disconnect(); + return Optional.of(uuid); + } + } catch (final Exception e) { + if (connection != null) { + connection.disconnect(); + } + logger.error("Trying to request data from '{}' resulted in an exception", urlText, e); + return Optional.empty(); + } + }); + } + + /** + * Formats a given date time in a human-readable form. + * + * @param localDateTime The local date time to format. + * @return Returns the formatted date time. + */ + public static @NotNull String formatDateTime(final @NotNull LocalDateTime localDateTime) { + return localDateTime.format(readableDateTimeFormatter); + } + + /** + * Checks if a given hostname is a hostname of GrieferGames. + *

+ * Following domains are taken into account: + *
+ * - griefergames.net + *
+ * - griefergames.de + *
+ * - griefergames.live + * + * @param hostName The hostname to check. + * @return Returns, whether the given hostname is one of the GrieferGames hostnames. + */ + public static boolean isGrieferGamesHostName(final @NotNull String hostName) { + final String filteredHostName = Optional.of(hostName) + .filter(host -> host.endsWith(".")) + .map(host -> host.substring(0, host.length() - 1).toLowerCase(Locale.ENGLISH)) + .orElse(hostName.toLowerCase(Locale.ENGLISH)); + return filteredHostName.endsWith("griefergames.net") || filteredHostName.endsWith("griefergames.de") || filteredHostName.endsWith("griefergames.live"); + } + + /** + * Gets a {@link NetworkPlayerInfo} by the uuid of a player. + * + * @param uuid The uuid to get the network player info for. + * @return Returns an optional with the network player info of an online player to the uuid. + */ + private static @NotNull Optional getNetworkPlayerInfoByUuid(final @NotNull UUID uuid) { + return Minecraft.getMinecraft().thePlayer.sendQueue.getPlayerInfoMap().stream() + .filter(player -> player.getGameProfile() != null && uuid.equals(player.getGameProfile().getId())) + .findFirst(); + } + + /** + * Gets a {@link EntityPlayer} by the uuid of a player. + * + * @param uuid The uuid to get the entity player for. + * @return Returns an optional with the entity player to the uuid. + */ + private static @NotNull Optional getEntityPlayerByUuid(final @NotNull UUID uuid) { + final World world = Minecraft.getMinecraft().theWorld; + if (world == null) { + return Optional.empty(); + } + + return world.playerEntities.stream() + .filter(player -> player.getGameProfile() != null && uuid.equals(player.getGameProfile().getId())) + .findFirst(); + } + + /** + * Updates a player display name and name tag by its uuid. + * + * @param uuid The uuid to update the corresponding player. + */ + public static void updatePlayerByUuid(final @NotNull UUID uuid, final @NotNull Set oldPrefixes) { + getEntityPlayerByUuid(uuid).ifPresent(player -> updatePlayerNameTag(player, oldPrefixes)); + getNetworkPlayerInfoByUuid(uuid).ifPresent(networkPlayerInfo -> updatePlayerPrefix(networkPlayerInfo, oldPrefixes)); + } + + /** + * Handles updating the name tag of a player entity. + * + * @param player The player entity to update the name tag. + * @param oldPrefixes The old prefixes that need to be removed before adding the new one. + */ + public static void updatePlayerNameTag(final @NotNull EntityPlayer player, final @NotNull Set oldPrefixes) { + player.getPrefixes().removeIf(prefix -> oldPrefixes.stream().anyMatch(oldPrefix -> new ChatComponentText(oldPrefix.replace("&", "§") + " ").getUnformattedText().equals(prefix.getUnformattedText()))); + final String addonPrefix = CommunityRadarMod.getListManager() + .getPrefix(player.getGameProfile().getId()) + .replace("&", "§"); + + if (!addonPrefix.isEmpty()) { + player.addPrefix(new ChatComponentText(addonPrefix + " ")); + } + } + + /** + * Handles updating the player prefixes in the display name. + * + * @param oldPrefixes The old prefixes that need to be removed before adding the new one. + */ + public static void updatePrefixes(final @NotNull Set oldPrefixes) { + Minecraft.getMinecraft().thePlayer.sendQueue.getPlayerInfoMap() + .forEach(player -> updatePlayerPrefix(player, oldPrefixes)); + } + + /** + * Handles updating the player prefix in the display name of a single player. + * + * @param player The player to update. + * @param oldPrefixes The old prefixes that need to be removed before adding the new one. + */ + private static void updatePlayerPrefix(final @NotNull NetworkPlayerInfo player, final @NotNull Set oldPrefixes) { + if (player.getGameProfile() == null || player.getGameProfile().getId() == null || player.getDisplayName() == null) { + return; + } + + final IChatComponent displayName = player.getDisplayName(); + IChatComponent newDisplayName = displayName; + for (final String prefix : oldPrefixes) { + if (!displayName.getUnformattedText().startsWith(new ChatComponentText(prefix.replace("&", "§") + " ").getUnformattedText())) { + continue; + } + newDisplayName = displayName.getSiblings().get(displayName.getSiblings().size() - 1); + } + + final String addonPrefix = CommunityRadarMod.getListManager() + .getPrefix(player.getGameProfile().getId()) + .replace("&", "§"); + if (!addonPrefix.isEmpty()) { + newDisplayName = new ChatComponentText(addonPrefix.replace("&", "§") + " ").appendSibling(newDisplayName); + } + player.setDisplayName(newDisplayName); + } +} diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info new file mode 100644 index 0000000..34e82e8 --- /dev/null +++ b/src/main/resources/mcmod.info @@ -0,0 +1,19 @@ +[ + { + "modid": "communityradargg", + "name": "CommunityRadar Mod - Official - 1.8.9", + "description": "The Official CommunityRadar Forge 1.8.9 Mod.", + "version": "${version}", + "mcversion": "${mcversion}", + "url": "https://discord.community-radar.de", + "updateUrl": "", + "authorList": [ + "MrMystery", + "BlockyTheDev" + ], + "credits": "CommunityRadar-Team, BlockyTheDev, MrMystery", + "logoFile": "assets/communityradargg/logo.jpeg", + "screenshots": [], + "dependencies": [] + } +]