├── gradle.properties ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.conf ├── settings.gradle.kts ├── velocity ├── src │ └── main │ │ └── java │ │ └── com │ │ └── nearvanilla │ │ └── bat │ │ └── velocity │ │ ├── tab │ │ ├── group │ │ │ ├── EmptyGroupProvider.java │ │ │ ├── GroupProvider.java │ │ │ └── LuckPermsGroupProvider.java │ │ ├── Tablist.java │ │ ├── ServerDataProvider.java │ │ └── TablistService.java │ │ ├── listener │ │ └── LuckPermsListener.java │ │ ├── config │ │ ├── PluginConfig.java │ │ ├── TablistConfig.java │ │ └── ConfigLoader.java │ │ ├── BatVelocityPlugin.java │ │ └── command │ │ └── Commands.java └── build.gradle.kts ├── .github └── workflows │ └── build.yml ├── .gitignore ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /gradle.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NearVanilla/bat/HEAD/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-8.11-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "bat" 2 | 3 | include("velocity") 4 | project(":velocity").name = "bat-velocity" 5 | 6 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 7 | 8 | pluginManagement { 9 | repositories { 10 | gradlePluginPortal() 11 | maven("https://repo.stellardrift.ca/repository/snapshots/") 12 | } 13 | } 14 | 15 | plugins { 16 | id("ca.stellardrift.polyglot-version-catalogs") version "6.2.0" 17 | } 18 | -------------------------------------------------------------------------------- /velocity/src/main/java/com/nearvanilla/bat/velocity/tab/group/EmptyGroupProvider.java: -------------------------------------------------------------------------------- 1 | package com.nearvanilla.bat.velocity.tab.group; 2 | 3 | import org.checkerframework.checker.nullness.qual.NonNull; 4 | 5 | import java.util.Collection; 6 | import java.util.List; 7 | import java.util.UUID; 8 | 9 | /** 10 | * {@code EmptyGroupProvider} returns an empty {@link Collection} when queried. 11 | */ 12 | public class EmptyGroupProvider implements GroupProvider { 13 | 14 | @Override 15 | public @NonNull Collection groups(final @NonNull UUID uuid) { 16 | return List.of(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /velocity/src/main/java/com/nearvanilla/bat/velocity/tab/group/GroupProvider.java: -------------------------------------------------------------------------------- 1 | package com.nearvanilla.bat.velocity.tab.group; 2 | 3 | import org.checkerframework.checker.nullness.qual.NonNull; 4 | 5 | import java.util.Collection; 6 | import java.util.UUID; 7 | 8 | /** 9 | * {@code GroupProvider} defines methods to retrieve a user's inherited and primary groups. 10 | */ 11 | public interface GroupProvider { 12 | 13 | /** 14 | * Returns a {@link Collection} containing the IDs of all groups a player is a member of. 15 | * 16 | * @param uuid the uuid of a player 17 | * @return a {@link Collection}, will be empty if there is no group data for the provided {@link UUID} 18 | */ 19 | @NonNull Collection groups(final @NonNull UUID uuid); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | gradle-build: 8 | name: "Build Gradle project" 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout Git repository 12 | uses: actions/checkout@v6 13 | - name: Validate Gradle Wrapper 14 | uses: gradle/wrapper-validation-action@v1 15 | - name: Setup JDK 16 | uses: actions/setup-java@v5 17 | with: 18 | distribution: temurin 19 | java-version: 21 20 | - name: Setup Gradle 21 | uses: gradle/actions/setup-gradle@v5 22 | - name: Build with Gradle 23 | run: ./gradlew build 24 | - name: Capture Gradle build artifacts 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: gradle-build-artifacts 28 | path: "build/libs/*.jar" 29 | -------------------------------------------------------------------------------- /gradle/libs.versions.conf: -------------------------------------------------------------------------------- 1 | metadata = { 2 | format = { version = "1.0" } 3 | polyglot-extensions = [plugins] 4 | } 5 | 6 | plugins = { 7 | "net.kyori.indra" = "3.2.0" 8 | "net.kyori.indra.checkstyle" = "3.0.1" 9 | "com.gradleup.shadow" = "9.0.0" 10 | } 11 | 12 | versions = { 13 | velocity = "3.4.0-SNAPSHOT" 14 | configurate = "4.2.0" 15 | cloud = "1.8.4" 16 | luckperms = "5.5" 17 | } 18 | 19 | dependencies = { 20 | velocity-api = { group = "com.velocitypowered", name = "velocity-api", version.ref = "velocity" } 21 | configurate-hocon = { group = "org.spongepowered", name = "configurate-hocon", version.ref = "configurate" } 22 | cloud-velocity = { group = "cloud.commandframework", name = "cloud-velocity", version.ref = "cloud" } 23 | cloud-annotations = { group = "cloud.commandframework", name = "cloud-annotations", version.ref = "cloud" } 24 | luckperms = { group = "net.luckperms", name = "api", version.ref = "luckperms" } 25 | } 26 | -------------------------------------------------------------------------------- /velocity/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | compileOnly(libs.velocity.api) 3 | annotationProcessor(libs.velocity.api) 4 | 5 | compileOnly(libs.luckperms) 6 | 7 | implementation(libs.cloud.velocity) 8 | implementation(libs.cloud.annotations) 9 | implementation(libs.configurate.hocon) 10 | } 11 | 12 | tasks { 13 | build { 14 | dependsOn(named("shadowJar")) 15 | } 16 | 17 | compileJava { 18 | sourceCompatibility = "21" 19 | targetCompatibility = "21" 20 | 21 | // Suppress annotation processing warnings 22 | options.compilerArgs.add("-Xlint:-processing") 23 | } 24 | 25 | shadowJar { 26 | enableAutoRelocation = true 27 | relocationPrefix = "${rootProject.property("group")}.${rootProject.property("name").toString().lowercase()}.lib" 28 | minimize() 29 | archiveClassifier.set("") 30 | // Exclude duplicate meta files to fix remap errors 31 | exclude("META-INF/LICENSE") 32 | exclude("META-INF/NOTICE") 33 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /velocity/src/main/java/com/nearvanilla/bat/velocity/listener/LuckPermsListener.java: -------------------------------------------------------------------------------- 1 | package com.nearvanilla.bat.velocity.listener; 2 | 3 | import com.google.inject.Inject; 4 | import com.nearvanilla.bat.velocity.tab.TablistService; 5 | import com.velocitypowered.api.event.Subscribe; 6 | import com.velocitypowered.api.proxy.ProxyServer; 7 | import net.luckperms.api.event.user.UserDataRecalculateEvent; 8 | import org.checkerframework.checker.nullness.qual.NonNull; 9 | 10 | public class LuckPermsListener { 11 | 12 | private final @NonNull ProxyServer server; 13 | private final @NonNull TablistService tablistService; 14 | 15 | @Inject 16 | public LuckPermsListener(final @NonNull ProxyServer server, 17 | final @NonNull TablistService tablistService) { 18 | this.server = server; 19 | this.tablistService = tablistService; 20 | } 21 | 22 | @Subscribe 23 | public void onGroupChange(final @NonNull UserDataRecalculateEvent event) { 24 | server.getPlayer(event.getUser().getUniqueId()).ifPresent(this.tablistService::handleServerConnection); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /velocity/src/main/java/com/nearvanilla/bat/velocity/tab/group/LuckPermsGroupProvider.java: -------------------------------------------------------------------------------- 1 | package com.nearvanilla.bat.velocity.tab.group; 2 | 3 | import net.luckperms.api.LuckPerms; 4 | import net.luckperms.api.model.group.Group; 5 | import net.luckperms.api.model.user.User; 6 | import net.luckperms.api.query.Flag; 7 | import net.luckperms.api.query.QueryOptions; 8 | import org.checkerframework.checker.nullness.qual.NonNull; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.UUID; 13 | 14 | import static java.util.stream.Collectors.toUnmodifiableList; 15 | 16 | /** 17 | * {@code LuckPermsGroupProvider} utilizes the {@link LuckPerms} API to retrieve a user's group information. 18 | */ 19 | public class LuckPermsGroupProvider implements GroupProvider { 20 | 21 | private final @NonNull LuckPerms luckPerms; 22 | 23 | /** 24 | * Constructs {@code LuckPermsGroupProvider}. 25 | * 26 | * @param luckPerms the {@link LuckPerms} API instance 27 | */ 28 | public LuckPermsGroupProvider(final @NonNull LuckPerms luckPerms) { 29 | this.luckPerms = luckPerms; 30 | } 31 | 32 | /** 33 | * Returns a player's groups. 34 | * 35 | * @param uuid the uuid 36 | * @return the collection of group names 37 | */ 38 | @Override 39 | public @NonNull Collection groups(final @NonNull UUID uuid) { 40 | final User user = this.luckPerms.getUserManager().getUser(uuid); 41 | 42 | if (user == null) { 43 | return new ArrayList<>(); 44 | } 45 | 46 | return user.getInheritedGroups(QueryOptions.defaultContextualOptions().toBuilder().flag(Flag.RESOLVE_INHERITANCE, true).build()) 47 | .stream() 48 | .map(Group::getName) 49 | .toList(); 50 | } 51 | 52 | 53 | } 54 | -------------------------------------------------------------------------------- /velocity/src/main/java/com/nearvanilla/bat/velocity/config/PluginConfig.java: -------------------------------------------------------------------------------- 1 | package com.nearvanilla.bat.velocity.config; 2 | 3 | import org.checkerframework.checker.nullness.qual.NonNull; 4 | import org.spongepowered.configurate.objectmapping.meta.Comment; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | /** 10 | * Contains the plugin's configuration. 11 | */ 12 | public class PluginConfig { 13 | 14 | @Comment(""" 15 | Controls how many milliseconds should pass between tablist updates. 16 | """) 17 | public long updateFrequency = 1000; 18 | 19 | @Comment(""" 20 | Stores tablist configuration. 21 | """) 22 | public @NonNull Map tablists = Map.of( 23 | "default", new TablistConfig() 24 | ); 25 | 26 | @Comment(""" 27 | The default tablist to show players. 28 | """) 29 | public @NonNull String defaultTablist = "default"; 30 | 31 | @Comment(""" 32 | The format of a player's name in the tablist. Supports MiniMessage and every bat placeholder. 33 | """) 34 | public @NonNull String playerNameFormat = ""; 35 | 36 | @Comment(""" 37 | If a player is in a LuckPerms group defined in this map, then the template "" will return it's 38 | value in this map. Otherwise, "" will return nothing (""). 39 | """) 40 | public @NonNull Map groupCodes = Map.of( 41 | "admin", "A " 42 | ); 43 | 44 | @Comment(""" 45 | If a player is present on a server defined in this map, then the template "" will return it's 46 | value in this map. Otherwise, "" will return nothing (""). 47 | """) 48 | public @NonNull Map serverCodes = Map.of( 49 | "survival", " [S]" 50 | ); 51 | 52 | } 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | run/ 116 | 117 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 118 | !gradle-wrapper.jar 119 | -------------------------------------------------------------------------------- /velocity/src/main/java/com/nearvanilla/bat/velocity/config/TablistConfig.java: -------------------------------------------------------------------------------- 1 | package com.nearvanilla.bat.velocity.config; 2 | 3 | import org.checkerframework.checker.nullness.qual.NonNull; 4 | import org.spongepowered.configurate.objectmapping.ConfigSerializable; 5 | import org.spongepowered.configurate.objectmapping.meta.Comment; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Holds configuration data for a tab list. 11 | */ 12 | @ConfigSerializable 13 | public class TablistConfig { 14 | 15 | @Comment(""" 16 | A list of strings (with MiniMessage formatting) to use as the tablist's header. 17 | """) 18 | public @NonNull List headerFormatStrings = List.of( 19 | "", 20 | "bat", 21 | "", 22 | "The basic, awesome tablist plugin.", 23 | "", 24 | "Current time: ", 25 | "" 26 | ); 27 | 28 | @Comment(""" 29 | A list of strings (with MiniMessage formatting) to use as the tablist's footer. 30 | """) 31 | public @NonNull List footerFormatStrings = List.of( 32 | "", 33 | "bat supports a variety of placeholders.", 34 | "This is an incomplete list of all supported placeholders. Read the", 35 | "documentation for a full breakdown of all placeholders.", 36 | "", 37 | "\\<proxycount> - ", 38 | "\\<proxymax> - ", 39 | "\\<proxymotd> - ", 40 | "\\<servercount> - ", 41 | "\\<servermax> - ", 42 | "\\<servermotd> - ", 43 | "\\<playerping> - ms", 44 | "\\<playeruuid> - ", 45 | "\\<playername> - ", 46 | "\\<playerip> - ", 47 | "" 48 | ); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /velocity/src/main/java/com/nearvanilla/bat/velocity/BatVelocityPlugin.java: -------------------------------------------------------------------------------- 1 | package com.nearvanilla.bat.velocity; 2 | 3 | import com.google.inject.Inject; 4 | import com.google.inject.Injector; 5 | import com.nearvanilla.bat.velocity.command.Commands; 6 | import com.nearvanilla.bat.velocity.listener.LuckPermsListener; 7 | import com.nearvanilla.bat.velocity.tab.TablistService; 8 | import com.velocitypowered.api.event.Subscribe; 9 | import com.velocitypowered.api.event.connection.DisconnectEvent; 10 | import com.velocitypowered.api.event.player.ServerPostConnectEvent; 11 | import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; 12 | import com.velocitypowered.api.plugin.Plugin; 13 | import com.velocitypowered.api.proxy.ProxyServer; 14 | import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 15 | import org.checkerframework.checker.nullness.qual.NonNull; 16 | import org.slf4j.Logger; 17 | 18 | /** 19 | * bat's Velocity entrypoint. 20 | */ 21 | @Plugin( 22 | id = "bat", 23 | name = "bat", 24 | version = "1.1.1", 25 | authors = {"Bluely_", "Prof_Bloodstone"}, 26 | description = "Basic, awesome TAB plugin", 27 | url = "nearvanilla.com" 28 | ) 29 | public class BatVelocityPlugin { 30 | 31 | private final @NonNull ProxyServer server; 32 | private final @NonNull Logger logger; 33 | private final @NonNull Injector injector; 34 | 35 | private @MonotonicNonNull TablistService tablistService; 36 | private @MonotonicNonNull Commands commands; 37 | 38 | /** 39 | * Constructs {@code BatVelocityPlugin}. 40 | * 41 | * @param logger the logger 42 | */ 43 | @Inject 44 | public BatVelocityPlugin(final @NonNull Logger logger, 45 | final @NonNull ProxyServer server, 46 | final @NonNull Injector injector) { 47 | this.server = server; 48 | this.logger = logger; 49 | this.injector = injector; 50 | } 51 | 52 | @Subscribe 53 | public void onProxyInitialization(final @NonNull ProxyInitializeEvent event) { 54 | this.enable(); 55 | } 56 | 57 | @Subscribe 58 | public void onPlayerSwitch(final @NonNull ServerPostConnectEvent event) { 59 | this.tablistService.handleServerConnection(event.getPlayer()); 60 | } 61 | 62 | @Subscribe 63 | public void onPlayerQuit(final @NonNull DisconnectEvent event) { 64 | this.tablistService.handlePlayerLeave(event.getPlayer()); 65 | } 66 | 67 | public void enable() { 68 | this.tablistService = this.injector.getInstance(TablistService.class); 69 | this.tablistService.enable(); 70 | 71 | this.commands = this.injector.getInstance(Commands.class); 72 | this.commands.register(); 73 | 74 | if (this.server.getPluginManager().isLoaded("LuckPerms")) { 75 | this.server.getEventManager().register(this, this.injector.getInstance(LuckPermsListener.class)); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /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 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /velocity/src/main/java/com/nearvanilla/bat/velocity/command/Commands.java: -------------------------------------------------------------------------------- 1 | package com.nearvanilla.bat.velocity.command; 2 | 3 | import cloud.commandframework.Command; 4 | import cloud.commandframework.annotations.AnnotationParser; 5 | import cloud.commandframework.annotations.CommandDescription; 6 | import cloud.commandframework.annotations.CommandMethod; 7 | import cloud.commandframework.annotations.CommandPermission; 8 | import cloud.commandframework.meta.SimpleCommandMeta; 9 | import com.google.inject.Inject; 10 | import com.nearvanilla.bat.velocity.BatVelocityPlugin; 11 | import com.nearvanilla.bat.velocity.config.ConfigLoader; 12 | import com.nearvanilla.bat.velocity.tab.TablistService; 13 | import com.velocitypowered.api.proxy.ProxyServer; 14 | import net.kyori.adventure.text.Component; 15 | import net.kyori.adventure.text.format.NamedTextColor; 16 | import org.checkerframework.checker.nullness.qual.NonNull; 17 | import cloud.commandframework.velocity.VelocityCommandManager; 18 | import cloud.commandframework.CommandManager; 19 | import cloud.commandframework.execution.CommandExecutionCoordinator; 20 | import com.velocitypowered.api.command.CommandSource; 21 | 22 | import java.util.function.Function; 23 | 24 | /** 25 | * Manages commands 26 | */ 27 | public class Commands { 28 | private final static @NonNull Component CONFIG_RELOAD_SUCCESS = Component.text("Config reloaded successfully", NamedTextColor.GREEN); 29 | private final static @NonNull Component CONFIG_RELOAD_FAILURE = Component.text("Error reloading config - see console for details", NamedTextColor.RED); 30 | private final @NonNull BatVelocityPlugin plugin; 31 | private final @NonNull CommandManager manager; 32 | private final @NonNull ConfigLoader configLoader; 33 | private final @NonNull TablistService tablistService; 34 | 35 | @Inject 36 | public Commands(final @NonNull BatVelocityPlugin plugin, 37 | final @NonNull ConfigLoader configLoader, 38 | final @NonNull TablistService tablistService, 39 | final @NonNull ProxyServer server) { 40 | this.plugin = plugin; 41 | this.configLoader = configLoader; 42 | this.tablistService = tablistService; 43 | this.manager = new VelocityCommandManager<>( 44 | server.getPluginManager().ensurePluginContainer(plugin), 45 | server, 46 | CommandExecutionCoordinator.simpleCoordinator(), 47 | Function.identity(), 48 | Function.identity() 49 | ); 50 | } 51 | 52 | /** 53 | * Registers the commands 54 | */ 55 | public void register() { 56 | AnnotationParser annotationParser; 57 | annotationParser = new AnnotationParser<>( 58 | manager, CommandSource.class, parserParams -> SimpleCommandMeta.empty() 59 | ); 60 | // Parse commands 61 | annotationParser.parse(this); 62 | } 63 | 64 | 65 | @CommandMethod("bat reload") 66 | @CommandPermission("bat.command.reload") 67 | @CommandDescription("Reload BATs config") 68 | private void reloadConfig(final @NonNull CommandSource source) { 69 | try { 70 | this.configLoader.reloadConfig(); 71 | this.tablistService.enable(); 72 | } catch (RuntimeException e) { 73 | source.sendMessage(CONFIG_RELOAD_FAILURE); 74 | throw e; 75 | } 76 | source.sendMessage(CONFIG_RELOAD_SUCCESS); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /velocity/src/main/java/com/nearvanilla/bat/velocity/config/ConfigLoader.java: -------------------------------------------------------------------------------- 1 | package com.nearvanilla.bat.velocity.config; 2 | 3 | import com.google.inject.Inject; 4 | import com.google.inject.Singleton; 5 | import com.nearvanilla.bat.velocity.BatVelocityPlugin; 6 | import com.velocitypowered.api.plugin.annotation.DataDirectory; 7 | import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 8 | import org.checkerframework.checker.nullness.qual.NonNull; 9 | import org.spongepowered.configurate.CommentedConfigurationNode; 10 | import org.spongepowered.configurate.hocon.HoconConfigurationLoader; 11 | import org.spongepowered.configurate.objectmapping.ObjectMapper; 12 | 13 | import java.io.File; 14 | import java.nio.file.Path; 15 | 16 | /** 17 | * Loads and provides configuration objects. 18 | */ 19 | @Singleton 20 | public class ConfigLoader { 21 | 22 | private static final @NonNull String CONFIG_HEADER = 23 | """ 24 | bat - tablist.conf 25 | ------------------ 26 | This configuration uses MiniMessage formatting for strings. 27 | In addition to the default MiniMessage tags, the following tags are supported: 28 | - - the total players connected to the proxy 29 | - - the max amount of players able to be connected to the proxy 30 | - - the proxy's motd 31 | - - the total players connected to the player's current server 32 | - - the max amount of players able to be connected to the server 33 | - - the server's motd 34 | - - the player's ping 35 | - - the player's uuid 36 | - - the player's name 37 | - - the player's ip 38 | - - the player's connected server name 39 | """; 40 | 41 | private final @NonNull BatVelocityPlugin plugin; 42 | private final @NonNull Path dataDirectory; 43 | private @MonotonicNonNull PluginConfig pluginConfig; 44 | 45 | @Inject 46 | public ConfigLoader(final @NonNull BatVelocityPlugin plugin, 47 | final @NonNull @DataDirectory Path dataDirectory) { 48 | this.plugin = plugin; 49 | this.dataDirectory = dataDirectory; 50 | } 51 | 52 | /** 53 | * Reloads {@link PluginConfig} 54 | */ 55 | public void reloadConfig() { 56 | this.pluginConfig = this.loadConfiguration(); 57 | } 58 | 59 | /** 60 | * Returns the {@link PluginConfig}. 61 | * 62 | * @return the config 63 | */ 64 | public @NonNull PluginConfig batConfig() { 65 | if (this.pluginConfig == null) { 66 | reloadConfig(); 67 | } 68 | 69 | return this.pluginConfig; 70 | } 71 | 72 | private @NonNull PluginConfig loadConfiguration() { 73 | final File configFile = new File(dataDirectory.toFile(), "bat.conf"); 74 | 75 | final @NonNull HoconConfigurationLoader loader = HoconConfigurationLoader 76 | .builder() 77 | .defaultOptions(opts -> opts 78 | .shouldCopyDefaults(true) 79 | .header(CONFIG_HEADER) 80 | ) 81 | .file(configFile) 82 | .build(); 83 | 84 | 85 | try { 86 | @NonNull CommentedConfigurationNode node = loader.load(); 87 | final PluginConfig config = ObjectMapper.factory().get(PluginConfig.class).load(node); 88 | if (config == null) { 89 | throw new RuntimeException("Config is null"); 90 | } 91 | loader.save(node); 92 | return config; 93 | } catch (Exception e) { 94 | throw new RuntimeException(e); 95 | } 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /velocity/src/main/java/com/nearvanilla/bat/velocity/tab/Tablist.java: -------------------------------------------------------------------------------- 1 | package com.nearvanilla.bat.velocity.tab; 2 | 3 | import com.velocitypowered.api.proxy.Player; 4 | import com.velocitypowered.api.proxy.player.TabList; 5 | import com.velocitypowered.api.proxy.player.TabListEntry; 6 | import com.velocitypowered.api.util.GameProfile; 7 | import org.checkerframework.checker.nullness.qual.NonNull; 8 | 9 | import java.util.*; 10 | import java.util.logging.Logger; 11 | 12 | public class Tablist { 13 | 14 | private final @NonNull Logger logger; 15 | private final @NonNull TablistService tablistService; 16 | private final @NonNull ServerDataProvider serverDataProvider; 17 | private final @NonNull List headerFormatStrings; 18 | private final @NonNull List footerFormatStrings; 19 | private final @NonNull Collection profileEntries; 20 | 21 | /** 22 | * Constructs {@code Tablist}. 23 | * 24 | * @param logger the logger 25 | * @param tablistService the tablist service 26 | * @param serverDataProvider the server data provider 27 | * @param headerFormatStrings a list containing the tablist's header 28 | * @param footerFormatStrings a list containing the tablist's footer 29 | */ 30 | public Tablist(final @NonNull Logger logger, 31 | final @NonNull TablistService tablistService, 32 | final @NonNull ServerDataProvider serverDataProvider, 33 | final @NonNull List headerFormatStrings, 34 | final @NonNull List footerFormatStrings) { 35 | this.logger = logger; 36 | this.tablistService = tablistService; 37 | this.serverDataProvider = serverDataProvider; 38 | this.headerFormatStrings = headerFormatStrings; 39 | this.footerFormatStrings = footerFormatStrings; 40 | this.profileEntries = Collections.synchronizedCollection(new ArrayList<>()); 41 | } 42 | 43 | /** 44 | * Adds a player to the tablist. 45 | * 46 | * @param player the player 47 | */ 48 | public void addPlayer(final @NonNull Player player) { 49 | this.profileEntries.add(player.getGameProfile()); 50 | } 51 | 52 | /** 53 | * Removes the player from the tablist. 54 | * 55 | * @param player the player 56 | */ 57 | public void removePlayer(final @NonNull Player player) { 58 | synchronized (profileEntries) { 59 | this.profileEntries.removeIf(profile -> profile.getId().equals(player.getUniqueId())); 60 | } 61 | } 62 | 63 | /** 64 | * Generates a list of {@link TabListEntry}s for the tablist. 65 | * 66 | * @param tabList the tablist 67 | * @return the list of tablist entries 68 | */ 69 | public @NonNull List entries(final @NonNull TabList tabList) { 70 | synchronized (profileEntries) { 71 | return profileEntries 72 | .stream() 73 | .sorted(Comparator.comparing(GameProfile::getName)) 74 | .map(gameProfile -> 75 | TabListEntry.builder() 76 | .latency(this.tablistService.ping(gameProfile.getId())) 77 | .tabList(tabList) 78 | .profile(gameProfile) 79 | .displayName(this.tablistService.displayName(gameProfile.getId())) 80 | .gameMode(this.getGameMode(tabList, gameProfile.getId())) 81 | .build() 82 | ).toList(); 83 | } 84 | } 85 | 86 | public @NonNull List headerFormatStrings() { 87 | return this.headerFormatStrings; 88 | } 89 | 90 | public @NonNull List footerFormatStrings() { 91 | return this.footerFormatStrings; 92 | } 93 | 94 | private int getGameMode(final @NonNull TabList tabList, 95 | final @NonNull UUID uuid) { 96 | for (final TabListEntry entry : tabList.getEntries()) { 97 | if (entry.getProfile().getId().equals(uuid)) { 98 | return entry.getGameMode(); 99 | } 100 | } 101 | 102 | this.logger.warning(String.format("Failed to determine GameMode for %s! Returning: 0", uuid)); 103 | 104 | return 0; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bat 2 | 3 | Basic, awesome TAB plugin. 4 | 5 | ## Installation 6 | 7 | #### Requirements 8 | 9 | - Java 17+ 10 | - The latest **development** version of Velocity *(This plugin does not work on the Stable version!)* 11 | - Git (This is only required if you intend on cloning the repository through your Terminal) 12 | 13 | #### Compilation 14 | 15 | - Clone this repository to a directory of your choice through running `git clone https://github.com/NearVanilla/bat.git` in your terminal. Alternatively, you can download this repository as a ZIP at the top of this page. 16 | - Change your directory to the `bat` folder through running `cd bat` 17 | - Run `gradlew build` to compile the project *(Ensure you have the correct version of Java)*. 18 | - Navigate to the `libs` folder inside of the newly created `build` folder in your File Explorer. 19 | - Drag the `bat-velocity.jar` file into the `plugins` folder of your Velocity Server. 20 | - If you do not have a `plugins/bat` folder, then start your Velocity server to generate the default configuration. 21 | - Configure `bat` to your liking! 22 | 23 | 24 | ## Placeholders 25 | 26 | By default, bat provides a set of placeholders using [MiniMessage's template system](https://docs.adventure.kyori.net/minimessage#template). 27 | 28 | ``, `