├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── net │ └── entityoutliner │ ├── EntityOutliner.java │ ├── mixin │ ├── MixinMinecraftClient.java │ └── MixinWorldRenderer.java │ └── ui │ ├── ColorWidget.java │ ├── EntityListWidget.java │ ├── EntitySelector.java │ └── ModMenuIntegration.java └── resources ├── assets └── entityoutliner │ ├── icon.png │ ├── lang │ ├── en_us.json │ ├── fr_fr.json │ └── zh_cn.json │ └── textures │ └── gui │ └── colors.png ├── entityoutliner.mixins.json └── fabric.mod.json /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | 8 | # eclipse 9 | 10 | *.launch 11 | 12 | # idea 13 | 14 | .idea/ 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | # vscode 20 | 21 | .settings/ 22 | .vscode/ 23 | bin/ 24 | .classpath 25 | .project 26 | 27 | # fabric 28 | 29 | run/ 30 | remappedSrc/ 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Adam Viola 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Entity Outliner 2 | Entity Outliner is a clientside mod that allows you to select entity types to outline, making them visible through obstructions at any distance. 3 | 4 | ## Why Use It? 5 | This mod will help with: 6 |
7 | Finding passive mobs 8 | 9 | by outlining them. 10 | 11 | ![Image of outlined bees](https://i.imgur.com/jqhVLSX.png "It's hard to find bees!") 12 |
13 | 14 |
15 | Finding unlit caves 16 | 17 | by outlining zombies, creepers, skeletons, and spiders. 18 | 19 | ![Gif showing how outlining monsters can reveal unlit caves](https://i.imgur.com/owNj5BE.gif "Great for when you've reached a dead end in your cave!") 20 | 21 | 22 |
23 | 24 |
25 | Fighting other players 26 | 27 | by outlining players. 28 | 29 | ![Image of outlined players](https://i.imgur.com/TiEldyM.png "Even works while they're sneaking!") 30 | 31 |
32 | 33 |
34 | Finding your death location 35 | 36 | by outlining items and experience orbs. 37 | 38 | 39 | ![Image of outlined items/xp orbs of death location](https://i.imgur.com/sOzk89i.png "Tombstone mods are cool too!") 40 | 41 | 42 |
43 | 44 |
45 | Wither skeleton skull hunting 46 | 47 | by outlining wither skeletons 48 | 49 | ![Image of outlined wither skeletons](https://i.imgur.com/cc4rhaY.png "I actually like the grind for wither skeleton skulls!") 50 | 51 |
52 | 53 |
54 | Finding mineshafts 55 | 56 | by outlining cave spiders and minecarts with chests. 57 | 58 | 59 | ![Image of outlined chest minecarts](https://i.imgur.com/36rMnDc.png "I hate cave spiders!") 60 | 61 |
62 | 63 | And many more! 64 | 65 | ## Features 66 | **Entity Selector** 67 | 68 | ![GIF demonstrating use of the entity selector screen](https://i.imgur.com/XozyBa4.gif "It's a prefix search!") 69 | 70 | This screen allows outlining of any entity in the game. There's a search bar for narrowing down entities and buttons to organize the results by entity category, deselect all entities, and toggle on/off the outlines. Entities added by other mods **do** appear in the results. 71 | 72 | For the technically inclined, the search works using a precomputed hashtable that maps a string prefix to a corresponding list of results. The lists of results are computed for all prefixes that correspond at least one entity type. 73 | 74 | **Controls for toggling the outlines and opening the selector** 75 | 76 | ![Image of the keybind selector for toggling the outlines and selector screen](https://i.imgur.com/au39Ov1.png "Hopefully o and p aren't taken!") 77 | 78 | Custom keybinds are provided to open the entity selector and toggle the outline. The outline can also be toggled via a button inside the entity selector. 79 | 80 | 81 | ## Installation 82 | 1. Install [Fabric](https://fabricmc.net/use/) 83 | 2. Drop the [Fabric API](https://www.curseforge.com/minecraft/mc-mods/fabric-api) jar into the mods folder 84 | 3. Drop the Entity Outliner jar into the mods folder 85 | 86 | ## Compatibility 87 | Works with MobZ. Let me know if you find any compatibility issues. 88 | 89 | ## License 90 | MIT. Feel free to use this mod in any modpack. 91 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'fabric-loom' version '0.11-SNAPSHOT' 3 | id 'maven-publish' 4 | } 5 | 6 | sourceCompatibility = JavaVersion.VERSION_17 7 | targetCompatibility = JavaVersion.VERSION_17 8 | 9 | archivesBaseName = project.archives_base_name 10 | version = project.mod_version 11 | group = project.maven_group 12 | 13 | repositories { 14 | maven { 15 | url "https://maven.terraformersmc.com/releases/" 16 | } 17 | } 18 | 19 | dependencies { 20 | minecraft "com.mojang:minecraft:${project.minecraft_version}" 21 | mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" 22 | modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" 23 | 24 | //Fabric api 25 | modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" 26 | 27 | // ModMenu 28 | modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" 29 | 30 | compileOnly 'com.google.code.findbugs:jsr305:+' 31 | } 32 | 33 | processResources { 34 | inputs.property "version", project.version 35 | 36 | filesMatching("fabric.mod.json") { 37 | expand "version": project.version 38 | } 39 | 40 | // from(sourceSets.main.resources.srcDirs) { 41 | // exclude "fabric.mod.json" 42 | // } 43 | } 44 | 45 | // ensure that the encoding is set to UTF-8, no matter what the system default is 46 | // this fixes some edge cases with special characters not displaying correctly 47 | // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html 48 | tasks.withType(JavaCompile) { 49 | options.encoding = "UTF-8" 50 | } 51 | 52 | // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task 53 | // if it is present. 54 | // If you remove this task, sources will not be generated. 55 | task sourcesJar(type: Jar, dependsOn: classes) { 56 | classifier = "sources" 57 | from sourceSets.main.allSource 58 | } 59 | 60 | jar { 61 | from "LICENSE" 62 | } 63 | 64 | // configure the maven publication 65 | publishing { 66 | publications { 67 | mavenJava(MavenPublication) { 68 | // add all the jars that should be included when publishing to maven 69 | artifact(remapJar) { 70 | builtBy remapJar 71 | } 72 | artifact(sourcesJar) { 73 | builtBy remapSourcesJar 74 | } 75 | } 76 | } 77 | 78 | // select the repositories you want to publish to 79 | repositories { 80 | // uncomment to publish to the local maven 81 | // mavenLocal() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx1G 3 | 4 | # Fabric Properties 5 | # check these on https://fabricmc.net/use 6 | minecraft_version=1.19.3 7 | yarn_mappings=1.19.3+build.5 8 | loader_version=0.14.13 9 | 10 | 11 | # Mod Properties 12 | mod_version = 1.2.6 13 | maven_group = net.entityoutliner 14 | archives_base_name = entity-outliner 15 | 16 | 17 | 18 | # Dependencies 19 | # currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api 20 | fabric_version=0.73.0+1.19.3 21 | modmenu_version=5.0.2 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamviola/EntityOutliner/2716cd8434f482745032d82045d826013bf8d055/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | jcenter() 4 | maven { 5 | name = 'Fabric' 6 | url = 'https://maven.fabricmc.net/' 7 | } 8 | gradlePluginPortal() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/net/entityoutliner/EntityOutliner.java: -------------------------------------------------------------------------------- 1 | package net.entityoutliner; 2 | 3 | import java.io.IOException; 4 | import java.lang.reflect.Type; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.stream.Collectors; 10 | 11 | 12 | import com.google.gson.JsonObject; 13 | import com.google.gson.JsonSyntaxException; 14 | import com.google.gson.Gson; 15 | import com.google.gson.reflect.TypeToken; 16 | 17 | import org.lwjgl.glfw.GLFW; 18 | 19 | import net.entityoutliner.ui.EntitySelector; 20 | import net.entityoutliner.ui.ColorWidget.Color; 21 | import net.fabricmc.api.ClientModInitializer; 22 | import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; 23 | import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; 24 | import net.fabricmc.loader.api.FabricLoader; 25 | import net.minecraft.client.MinecraftClient; 26 | import net.minecraft.client.option.KeyBinding; 27 | import net.minecraft.client.util.InputUtil; 28 | import net.minecraft.entity.EntityType; 29 | import net.minecraft.registry.Registries; 30 | 31 | public class EntityOutliner implements ClientModInitializer { 32 | private static final Gson GSON = new Gson(); 33 | public static boolean outliningEntities; 34 | 35 | private static final KeyBinding CONFIG_BIND = new KeyBinding( 36 | "key.entity-outliner.selector", 37 | InputUtil.Type.KEYSYM, 38 | GLFW.GLFW_KEY_SEMICOLON, 39 | "title.entity-outliner.title" 40 | ); 41 | 42 | private static final KeyBinding OUTLINE_BIND = new KeyBinding( 43 | "key.entity-outliner.outline", 44 | InputUtil.Type.KEYSYM, 45 | GLFW.GLFW_KEY_O, 46 | "title.entity-outliner.title" 47 | ); 48 | 49 | @Override 50 | public void onInitializeClient() { 51 | KeyBindingHelper.registerKeyBinding(CONFIG_BIND); 52 | KeyBindingHelper.registerKeyBinding(OUTLINE_BIND); 53 | 54 | loadConfig(); 55 | 56 | ClientTickEvents.END_CLIENT_TICK.register(this::onEndTick); 57 | } 58 | 59 | private static Path getConfigPath() { 60 | return FabricLoader.getInstance().getConfigDir().resolve("entityoutliner.json"); 61 | } 62 | 63 | public static void saveConfig() { 64 | JsonObject config = new JsonObject(); 65 | 66 | List> outlinedEntityNames = EntitySelector.outlinedEntityTypes.entrySet().stream() 67 | .map(entry -> List.of(EntityType.getId(entry.getKey()).toString(), entry.getValue().name())) 68 | .collect(Collectors.toList()); 69 | 70 | config.add("outlinedEntities", GSON.toJsonTree(outlinedEntityNames)); 71 | 72 | try { 73 | Files.write(getConfigPath(), GSON.toJson(config).getBytes()); 74 | } 75 | catch (IOException ex) { 76 | logException(ex, "Failed to save EntityOutliner config"); 77 | } 78 | } 79 | 80 | private void loadConfig() { 81 | try { 82 | JsonObject config = GSON.fromJson(new String(Files.readAllBytes(getConfigPath())), JsonObject.class); 83 | if (config.has("outlinedEntities")) { 84 | Type setType = new TypeToken>>(){}.getType(); 85 | List> outlinedEntityNames = GSON.fromJson(config.get("outlinedEntities"), setType); 86 | 87 | Map, Color> outlinedEntityTypes = outlinedEntityNames.stream() 88 | .collect(Collectors.toMap(list -> EntityType.get(list.get(0)).get(), list -> Color.valueOf(list.get(1))));; 89 | 90 | for (EntityType entityType : Registries.ENTITY_TYPE) 91 | if (outlinedEntityTypes.containsKey(entityType)) 92 | EntitySelector.outlinedEntityTypes.put(entityType, outlinedEntityTypes.get(entityType)); 93 | } 94 | } 95 | catch (IOException | JsonSyntaxException ex) { 96 | logException(ex, "Failed to load EntityOutliner config"); 97 | } 98 | } 99 | 100 | private void onEndTick(MinecraftClient client) { 101 | while (OUTLINE_BIND.wasPressed()) { 102 | outliningEntities = !outliningEntities; 103 | } 104 | 105 | if (CONFIG_BIND.isPressed()) { 106 | client.setScreen(new EntitySelector(null)); 107 | } 108 | } 109 | 110 | public static void logException(Exception ex, String message) { 111 | System.err.printf("[EntityOutliner] %s (%s: %s)", message, ex.getClass().getSimpleName(), ex.getLocalizedMessage()); 112 | } 113 | } -------------------------------------------------------------------------------- /src/main/java/net/entityoutliner/mixin/MixinMinecraftClient.java: -------------------------------------------------------------------------------- 1 | package net.entityoutliner.mixin; 2 | 3 | import net.entityoutliner.EntityOutliner; 4 | import net.entityoutliner.ui.EntitySelector; 5 | import net.minecraft.client.MinecraftClient; 6 | import net.minecraft.entity.Entity; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 11 | 12 | @Mixin(MinecraftClient.class) 13 | public abstract class MixinMinecraftClient { 14 | 15 | @Inject(method = "hasOutline", at = @At("HEAD"), cancellable = true) 16 | private void outlineEntities(Entity entity, CallbackInfoReturnable ci) { 17 | if (EntityOutliner.outliningEntities && EntitySelector.outlinedEntityTypes != null) { 18 | if (EntitySelector.outlinedEntityTypes.containsKey(entity.getType())) { 19 | ci.setReturnValue(true); 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/java/net/entityoutliner/mixin/MixinWorldRenderer.java: -------------------------------------------------------------------------------- 1 | package net.entityoutliner.mixin; 2 | 3 | import org.spongepowered.asm.mixin.Mixin; 4 | import org.spongepowered.asm.mixin.injection.At; 5 | import org.spongepowered.asm.mixin.injection.Inject; 6 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 7 | 8 | import net.entityoutliner.EntityOutliner; 9 | import net.entityoutliner.ui.EntitySelector; 10 | import net.entityoutliner.ui.ColorWidget.Color; 11 | import net.minecraft.client.render.OutlineVertexConsumerProvider; 12 | import net.minecraft.client.render.VertexConsumerProvider; 13 | import net.minecraft.client.render.WorldRenderer; 14 | import net.minecraft.client.util.math.MatrixStack; 15 | import net.minecraft.entity.Entity; 16 | import net.minecraft.entity.EntityType; 17 | import net.minecraft.entity.player.PlayerEntity; 18 | import net.minecraft.scoreboard.AbstractTeam; 19 | 20 | @Mixin(WorldRenderer.class) 21 | public abstract class MixinWorldRenderer { 22 | 23 | @Inject(method = "renderEntity", at = @At("HEAD")) 24 | private void renderEntity(Entity entity, double cameraX, double cameraY, double cameraZ, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, CallbackInfo ci) { 25 | if (EntityOutliner.outliningEntities 26 | && vertexConsumers instanceof OutlineVertexConsumerProvider 27 | && EntitySelector.outlinedEntityTypes.containsKey(entity.getType())) { 28 | 29 | Color color = EntitySelector.outlinedEntityTypes.get(entity.getType()); 30 | OutlineVertexConsumerProvider outlineVertexConsumers = (OutlineVertexConsumerProvider) vertexConsumers; 31 | outlineVertexConsumers.setColor(color.red, color.green, color.blue, 255); 32 | 33 | if (entity.getType() == EntityType.PLAYER) { 34 | PlayerEntity player = (PlayerEntity) entity; 35 | AbstractTeam team = player.getScoreboardTeam(); 36 | if (team != null && team.getColor().getColorValue() != null) { 37 | int hexColor = team.getColor().getColorValue(); 38 | int blue = hexColor % 256; 39 | int green = (hexColor / 256) % 256; 40 | int red = (hexColor / 65536) % 256; 41 | outlineVertexConsumers.setColor(red, green, blue, 255); 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/net/entityoutliner/ui/ColorWidget.java: -------------------------------------------------------------------------------- 1 | package net.entityoutliner.ui; 2 | 3 | import java.util.Map; 4 | 5 | import com.mojang.blaze3d.platform.GlStateManager; 6 | import com.mojang.blaze3d.systems.RenderSystem; 7 | 8 | import net.fabricmc.api.EnvType; 9 | import net.fabricmc.api.Environment; 10 | import net.minecraft.client.MinecraftClient; 11 | import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; 12 | import net.minecraft.client.gui.widget.PressableWidget; 13 | import net.minecraft.client.util.math.MatrixStack; 14 | import net.minecraft.entity.EntityType; 15 | import net.minecraft.entity.SpawnGroup; 16 | import net.minecraft.text.Text; 17 | import net.minecraft.util.Identifier; 18 | 19 | @Environment(EnvType.CLIENT) 20 | public class ColorWidget extends PressableWidget { 21 | private static final Identifier TEXTURE = new Identifier("entityoutliner:textures/gui/colors.png"); 22 | private Color color; 23 | private EntityType entityType; 24 | 25 | private ColorWidget(int x, int y, int width, int height, Text message, EntityType entityType) { 26 | super(x, y, width, height, message); 27 | this.entityType = entityType; 28 | 29 | if (EntitySelector.outlinedEntityTypes.containsKey(this.entityType)) 30 | onShow(); 31 | } 32 | 33 | public ColorWidget(int x, int y, int width, int height, EntityType entityType) { 34 | this(x, y, width, height, Text.translatable("options.chat.color"), entityType); 35 | } 36 | 37 | public void onShow() { 38 | this.color = EntitySelector.outlinedEntityTypes.get(this.entityType); 39 | } 40 | 41 | public void onPress() { 42 | this.color = this.color.next(); 43 | EntitySelector.outlinedEntityTypes.put(this.entityType, this.color); 44 | } 45 | 46 | public void renderButton(MatrixStack matrices, int mouseX, int mouseY, float delta) { 47 | MinecraftClient minecraftClient = MinecraftClient.getInstance(); 48 | RenderSystem.setShaderTexture(0, TEXTURE); 49 | RenderSystem.enableDepthTest(); 50 | RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, this.alpha); 51 | RenderSystem.enableBlend(); 52 | RenderSystem.defaultBlendFunc(); 53 | RenderSystem.blendFunc(GlStateManager.SrcFactor.SRC_ALPHA, GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA); 54 | drawTexture(matrices, this.getX(), this.getY(), this.isFocused() ? 20.0F : 0.0F, this.color.ordinal() * 20, 20, 20, 40, 180); 55 | this.renderBackground(matrices, minecraftClient, mouseX, mouseY); 56 | } 57 | 58 | public enum Color { 59 | WHITE(255, 255, 255), 60 | BLACK(0, 0, 0), 61 | RED(255, 0, 0), 62 | ORANGE(255, 127, 0), 63 | YELLOW(255, 255, 0), 64 | GREEN(0, 255, 0), 65 | BLUE(0, 0, 255), 66 | PURPLE(127, 0, 127), 67 | PINK(255, 155, 182); 68 | 69 | public int red; 70 | public int green; 71 | public int blue; 72 | 73 | private static final Map spawnGroupColors = Map.of( 74 | SpawnGroup.AMBIENT, Color.PURPLE, 75 | SpawnGroup.AXOLOTLS, Color.PINK, 76 | SpawnGroup.CREATURE, Color.YELLOW, 77 | SpawnGroup.MISC, Color.WHITE, 78 | SpawnGroup.MONSTER, Color.RED, 79 | SpawnGroup.UNDERGROUND_WATER_CREATURE, Color.ORANGE, 80 | SpawnGroup.WATER_AMBIENT, Color.GREEN, 81 | SpawnGroup.WATER_CREATURE, Color.BLUE 82 | ); 83 | 84 | private static Color[] colors = Color.values(); 85 | 86 | private Color(int red, int green, int blue) { 87 | this.red = red; 88 | this.green = green; 89 | this.blue = blue; 90 | } 91 | 92 | public static Color of(SpawnGroup group) { 93 | return spawnGroupColors.get(group); 94 | } 95 | 96 | public Color next() { 97 | return get((this.ordinal() + 1) % colors.length); 98 | } 99 | 100 | public Color get(int index) { 101 | return colors[index]; 102 | } 103 | } 104 | 105 | @Override 106 | protected void appendClickableNarrations(NarrationMessageBuilder builder) {} 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/net/entityoutliner/ui/EntityListWidget.java: -------------------------------------------------------------------------------- 1 | package net.entityoutliner.ui; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.apache.commons.lang3.StringUtils; 7 | 8 | import net.entityoutliner.ui.ColorWidget.Color; 9 | import net.fabricmc.api.EnvType; 10 | import net.fabricmc.api.Environment; 11 | import net.minecraft.client.MinecraftClient; 12 | import net.minecraft.client.font.TextRenderer; 13 | import net.minecraft.client.gui.DrawableHelper; 14 | import net.minecraft.client.gui.Element; 15 | import net.minecraft.client.gui.Selectable; 16 | import net.minecraft.client.gui.widget.CheckboxWidget; 17 | import net.minecraft.client.gui.widget.ElementListWidget; 18 | import net.minecraft.client.gui.widget.PressableWidget; 19 | import net.minecraft.client.util.math.MatrixStack; 20 | import net.minecraft.entity.EntityType; 21 | import net.minecraft.entity.SpawnGroup; 22 | import net.minecraft.util.Language; 23 | 24 | @Environment(EnvType.CLIENT) 25 | public class EntityListWidget extends ElementListWidget { 26 | 27 | public EntityListWidget(MinecraftClient client, int width, int height, int top, int bottom, int itemHeight) { 28 | super(client, width, height, top, bottom, itemHeight); 29 | this.centerListVertically = false; 30 | } 31 | 32 | public void addListEntry(EntityListWidget.Entry entry) { 33 | super.addEntry(entry); 34 | } 35 | 36 | public void clearListEntries() { 37 | super.clearEntries(); 38 | } 39 | 40 | public int getRowWidth() { 41 | return 400; 42 | } 43 | 44 | protected int getScrollbarPositionX() { 45 | return super.getScrollbarPositionX() + 32; 46 | } 47 | 48 | @Environment(EnvType.CLIENT) 49 | public static abstract class Entry extends ElementListWidget.Entry { } 50 | 51 | @Environment(EnvType.CLIENT) 52 | public static class EntityEntry extends EntityListWidget.Entry { 53 | 54 | private final CheckboxWidget checkbox; 55 | private final ColorWidget color; 56 | private final EntityType entityType; 57 | private final List children = new ArrayList<>(); 58 | 59 | private EntityEntry(CheckboxWidget checkbox, ColorWidget color, EntityType entityType) { 60 | this.checkbox = checkbox; 61 | this.entityType = entityType; 62 | this.color = color; 63 | 64 | this.children.add(checkbox); 65 | if (EntitySelector.outlinedEntityTypes.containsKey(entityType)) 66 | this.children.add(color); 67 | } 68 | 69 | public static EntityListWidget.EntityEntry create(EntityType entityType, int width) { 70 | return new EntityListWidget.EntityEntry( 71 | new CheckboxWidget(width / 2 - 155, 0, 310, 20, entityType.getName(), EntitySelector.outlinedEntityTypes.containsKey(entityType)), 72 | new ColorWidget(width / 2 + 130, 0, 310, 20, entityType), 73 | entityType 74 | ); 75 | } 76 | 77 | public void render(MatrixStack matrices, int i, int j, int k, int l, int m, int n, int o, boolean bl, float f) { 78 | this.checkbox.setY(j); 79 | this.checkbox.render(matrices, n, o, f); 80 | 81 | if (this.children.contains(this.color)) { 82 | this.color.setY(j); 83 | this.color.render(matrices, n, o, f); 84 | } 85 | } 86 | 87 | public boolean mouseClicked(double mouseX, double mouseY, int button) { 88 | if (EntitySelector.outlinedEntityTypes.containsKey(entityType)) { 89 | if (this.color.isMouseOver(mouseX, mouseY)) { 90 | this.color.onPress(); 91 | } else { 92 | EntitySelector.outlinedEntityTypes.remove(entityType); 93 | 94 | this.checkbox.onPress(); 95 | this.children.remove(this.color); 96 | } 97 | } else { 98 | EntitySelector.outlinedEntityTypes.put(entityType, Color.of(entityType.getSpawnGroup())); 99 | 100 | this.color.onShow(); 101 | this.checkbox.onPress(); 102 | this.children.add(this.color); 103 | } 104 | 105 | return true; 106 | } 107 | 108 | public List children() { 109 | return this.children; 110 | } 111 | 112 | public EntityType getEntityType() { 113 | return this.entityType; 114 | } 115 | 116 | public CheckboxWidget getCheckbox() { 117 | return this.checkbox; 118 | } 119 | 120 | public List selectableChildren() { 121 | return this.children; 122 | } 123 | } 124 | 125 | @Environment(EnvType.CLIENT) 126 | public static class HeaderEntry extends EntityListWidget.Entry { 127 | 128 | private final TextRenderer font; 129 | private final String title; 130 | private final int width; 131 | private final int height; 132 | 133 | private HeaderEntry(SpawnGroup category, TextRenderer font, int width, int height) { 134 | this.font = font; 135 | this.width = width; 136 | this.height = height; 137 | 138 | if (category != null) { 139 | String title = ""; 140 | for (String term : category.getName().split("\\p{Punct}|\\p{Space}")) { 141 | title += StringUtils.capitalize(term) + " "; 142 | } 143 | this.title = title.trim(); 144 | } else { 145 | this.title = Language.getInstance().get("gui.entity-outliner.no_results"); 146 | } 147 | 148 | } 149 | 150 | public static EntityListWidget.HeaderEntry create(SpawnGroup category, TextRenderer font, int width, int height) { 151 | return new EntityListWidget.HeaderEntry(category, font, width, height); 152 | } 153 | 154 | public void render(MatrixStack matrices, int i, int j, int k, int l, int m, int n, int o, boolean bl, float f) { 155 | DrawableHelper.drawCenteredText(matrices, this.font, this.title, this.width / 2, j + (this.height / 2) - (this.font.fontHeight / 2), 16777215); 156 | } 157 | 158 | public List children() { 159 | return new ArrayList<>(); 160 | } 161 | 162 | public String toString() { 163 | return this.title; 164 | } 165 | 166 | @Override 167 | public List selectableChildren() { 168 | return new ArrayList<>(); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/net/entityoutliner/ui/EntitySelector.java: -------------------------------------------------------------------------------- 1 | package net.entityoutliner.ui; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Comparator; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | 8 | import net.entityoutliner.EntityOutliner; 9 | import net.entityoutliner.ui.ColorWidget.Color; 10 | import net.minecraft.client.gui.screen.Screen; 11 | import net.minecraft.client.gui.widget.ButtonWidget; 12 | 13 | import net.minecraft.client.gui.widget.TextFieldWidget; 14 | import net.minecraft.client.util.math.MatrixStack; 15 | import net.minecraft.entity.SpawnGroup; 16 | import net.minecraft.entity.EntityType; 17 | import net.minecraft.text.Text; 18 | // import net.minecraft.util.registry.Registry; 19 | import net.minecraft.registry.Registries; 20 | 21 | public class EntitySelector extends Screen { 22 | protected final Screen parent; 23 | 24 | private TextFieldWidget searchField; 25 | private EntityListWidget list; 26 | public static boolean groupByCategory = true; 27 | private static String searchText = ""; 28 | public static HashMap>> searcher; // Prefix -> arr of results 29 | public static HashMap, Color> outlinedEntityTypes = new HashMap<>(); 30 | 31 | public EntitySelector(Screen parent) { 32 | super(Text.translatable("title.entity-outliner.selector")); 33 | this.parent = parent; 34 | } 35 | 36 | public void onClose() { 37 | this.client.setScreen(this.parent); 38 | } 39 | 40 | protected void init() { 41 | if (searcher == null) { 42 | initializePrefixTree(); 43 | } 44 | 45 | this.list = new EntityListWidget(this.client, this.width, this.height, 32, this.height - 32, 25); 46 | this.addSelectableChild(list); 47 | 48 | // Create search field 49 | this.searchField = new TextFieldWidget(this.textRenderer, this.width / 2 - 100, 6, 200, 20, Text.of(searchText)); 50 | this.searchField.setText(searchText); 51 | this.searchField.setChangedListener(this::onSearchFieldUpdate); 52 | this.addSelectableChild(searchField); 53 | 54 | // Create buttons 55 | int buttonWidth = 80; 56 | int buttonHeight = 20; 57 | int buttonInterval = (this.width - 4 * buttonWidth) / 5; 58 | int buttonOffset = buttonInterval; 59 | int buttonY = this.height - 16 - (buttonHeight / 2); 60 | 61 | // Add sort type button 62 | // this.addDrawableChild(new ButtonWidget(buttonOffset, buttonY, buttonWidth, buttonHeight, Text.translatable(groupByCategory ? "button.entity-outliner.categories" : "button.entity-outliner.no-categories"), (button) -> { 63 | // groupByCategory = !groupByCategory; 64 | // this.onSearchFieldUpdate(this.searchField.getText()); 65 | // button.setMessage(Text.translatable(groupByCategory ? "button.entity-outliner.categories" : "button.entity-outliner.no-categories")); 66 | // })); 67 | 68 | this.addDrawableChild( 69 | ButtonWidget.builder( 70 | Text.translatable(groupByCategory ? "button.entity-outliner.categories" : "button.entity-outliner.no-categories"), 71 | (button) -> { 72 | groupByCategory = !groupByCategory; 73 | this.onSearchFieldUpdate(this.searchField.getText()); 74 | button.setMessage(Text.translatable(groupByCategory ? "button.entity-outliner.categories" : "button.entity-outliner.no-categories")); 75 | } 76 | ).size(buttonWidth, buttonHeight).position(buttonOffset, buttonY).build() 77 | ); 78 | 79 | // Add Deselect All button 80 | // this.addDrawableChild(new ButtonWidget(buttonOffset + (buttonWidth + buttonInterval), buttonY, buttonWidth, buttonHeight, Text.translatable("button.entity-outliner.deselect"), (button) -> { 81 | // outlinedEntityTypes.clear(); 82 | // this.onSearchFieldUpdate(this.searchField.getText()); 83 | // })); 84 | 85 | this.addDrawableChild( 86 | ButtonWidget.builder( 87 | Text.translatable("button.entity-outliner.deselect"), 88 | (button) -> { 89 | outlinedEntityTypes.clear(); 90 | this.onSearchFieldUpdate(this.searchField.getText()); 91 | } 92 | ).size(buttonWidth, buttonHeight).position(buttonOffset + (buttonWidth + buttonInterval), buttonY).build() 93 | ); 94 | 95 | // Add toggle outlining button 96 | // this.addDrawableChild(new ButtonWidget(buttonOffset + (buttonWidth + buttonInterval) * 2, buttonY, buttonWidth, buttonHeight, Text.translatable(EntityOutliner.outliningEntities ? "button.entity-outliner.on" : "button.entity-outliner.off"), (button) -> { 97 | // EntityOutliner.outliningEntities = !EntityOutliner.outliningEntities; 98 | // button.setMessage(Text.translatable(EntityOutliner.outliningEntities ? "button.entity-outliner.on" : "button.entity-outliner.off")); 99 | // })); 100 | 101 | this.addDrawableChild( 102 | ButtonWidget.builder( 103 | Text.translatable(EntityOutliner.outliningEntities ? "button.entity-outliner.on" : "button.entity-outliner.off"), 104 | (button) -> { 105 | EntityOutliner.outliningEntities = !EntityOutliner.outliningEntities; 106 | button.setMessage(Text.translatable(EntityOutliner.outliningEntities ? "button.entity-outliner.on" : "button.entity-outliner.off")); 107 | } 108 | ).size(buttonWidth, buttonHeight).position(buttonOffset + (buttonWidth + buttonInterval) * 2, buttonY).build() 109 | ); 110 | 111 | // Add Done button 112 | // this.addDrawableChild(new ButtonWidget(buttonOffset + (buttonWidth + buttonInterval) * 3, buttonY, buttonWidth, buttonHeight, Text.translatable("button.entity-outliner.done"), (button) -> { 113 | // this.client.setScreen(null); 114 | // })); 115 | 116 | this.addDrawableChild( 117 | ButtonWidget.builder( 118 | Text.translatable("button.entity-outliner.done"), 119 | (button) -> { this.client.setScreen(null); } 120 | ).size(buttonWidth, buttonHeight).position(buttonOffset + (buttonWidth + buttonInterval) * 3, buttonY).build() 121 | ); 122 | 123 | this.setInitialFocus(this.searchField); 124 | this.onSearchFieldUpdate(this.searchField.getText()); 125 | } 126 | 127 | // Initializes the prefix tree used for searching in the entity selector screen 128 | private void initializePrefixTree() { 129 | EntitySelector.searcher = new HashMap<>(); 130 | 131 | // Initialize no-text results 132 | List> allResults = new ArrayList>(); 133 | EntitySelector.searcher.put("", allResults); 134 | 135 | // Get sorted list of entity types 136 | List> entityTypes = new ArrayList<>(); 137 | for (EntityType entityType : Registries.ENTITY_TYPE) { 138 | entityTypes.add(entityType); 139 | } 140 | entityTypes.sort(new Comparator>() { 141 | @Override 142 | public int compare(EntityType o1, EntityType o2) { 143 | return o1.getName().getString().compareTo(o2.getName().getString()); 144 | } 145 | }); 146 | 147 | // Add each entity type to everywhere it belongs in the prefix "tree" 148 | for (EntityType entityType : entityTypes) { 149 | 150 | String name = entityType.getName().getString().toLowerCase(); 151 | allResults.add(entityType); 152 | 153 | List prefixes = new ArrayList<>(); 154 | prefixes.add(""); 155 | 156 | // By looping over the name's length, we add to every possible prefix 157 | for (int i = 0; i < name.length(); i++) { 158 | char character = name.charAt(i); 159 | 160 | // Loop over every prefix 161 | for (int p = 0; p < prefixes.size(); p++) { 162 | String prefix = prefixes.get(p) + character; 163 | prefixes.set(p, prefix); 164 | 165 | // Get results for current prefix 166 | List> results; 167 | if (EntitySelector.searcher.containsKey(prefix)) { 168 | results = EntitySelector.searcher.get(prefix); 169 | } else { 170 | results = new ArrayList>(); 171 | EntitySelector.searcher.put(prefix, results); 172 | } 173 | 174 | results.add(entityType); 175 | } 176 | 177 | // Add another prefix to allow searching by second/third/... word 178 | if (Character.isWhitespace(character)) { 179 | prefixes.add(""); 180 | } 181 | } 182 | } 183 | } 184 | 185 | // Callback provided to TextFieldWidget triggered when its text updates 186 | private void onSearchFieldUpdate(String text) { 187 | searchText = text; 188 | text = text.toLowerCase().trim(); 189 | 190 | this.list.clearListEntries(); 191 | 192 | if (searcher.containsKey(text)) { 193 | List> results = searcher.get(text); 194 | 195 | // Splits results into categories and separates them with headers 196 | if (groupByCategory) { 197 | HashMap>> resultsByCategory = new HashMap<>(); 198 | 199 | for (EntityType entityType : results) { 200 | SpawnGroup category = entityType.getSpawnGroup(); 201 | if (!resultsByCategory.containsKey(category)) { 202 | resultsByCategory.put(category, new ArrayList<>()); 203 | } 204 | 205 | resultsByCategory.get(category).add(entityType); 206 | } 207 | 208 | for (SpawnGroup category : SpawnGroup.values()) { 209 | if (resultsByCategory.containsKey(category)) { 210 | this.list.addListEntry(EntityListWidget.HeaderEntry.create(category, this.client.textRenderer, this.width, 25)); 211 | 212 | for (EntityType entityType : resultsByCategory.get(category)) { 213 | this.list.addListEntry(EntityListWidget.EntityEntry.create(entityType, this.width)); 214 | } 215 | 216 | } 217 | } 218 | 219 | } else { 220 | for (EntityType entityType : results) { 221 | this.list.addListEntry(EntityListWidget.EntityEntry.create(entityType, this.width)); 222 | } 223 | } 224 | } else { // If there are no results, let the user know 225 | this.list.addListEntry(EntityListWidget.HeaderEntry.create(null, this.client.textRenderer, this.width, 25)); 226 | } 227 | 228 | // This prevents an overscroll when the user is already scrolled down and the results list is shortened 229 | this.list.setScrollAmount(this.list.getScrollAmount()); 230 | } 231 | 232 | // Called when config screen is escaped 233 | public void removed() { 234 | EntityOutliner.saveConfig(); 235 | } 236 | 237 | public void tick() { 238 | this.searchField.tick(); 239 | } 240 | 241 | public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { 242 | // Render dirt background 243 | this.renderBackground(matrices); 244 | 245 | // Render scrolling list 246 | this.list.render(matrices, mouseX, mouseY, delta); 247 | 248 | // Render our search bar 249 | this.setFocused(this.searchField); 250 | this.searchField.setTextFieldFocused(true); 251 | this.searchField.render(matrices, mouseX, mouseY, delta); 252 | 253 | // Render buttons 254 | super.render(matrices, mouseX, mouseY, delta); 255 | } 256 | 257 | // Sends mouseDragged event to the scrolling list 258 | public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { 259 | return this.list.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/main/java/net/entityoutliner/ui/ModMenuIntegration.java: -------------------------------------------------------------------------------- 1 | package net.entityoutliner.ui; 2 | 3 | import com.terraformersmc.modmenu.api.ConfigScreenFactory; 4 | import com.terraformersmc.modmenu.api.ModMenuApi; 5 | 6 | public class ModMenuIntegration implements ModMenuApi { 7 | @Override 8 | public ConfigScreenFactory getModConfigScreenFactory() { 9 | return EntitySelector::new; 10 | } 11 | } -------------------------------------------------------------------------------- /src/main/resources/assets/entityoutliner/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamviola/EntityOutliner/2716cd8434f482745032d82045d826013bf8d055/src/main/resources/assets/entityoutliner/icon.png -------------------------------------------------------------------------------- /src/main/resources/assets/entityoutliner/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "key.entity-outliner.selector": "Open Entity Selector", 3 | "key.entity-outliner.outline": "Toggle Entity Outline", 4 | "title.entity-outliner.selector": "Entity Selector", 5 | "title.entity-outliner.title": "Entity Outliner", 6 | "button.entity-outliner.deselect": "Deselect All", 7 | "button.entity-outliner.done": "Done", 8 | "button.entity-outliner.categories": "Categories", 9 | "button.entity-outliner.no-categories": "No Categories", 10 | "button.entity-outliner.on": "Toggled On", 11 | "button.entity-outliner.off": "Toggled Off", 12 | "gui.entity-outliner.no_results": "No results" 13 | } -------------------------------------------------------------------------------- /src/main/resources/assets/entityoutliner/lang/fr_fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "key.entity-outliner.selector": "Ouvrir le sélecteur d'entités", 3 | "key.entity-outliner.outline": "Activer le contour d'entités", 4 | "title.entity-outliner.selector": "Sélecteur d'entités", 5 | "title.entity-outliner.title": "Sélecteur d'entités (Entity Outliner)", 6 | "button.entity-outliner.deselect": "Tout déselectionner", 7 | "button.entity-outliner.done": "Terminer", 8 | "button.entity-outliner.categories": "Catégories", 9 | "button.entity-outliner.no-categories": "Aucune catégories", 10 | "button.entity-outliner.on": "Activer", 11 | "button.entity-outliner.off": "Désactiver", 12 | "gui.entity-outliner.no_results": "Aucun résultat" 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/assets/entityoutliner/lang/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "key.entity-outliner.selector": "打开实体选择器", 3 | "key.entity-outliner.outline": "实体轮廓显示开关", 4 | "title.entity-outliner.selector": "实体选择器", 5 | "title.entity-outliner.title": "实体轮廓显示(Entity Outliner)", 6 | "button.entity-outliner.deselect": "取消选择", 7 | "button.entity-outliner.done": "完成", 8 | "button.entity-outliner.categories": "有分类", 9 | "button.entity-outliner.no-categories": "无分类", 10 | "button.entity-outliner.on": "已打开", 11 | "button.entity-outliner.off": "已关闭", 12 | "gui.entity-outliner.no_results": "无结果" 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/assets/entityoutliner/textures/gui/colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamviola/EntityOutliner/2716cd8434f482745032d82045d826013bf8d055/src/main/resources/assets/entityoutliner/textures/gui/colors.png -------------------------------------------------------------------------------- /src/main/resources/entityoutliner.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "net.entityoutliner.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "mixins": [ 7 | ], 8 | "client": [ 9 | "MixinMinecraftClient", 10 | "MixinWorldRenderer" 11 | ], 12 | "injectors": { 13 | "defaultRequire": 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "entityoutliner", 4 | "version": "${version}", 5 | 6 | "name": "Entity Outliner", 7 | "description": "Allows you to select entity types to outline, making them visible through all obstructions within render distance.", 8 | "authors": [ 9 | "adamviola" 10 | ], 11 | "contact": { 12 | "homepage": "https://github.com/adamviola/EntityOutliner", 13 | "sources": "https://github.com/adamviola/EntityOutliner" 14 | }, 15 | 16 | "license": "MIT", 17 | "icon": "assets/entityoutliner/icon.png", 18 | 19 | "environment": "client", 20 | "entrypoints": { 21 | "client": [ 22 | "net.entityoutliner.EntityOutliner" 23 | ], 24 | "modmenu": [ 25 | "net.entityoutliner.ui.ModMenuIntegration" 26 | ] 27 | }, 28 | "mixins": [ 29 | { 30 | "config": "entityoutliner.mixins.json", 31 | "environment": "client" 32 | } 33 | ], 34 | 35 | "depends": { 36 | "fabricloader": ">=0.7.4", 37 | "fabric": "*", 38 | "fabric-lifecycle-events-v1": "*", 39 | "fabric-key-binding-api-v1": "*", 40 | "minecraft": ">=1.19" 41 | }, 42 | "suggests": { 43 | "flamingo": "*" 44 | } 45 | } 46 | --------------------------------------------------------------------------------