├── .github └── workflows │ └── build.yaml ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── animation_example.gif ├── nice_example.gif ├── pager_example.gif └── sidebar.gif ├── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── renovate.json ├── run_test_server.sh ├── server └── docker-compose.yaml ├── settings.gradle.kts ├── src ├── main │ └── java │ │ └── me │ │ └── catcoder │ │ └── sidebar │ │ ├── ProtocolSidebar.java │ │ ├── ScoreboardObjective.java │ │ ├── Sidebar.java │ │ ├── SidebarLine.java │ │ ├── pager │ │ ├── PageConsumer.java │ │ └── SidebarPager.java │ │ ├── protocol │ │ ├── ChannelInjector.java │ │ ├── PacketIds.java │ │ ├── ProtocolConstants.java │ │ ├── ScoreNumberFormat.java │ │ └── ScoreboardPackets.java │ │ ├── text │ │ ├── FrameIterator.java │ │ ├── TextFrame.java │ │ ├── TextIterator.java │ │ ├── TextIterators.java │ │ ├── TextProvider.java │ │ ├── impl │ │ │ ├── TextFadeAnimation.java │ │ │ ├── TextSlideAnimation.java │ │ │ └── TextTypingAnimation.java │ │ └── provider │ │ │ ├── AdventureTextProvider.java │ │ │ ├── BungeeCordChatTextProvider.java │ │ │ ├── MiniMessageTextProvider.java │ │ │ └── MiniPlaceholdersTextProvider.java │ │ └── util │ │ ├── NbtComponentSerializer.java │ │ ├── RandomString.java │ │ ├── Reflection.java │ │ ├── buffer │ │ ├── ByteBufNetOutput.java │ │ └── NetOutput.java │ │ ├── lang │ │ ├── ThrowingConsumer.java │ │ ├── ThrowingFunction.java │ │ ├── ThrowingPredicate.java │ │ └── ThrowingSupplier.java │ │ └── version │ │ ├── MinecraftProtocolVersion.java │ │ ├── MinecraftVersion.java │ │ └── VersionUtil.java └── test │ └── java │ └── me │ └── catcoder │ └── sidebar │ └── PacketIdsTest.java └── standalone-plugin ├── .gitignore ├── build.gradle.kts └── src └── main ├── java └── me │ └── catcoder │ └── sidebar │ └── ProtocolSidebarPlugin.java └── resources └── plugin.yml /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [ push ] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Set up JDK 17 13 | uses: actions/setup-java@v3 14 | with: 15 | java-version: '17' 16 | distribution: 'adopt' 17 | cache: 'gradle' 18 | 19 | - name: Build with Gradle 20 | run: ./gradlew test build --no-daemon 21 | 22 | - name: Publish with Gradle 23 | if: github.ref == 'refs/heads/master' 24 | env: 25 | GPG_PASSPHRASE: ${{ secrets.GPG_KEY_PASSPHRASE }} 26 | GPG_SECRET_KEY: ${{ secrets.GPG_SECRET_KEY }} 27 | USERNAME: ${{ secrets.REPO_USER }} 28 | TOKEN: ${{ secrets.REPO_TOKEN }} 29 | 30 | run: ./gradlew publish --no-daemon -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # From https://github.com/github/gitignore/blob/master/Gradle.gitignore 2 | .gradle 3 | /build/ 4 | 5 | # Ignore Gradle GUI config 6 | gradle-app.setting 7 | 8 | /bin/ 9 | 10 | # Cache of project 11 | .gradletasknamecache 12 | 13 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 14 | # gradle/wrapper/gradle-wrapper.properties 15 | 16 | 17 | 18 | # From https://github.com/github/gitignore/blob/master/Java.gitignore 19 | *.class 20 | 21 | # Mobile Tools for Java (J2ME) 22 | .mtj.tmp/ 23 | 24 | # Package Files # 25 | *.jar 26 | *.war 27 | *.ear 28 | 29 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 30 | !gradle-wrapper.jar 31 | 32 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 33 | hs_err_pid* 34 | 35 | 36 | # From https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore 37 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 38 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 39 | 40 | # User-specific stuff: 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/dictionaries 44 | .idea/vcs.xml 45 | .idea/jsLibraryMappings.xml 46 | 47 | # Sensitive or high-churn files: 48 | .idea/dataSources.ids 49 | .idea/dataSources.xml 50 | .idea/dataSources.local.xml 51 | .idea/sqlDataSources.xml 52 | .idea/dynamic.xml 53 | .idea/uiDesigner.xml 54 | 55 | # Gradle: 56 | .idea/gradle.xml 57 | .idea/libraries 58 | 59 | .idea/ 60 | 61 | # Mongo Explorer plugin: 62 | .idea/mongoSettings.xml 63 | 64 | ## File-based project format: 65 | *.iws 66 | 67 | *.iml 68 | 69 | ## Plugin-specific files: 70 | 71 | # IntelliJ 72 | /out/ 73 | 74 | # mpeltonen/sbt-idea plugin 75 | .idea_modules/ 76 | 77 | # JIRA plugin 78 | atlassian-ide-plugin.xml 79 | 80 | # Crashlytics plugin (for Android Studio and IntelliJ) 81 | com_crashlytics_export_strings.xml 82 | crashlytics.properties 83 | crashlytics-build.properties 84 | fabric.properties 85 | 86 | 87 | *.DS_Store 88 | .AppleDouble 89 | .LSOverride 90 | 91 | # Icon must end with two \r 92 | Icon 93 | 94 | 95 | # Thumbnails 96 | ._* 97 | 98 | # Files that might appear in the root of a volume 99 | .DocumentRevisions-V100 100 | .fseventsd 101 | .Spotlight-V100 102 | .TemporaryItems 103 | .Trashes 104 | .VolumeIcon.icns 105 | .com.apple.timemachine.donotpresent 106 | 107 | # Directories potentially created on remote AFP share 108 | .AppleDB 109 | .AppleDesktop 110 | Network Trash Folder 111 | Temporary Items 112 | .apdisk 113 | 114 | server/data 115 | 116 | !docker-compose.yaml 117 | !ProtocolSidebar-*.jar -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2023 CatCoder and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | ProtocolSidebar 3 |

4 |

Powerful feature-packed Minecraft scoreboard library

5 |

6 | 7 |

8 | Build 9 | License 10 | Minecraft Versions 11 |

12 | 13 | * [Features](#features) 14 | * [Adding to your project](#adding-to-your-project) 15 | * [Maven](#maven) 16 | * [Gradle](#gradle) 17 | * [Gradle (Kotlin DSL)](#gradle-kotlin-dsl) 18 | * [Basic usage](#basic-usage) 19 | * [Conditional lines](#conditional-lines) 20 | * [Score number formatting](#score-number-formatting) 21 | * [Sidebar title animations](#sidebar-title-animations) 22 | * [Sidebar Pager](#sidebar-pager) 23 | 24 | ![Sidebar](https://github.com/CatCoderr/ProtocolSidebar/raw/master/assets/sidebar.gif) 25 | 26 | 27 | ## Donations 28 | [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Donate%20Now-yellow?style=for-the-badge&logo=buy-me-a-coffee)](https://www.buymeacoffee.com/catcoderr) 29 | 30 | 31 | ## Features 32 | 33 | * No flickering (without using a buffer) 34 | * Does not require any additional libraries/plugins on the server 35 | * Easy to use 36 | * Folia support 37 | * Optionally supports [Adventure API](https://docs.advntr.dev/text.html), [MiniMessage](https://docs.advntr.dev/minimessage/index.html), [MiniPlaceholders](https://github.com/MiniPlaceholders/MiniPlaceholders) 38 | * Extremely fast, can be used asynchronously 39 | * Cool inbuilt animations 40 | * Inbuilt pager for showing multiple sidebars to the player 41 | * Automatic score management system: sidebar reorders lines automatically 42 | * Everything is at the packet level, so it works with other plugins using scoreboard and/or teams 43 | * Supports up to 30 characters per line on 1.12.2 and below 44 | * No character limit on 1.13 and higher 45 | * Supports hex colors on 1.16 and higher 46 | * Minimized NMS interaction, means that packets are constructed at the byte buffer level and then sent directly to the player's channel. 47 | 48 | ## Adding To Your Project 49 | 50 | Instead of manually bundling the library into your JAR file, you can 51 | use [the standalone plugin](https://github.com/CatCoderr/ProtocolSidebar/tree/master/standalone-plugin). 52 | 53 | Simply run `./gradlew clean shadowJar` and put the resulting JAR file located in `bin` folder into your plugins folder. 54 | 55 | In other cases, you must use something like [shadow](https://imperceptiblethoughts.com/shadow/) (for Gradle) 56 | or [maven-shade-plugin](https://maven.apache.org/plugins/maven-shade-plugin/) (for Maven). 57 | 58 | ### Maven 59 | 60 | ```xml 61 | 62 | catcoder-snapshots 63 | https://catcoder.pl.ua/snapshots 64 | 65 | ``` 66 | ```xml 67 | 68 | me.catcoder 69 | bukkit-sidebar 70 | 6.2.10-SNAPSHOT 71 | 72 | ``` 73 | 74 | ### Gradle 75 | 76 | ```groovy 77 | repositories { 78 | maven { url 'https://catcoder.pl.ua/snapshots' } 79 | } 80 | ``` 81 | ```groovy 82 | dependencies { 83 | implementation 'me.catcoder:bukkit-sidebar:6.2.10-SNAPSHOT' 84 | } 85 | ``` 86 | 87 | ### Gradle (Kotlin DSL) 88 | 89 | ```kotlin 90 | repositories { 91 | maven("https://catcoder.pl.ua/snapshots") 92 | } 93 | ``` 94 | ```kotlin 95 | dependencies { 96 | implementation("me.catcoder:bukkit-sidebar:6.2.10-SNAPSHOT") 97 | } 98 | ``` 99 | 100 | ## Basic Usage 101 | 102 | ```java 103 | // create sidebar which uses Adventure API 104 | // you can also use other methods from ProtocolSidebar class 105 | // for another text providers such as BungeeCord Chat, MiniMessage... 106 | Sidebar sidebar = ProtocolSidebar.newAdventureSidebar( 107 | TextIterators.textFadeHypixel("SIDEBAR"), this); 108 | 109 | // let's add some lines 110 | sidebar.addLine( 111 | Component.text("Just a static line").color(NamedTextColor.GREEN)); 112 | // add an empty line 113 | sidebar.addBlankLine(); 114 | // also you can add updatable lines which applies to all players receiving this sidebar 115 | sidebar.addUpdatableLine( 116 | player -> Component.text("Your Hunger: ") 117 | .append(Component.text(player.getFoodLevel()) 118 | .color(NamedTextColor.GREEN)) 119 | ); 120 | 121 | sidebar.addBlankLine(); 122 | sidebar.addUpdatableLine( 123 | player -> Component.text("Your Health: ") 124 | .append(Component.text(player.getHealth()) 125 | .color(NamedTextColor.GREEN)) 126 | ); 127 | sidebar.addBlankLine(); 128 | sidebar.addLine( 129 | Component.text("https://github.com/CatCoderr/ProtocolSidebar") 130 | .color(NamedTextColor.YELLOW 131 | )); 132 | 133 | // update all lines except static ones every 10 ticks 134 | sidebar.updateLinesPeriodically(0, 10); 135 | 136 | // ... 137 | 138 | // show to the player 139 | sidebar.addViewer(player); 140 | // ...hide from the player 141 | sidebar.removeViewer(player); 142 | ``` 143 | ![Example](https://github.com/CatCoderr/ProtocolSidebar/raw/master/assets/nice_example.gif) 144 | 145 | ## Conditional Lines 146 | The visibility of these lines depends on the condition you set. 147 | If the condition is true, the line will be shown, otherwise it will be hidden. 148 | It's an updatable line, so it will update along with other updatable lines. 149 | 150 | ## Score number formatting 151 | You can use `scoreNumberFormat` method in both `ScoreboardObjective` and `SidebarLine` classes to format score numbers. 152 | 153 | This feature is available starting from 1.20.4 version. 154 | ```java 155 | // removes scores completely for all lines 156 | sidebar.getObjective().scoreNumberFormatBlank(); 157 | // set's custom fixed text for all lines 158 | sidebar.getObjective().scoreNumberFormatFixed(player -> Component.text("Test").color(NamedTextColor.BLUE)); 159 | 160 | // set's score number format for specific line (overrides objective's format) 161 | var line = sidebar.addLine(Component.text("Some line").color(NamedTextColor.YELLOW)); 162 | line.scoreNumberFormatFixed(player -> Component.text("Test").color(NamedTextColor.BLUE)); 163 | ``` 164 | 165 | ## Sidebar Title Animations 166 | 167 | Library has built-in title animations, but you can also create your [own](https://github.com/CatCoderr/ProtocolSidebar/blob/master/src/main/java/me/catcoder/sidebar/text/TextIterator.java). 168 | ![Hypixel-like animation](https://github.com/CatCoderr/ProtocolSidebar/raw/master/assets/animation_example.gif) 169 | 170 | Animations also can be used in updatable lines: 171 | 172 | ```java 173 | TextIterator animation = TextIterators.textFadeHypixel("Hello World!"); 174 | SidebarLine line = sidebar.addUpdatableLine(sidebar.asLineUpdater(animation)); 175 | 176 | line.updatePeriodically(0, 1, sidebar); 177 | ``` 178 | 179 | ## Sidebar Pager 180 | 181 | You can also use sidebar pager, which allows you to show player multiple pages of information. 182 | ```java 183 | Sidebar anotherSidebar = ProtocolSidebar.newAdventureSidebar( 184 | TextIterators.textFadeHypixel("ANOTHER SIDEBAR"), this); 185 | 186 | Sidebar firstSidebar = ProtocolSidebar.newAdventureSidebar( 187 | TextIterators.textFadeHypixel("SIDEBAR"), this); 188 | 189 | SidebarPager pager = new SidebarPager<>( 190 | Arrays.asList(firstSidebar, anotherSidebar), 20 * 5, this); 191 | 192 | // add page status line to all sidebars in pager 193 | pager.addPageLine((page, maxPage, sidebar) -> 194 | sidebar.addLine(Component.text("Page " + page + "/" + maxPage) 195 | .color(NamedTextColor.GREEN))); 196 | 197 | pager.applyToAll(Sidebar::addBlankLine); 198 | 199 | // ...add some lines 200 | 201 | // show to player 202 | pager.show(player); 203 | 204 | // ... 205 | // hide from the player 206 | pager.hide(player); 207 | ``` 208 | 209 | ![Pager example](https://github.com/CatCoderr/ProtocolSidebar/raw/master/assets/pager_example.gif) 210 | -------------------------------------------------------------------------------- /assets/animation_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatCoderr/ProtocolSidebar/3d9ee529e22a92cb099183662c981abdfb5315a8/assets/animation_example.gif -------------------------------------------------------------------------------- /assets/nice_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatCoderr/ProtocolSidebar/3d9ee529e22a92cb099183662c981abdfb5315a8/assets/nice_example.gif -------------------------------------------------------------------------------- /assets/pager_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatCoderr/ProtocolSidebar/3d9ee529e22a92cb099183662c981abdfb5315a8/assets/pager_example.gif -------------------------------------------------------------------------------- /assets/sidebar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatCoderr/ProtocolSidebar/3d9ee529e22a92cb099183662c981abdfb5315a8/assets/sidebar.gif -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("maven-publish") 4 | id("signing") 5 | } 6 | 7 | group = "me.catcoder" 8 | version = "6.2.10-SNAPSHOT" 9 | description = "Powerful feature-packed Minecraft scoreboard library" 10 | 11 | val adventureVersion = "4.16.0" 12 | val paperVersion = "1.20.1-R0.1-SNAPSHOT" 13 | val viaVersionVersion = "5.0.0" 14 | val viaNBTVersion = "5.0.2" 15 | val miniPlaceholdersVersion = "2.2.3" 16 | val lombokVersion = "1.18.30" 17 | val foliaLibVersion = "main-SNAPSHOT" 18 | 19 | allprojects { 20 | apply(plugin = "java-library") 21 | 22 | repositories { 23 | mavenLocal() 24 | maven { url = uri("https://jitpack.io") } 25 | maven { url = uri("https://repo.papermc.io/repository/maven-public/") } 26 | maven { url = uri("https://hub.spigotmc.org/nexus/content/groups/public/") } 27 | maven { url = uri("https://repo.dmulloy2.net/content/groups/public/") } 28 | maven { url = uri("https://oss.sonatype.org/content/groups/public/") } 29 | maven { url = uri("https://repo.viaversion.com") } 30 | maven { url = uri("https://repo.maven.apache.org/maven2/") } 31 | maven { url = uri("https://repo.opencollab.dev/maven-releases/") } 32 | } 33 | dependencies { 34 | testImplementation("junit:junit:4.13.2") 35 | testImplementation("org.mockito:mockito-core:5.7.0") 36 | testImplementation("org.powermock:powermock-module-junit4:2.0.9") 37 | testImplementation("org.powermock:powermock-api-mockito2:2.0.9") 38 | 39 | compileOnly("io.papermc.paper:paper-api:${paperVersion}") 40 | testCompileOnly("io.papermc.paper:paper-api:${paperVersion}") 41 | 42 | implementation("com.viaversion:nbt:${viaNBTVersion}") 43 | implementation("com.github.technicallycoded:FoliaLib:${foliaLibVersion}") 44 | 45 | compileOnly("org.projectlombok:lombok:${lombokVersion}") 46 | annotationProcessor("org.projectlombok:lombok:${lombokVersion}") 47 | 48 | compileOnly("com.viaversion:viaversion-common:${viaVersionVersion}") 49 | compileOnly("com.viaversion:viaversion-bukkit:${viaVersionVersion}") 50 | 51 | compileOnly("io.netty:netty-buffer:4.1.101.Final") 52 | compileOnly("io.netty:netty-handler:4.1.101.Final") 53 | 54 | compileOnly("io.github.miniplaceholders:miniplaceholders-api:${miniPlaceholdersVersion}") 55 | 56 | compileOnly("net.kyori:adventure-api:${adventureVersion}") 57 | compileOnly("net.kyori:adventure-text-minimessage:${adventureVersion}") 58 | compileOnly("net.kyori:adventure-text-serializer-gson:${adventureVersion}") 59 | compileOnly("net.kyori:adventure-text-serializer-legacy:${adventureVersion}") 60 | } 61 | } 62 | 63 | val javadocJar by tasks.registering(Jar::class) { 64 | archiveClassifier.set("javadoc") 65 | } 66 | 67 | publishing { 68 | 69 | // Configure all publications 70 | publications { 71 | 72 | create("mavenJava") { 73 | from(components["java"]) 74 | 75 | artifact(javadocJar.get()) 76 | 77 | // Provide artifacts information requited by Maven Central 78 | pom { 79 | name.set("ProtocolSidebar") 80 | description.set(project.description) 81 | url.set("https://github.com/CatCoderr/ProtocolSidebar") 82 | 83 | licenses { 84 | license { 85 | name.set("MIT") 86 | url.set("https://opensource.org/licenses/MIT") 87 | } 88 | } 89 | developers { 90 | developer { 91 | id.set("CatCoder") 92 | name.set("Ruslan Onischenko") 93 | email.set("catcoderr@gmail.com") 94 | } 95 | } 96 | scm { 97 | url.set("https://github.com/CatCoderr/ProtocolSidebar") 98 | connection.set("scm:git:git://github.com:CatCoderr/ProtocolSidebar.git") 99 | developerConnection.set("scm:git:ssh://github.com:CatCoderr/ProtocolSidebar.git") 100 | } 101 | 102 | issueManagement { 103 | url.set("https://github.com/CatCoderr/ProtocolSidebar/issues") 104 | } 105 | 106 | } 107 | } 108 | } 109 | 110 | repositories { 111 | maven { 112 | name = "Snapshots" 113 | url = uri("https://catcoder.pl.ua/snapshots") 114 | credentials { 115 | username = System.getenv("USERNAME") 116 | password = System.getenv("TOKEN") 117 | } 118 | } 119 | } 120 | } 121 | 122 | signing { 123 | val signingKey = System.getenv("GPG_SECRET_KEY") 124 | val signingPassword = System.getenv("GPG_PASSPHRASE") 125 | 126 | useInMemoryPgpKeys(signingKey, signingPassword) 127 | 128 | sign(publishing.publications["mavenJava"]) 129 | } 130 | 131 | tasks.withType { 132 | options.encoding = "UTF-8" 133 | } 134 | 135 | tasks.withType { 136 | options.encoding = "UTF-8" 137 | } 138 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatCoderr/ProtocolSidebar/3d9ee529e22a92cb099183662c981abdfb5315a8/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.4-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /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/HEAD/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 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | 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 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /run_test_server.sh: -------------------------------------------------------------------------------- 1 | VIA_VERSION=5.0.3 2 | 3 | ./gradlew clean shadowJar 4 | 5 | echo "Copying ProtocolSidebar..." 6 | cp bin/ProtocolSidebar-*.jar server/data/plugins/ProtocolSidebar.jar 7 | 8 | echo "Downloading ViaVersion..." 9 | wget https://github.com/ViaVersion/ViaVersion/releases/download/$VIA_VERSION/ViaVersion-$VIA_VERSION.jar -O server/data/plugins/ViaVersion.jar 10 | 11 | echo "Starting server..." 12 | cd server && docker-compose up -d && docker attach server-mc-1 -------------------------------------------------------------------------------- /server/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | mc: 5 | image: itzg/minecraft-server 6 | ports: 7 | - 25565:25565 8 | - 5005:5005 9 | environment: 10 | EULA: "TRUE" 11 | TYPE: "PAPER" 12 | VERSION: "1.20.4" 13 | JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" 14 | tty: true 15 | stdin_open: true 16 | volumes: 17 | # attach a directory relative to the directory containing this compose file 18 | - ./data:/data -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "bukkit-sidebar" 2 | 3 | include(":standalone-plugin") -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/ProtocolSidebar.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar; 2 | 3 | import lombok.NonNull; 4 | import lombok.experimental.UtilityClass; 5 | import me.catcoder.sidebar.text.TextIterator; 6 | import me.catcoder.sidebar.text.TextProvider; 7 | import me.catcoder.sidebar.text.provider.AdventureTextProvider; 8 | import me.catcoder.sidebar.text.provider.BungeeCordChatTextProvider; 9 | import me.catcoder.sidebar.text.provider.MiniMessageTextProvider; 10 | import me.catcoder.sidebar.text.provider.MiniPlaceholdersTextProvider; 11 | import net.kyori.adventure.text.Component; 12 | import net.kyori.adventure.text.minimessage.MiniMessage; 13 | import net.md_5.bungee.api.chat.BaseComponent; 14 | import org.bukkit.plugin.Plugin; 15 | 16 | /** 17 | * Entry point class for creating new sidebars. 18 | *

19 | * This class provides methods for creating new sidebars with different text providers. 20 | *

21 | * 22 | * @author CatCoder 23 | */ 24 | @UtilityClass 25 | public class ProtocolSidebar { 26 | 27 | /** 28 | * Creates new sidebar with custom text provider. 29 | * 30 | * @param title - sidebar title 31 | * @param plugin - plugin instance 32 | * @param textProvider - text provider 33 | * @param - component entity type 34 | * @return new sidebar 35 | */ 36 | public Sidebar newSidebar( 37 | @NonNull R title, 38 | @NonNull Plugin plugin, 39 | @NonNull TextProvider textProvider 40 | ) { 41 | return new Sidebar<>(title, plugin, textProvider); 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public Sidebar newSidebar( 48 | @NonNull TextIterator title, 49 | @NonNull Plugin plugin, 50 | @NonNull TextProvider textProvider 51 | ) { 52 | return new Sidebar<>(title, plugin, textProvider); 53 | } 54 | 55 | 56 | /** 57 | * Creates new sidebar with {@see https://docs.advntr.dev/minimessage/api.html MiniMessage} text provider. 58 | * Adventure are natively supported on Paper 1.16.5+. 59 | * 60 | * @param title - sidebar title 61 | * @param plugin - plugin instance 62 | * @param miniMessage - MiniMessage instance 63 | * @return new sidebar 64 | */ 65 | public Sidebar newMiniMessageSidebar( 66 | @NonNull String title, 67 | @NonNull Plugin plugin, 68 | @NonNull MiniMessage miniMessage 69 | ) { 70 | return newSidebar(title, plugin, new MiniMessageTextProvider(miniMessage)); 71 | } 72 | 73 | /** 74 | * Creates new sidebar with MiniMessage and MiniPlaceholders text provider. 75 | * For more information about MiniPlaceholders, see {@see https://github.com/MiniPlaceholders/MiniPlaceholders} 76 | * 77 | * @param title - sidebar title 78 | * @param plugin - plugin instance 79 | * @param miniMessage - MiniMessage instance 80 | * @return new sidebar 81 | */ 82 | public Sidebar newMiniplaceholdersSidebar( 83 | @NonNull String title, 84 | @NonNull Plugin plugin, 85 | @NonNull MiniMessage miniMessage 86 | ) { 87 | return newSidebar(title, plugin, new MiniPlaceholdersTextProvider(miniMessage)); 88 | } 89 | 90 | /** 91 | * {@inheritDoc} 92 | */ 93 | public Sidebar newMiniplaceholdersSidebar( 94 | @NonNull TextIterator title, 95 | @NonNull Plugin plugin, 96 | @NonNull MiniMessage miniMessage 97 | ) { 98 | return newSidebar(title, plugin, new MiniPlaceholdersTextProvider(miniMessage)); 99 | } 100 | 101 | /** 102 | * {@inheritDoc} 103 | */ 104 | public Sidebar newMiniMessageSidebar( 105 | @NonNull TextIterator title, 106 | @NonNull Plugin plugin, 107 | @NonNull MiniMessage miniMessage 108 | ) { 109 | return newSidebar(title, plugin, new MiniMessageTextProvider(miniMessage)); 110 | } 111 | 112 | /** 113 | * {@inheritDoc} 114 | */ 115 | public Sidebar newMiniMessageSidebar( 116 | @NonNull String title, 117 | @NonNull Plugin plugin 118 | ) { 119 | return newSidebar(title, plugin, new MiniMessageTextProvider(MiniMessage.miniMessage())); 120 | } 121 | 122 | /** 123 | * {@inheritDoc} 124 | */ 125 | public Sidebar newMiniMessageSidebar( 126 | @NonNull TextIterator title, 127 | @NonNull Plugin plugin 128 | ) { 129 | return newSidebar(title, plugin, new MiniMessageTextProvider(MiniMessage.miniMessage())); 130 | } 131 | 132 | /** 133 | * Creates new sidebar with {@see https://docs.advntr.dev/getting-started.html Adventure} text provider. 134 | * Adventure are natively supported on Paper 1.16.5+. 135 | * 136 | * @param title - sidebar title 137 | * @param plugin - plugin instance 138 | * @return new sidebar 139 | */ 140 | public Sidebar newAdventureSidebar( 141 | @NonNull Component title, 142 | @NonNull Plugin plugin 143 | ) { 144 | return newSidebar(title, plugin, new AdventureTextProvider()); 145 | } 146 | 147 | /** 148 | * {@inheritDoc} 149 | */ 150 | public Sidebar newAdventureSidebar( 151 | @NonNull TextIterator title, 152 | @NonNull Plugin plugin 153 | ) { 154 | return newSidebar(title, plugin, new AdventureTextProvider()); 155 | } 156 | 157 | /** 158 | * Creates new sidebar with BungeeCord Chat API text provider. 159 | * Use this method if you're running on Spigot. 160 | * 161 | * @param title - sidebar title 162 | * @param plugin - plugin instance 163 | * @return new sidebar 164 | */ 165 | public Sidebar newBungeeChatSidebar( 166 | @NonNull BaseComponent[] title, 167 | @NonNull Plugin plugin 168 | ) { 169 | return newSidebar(title, plugin, new BungeeCordChatTextProvider()); 170 | } 171 | 172 | /** 173 | * {@inheritDoc} 174 | */ 175 | public Sidebar newBungeeChatSidebar( 176 | @NonNull TextIterator title, 177 | @NonNull Plugin plugin 178 | ) { 179 | return newSidebar(title, plugin, new BungeeCordChatTextProvider()); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/ScoreboardObjective.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar; 2 | 3 | import com.google.common.base.Function; 4 | import com.google.common.base.Preconditions; 5 | import io.netty.buffer.ByteBuf; 6 | import lombok.Getter; 7 | import lombok.NonNull; 8 | import me.catcoder.sidebar.protocol.ChannelInjector; 9 | import me.catcoder.sidebar.protocol.PacketIds; 10 | import me.catcoder.sidebar.protocol.ProtocolConstants; 11 | import me.catcoder.sidebar.protocol.ScoreNumberFormat; 12 | import me.catcoder.sidebar.text.TextProvider; 13 | import me.catcoder.sidebar.util.buffer.ByteBufNetOutput; 14 | import me.catcoder.sidebar.util.buffer.NetOutput; 15 | import me.catcoder.sidebar.util.version.VersionUtil; 16 | import org.bukkit.entity.Player; 17 | 18 | import static me.catcoder.sidebar.SidebarLine.sendPacket; 19 | 20 | /** 21 | * Encapsulates scoreboard objective 22 | * 23 | * @author CatCoder 24 | * @see Bukkit 25 | * documentation 26 | */ 27 | @Getter 28 | public class ScoreboardObjective { 29 | 30 | public static final int DISPLAY_SIDEBAR = 1; 31 | public static final int ADD_OBJECTIVE = 0; 32 | public static final int REMOVE_OBJECTIVE = 1; 33 | public static final int UPDATE_VALUE = 2; 34 | 35 | private final String name; 36 | private final TextProvider textProvider; 37 | 38 | private ScoreNumberFormat numberFormat; 39 | private Function numberFormatter; 40 | 41 | private R displayName; 42 | 43 | ScoreboardObjective(@NonNull String name, 44 | @NonNull R displayName, 45 | @NonNull TextProvider textProvider) { 46 | Preconditions.checkArgument( 47 | name.length() <= 16, "Objective name exceeds 16 symbols limit"); 48 | 49 | this.name = name; 50 | this.textProvider = textProvider; 51 | this.displayName = displayName; 52 | } 53 | 54 | void setDisplayName(@NonNull R displayName) { 55 | this.displayName = displayName; 56 | } 57 | 58 | void updateValue(@NonNull Player player) { 59 | ByteBuf packet = getPacket(player, UPDATE_VALUE); 60 | sendPacket(player, packet); 61 | } 62 | 63 | public void scoreNumberFormatFixed(@NonNull Function numberFormatter) { 64 | this.numberFormat = ScoreNumberFormat.FIXED; 65 | this.numberFormatter = numberFormatter; 66 | } 67 | 68 | public void scoreNumberFormatStyled(@NonNull Function numberFormatter) { 69 | this.numberFormat = ScoreNumberFormat.STYLED; 70 | this.numberFormatter = numberFormatter; 71 | } 72 | 73 | public void scoreNumberFormatBlank() { 74 | this.numberFormat = ScoreNumberFormat.BLANK; 75 | this.numberFormatter = null; 76 | } 77 | 78 | void create(@NonNull Player player) { 79 | ByteBuf packet = getPacket(player, ADD_OBJECTIVE); 80 | sendPacket(player, packet); 81 | } 82 | 83 | void remove(@NonNull Player player) { 84 | ByteBuf packet = getPacket(player, REMOVE_OBJECTIVE); 85 | sendPacket(player, packet); 86 | } 87 | 88 | void display(@NonNull Player player) { 89 | ByteBuf buf = ChannelInjector.IMP.getChannel(player).alloc().buffer(); 90 | 91 | NetOutput output = new ByteBufNetOutput(buf); 92 | 93 | output.writeVarInt(PacketIds.OBJECTIVE_DISPLAY.getServerPacketId()); 94 | 95 | output.writeByte(DISPLAY_SIDEBAR); 96 | output.writeString(name); 97 | 98 | sendPacket(player, buf); 99 | } 100 | 101 | private ByteBuf getPacket(@NonNull Player player, int mode) { 102 | int version = VersionUtil.getPlayerVersion(player.getUniqueId()); 103 | 104 | ByteBuf buf = ChannelInjector.IMP.getChannel(player).alloc().buffer(); 105 | 106 | NetOutput output = new ByteBufNetOutput(buf); 107 | 108 | output.writeVarInt(PacketIds.OBJECTIVE.getServerPacketId()); 109 | 110 | output.writeString(name); 111 | output.writeByte(mode); 112 | 113 | if (mode == ADD_OBJECTIVE || mode == UPDATE_VALUE) { 114 | String legacyText = textProvider.asLegacyMessage(player, displayName); 115 | // Since 1.13 characters limit for display name was removed 116 | if (version < ProtocolConstants.MINECRAFT_1_13 && legacyText.length() > 32) { 117 | legacyText = legacyText.substring(0, 32); 118 | } 119 | 120 | if (VersionUtil.SERVER_VERSION >= ProtocolConstants.MINECRAFT_1_20_3) { 121 | // what the heck 1.20.3? 122 | output.writeComponent(textProvider.asJsonMessage(player, displayName)); 123 | } else if (VersionUtil.SERVER_VERSION >= ProtocolConstants.MINECRAFT_1_13) { 124 | output.writeString(textProvider.asJsonMessage(player, displayName)); 125 | } else { 126 | output.writeString(legacyText); 127 | } 128 | 129 | if (VersionUtil.SERVER_VERSION >= ProtocolConstants.MINECRAFT_1_20_3) { 130 | output.writeVarInt(0); 131 | output.writeBoolean(numberFormat != null); // has number format 132 | 133 | if (numberFormat != null) { 134 | numberFormat.accept(output, numberFormatter == null ? 135 | null : textProvider.asJsonMessage(player, numberFormatter.apply(player)) 136 | ); 137 | } 138 | 139 | return buf; 140 | } 141 | 142 | if (VersionUtil.SERVER_VERSION >= ProtocolConstants.MINECRAFT_1_13) { 143 | output.writeVarInt(0); // Health display 144 | } else { 145 | output.writeString("integer"); // Health display 146 | } 147 | } 148 | 149 | 150 | return buf; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/Sidebar.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.tcoded.folialib.FoliaLib; 5 | import com.tcoded.folialib.wrapper.task.WrappedBukkitTask; 6 | import com.tcoded.folialib.wrapper.task.WrappedTask; 7 | import lombok.AccessLevel; 8 | import lombok.Getter; 9 | import lombok.NonNull; 10 | import lombok.SneakyThrows; 11 | import lombok.experimental.FieldDefaults; 12 | import me.catcoder.sidebar.text.TextIterator; 13 | import me.catcoder.sidebar.text.TextProvider; 14 | import me.catcoder.sidebar.util.RandomString; 15 | import me.catcoder.sidebar.util.lang.ThrowingConsumer; 16 | import me.catcoder.sidebar.util.lang.ThrowingFunction; 17 | import me.catcoder.sidebar.util.lang.ThrowingPredicate; 18 | import me.catcoder.sidebar.util.lang.ThrowingSupplier; 19 | import org.bukkit.Bukkit; 20 | import org.bukkit.entity.Player; 21 | import org.bukkit.plugin.Plugin; 22 | import org.bukkit.scheduler.BukkitTask; 23 | 24 | import java.util.*; 25 | 26 | /** 27 | * Represents a sidebar. 28 | *

29 | * Sidebar is a scoreboard with a title and lines. 30 | *

31 | */ 32 | 33 | @FieldDefaults(level = AccessLevel.PACKAGE) 34 | public class Sidebar { 35 | 36 | private static final String OBJECTIVE_PREFIX = "PS-"; 37 | private static final int MAX_LINES_COUNT = 15; 38 | 39 | private final Set viewers = Collections.synchronizedSet(new HashSet<>()); 40 | private final List> lines = new ArrayList<>(); 41 | @Getter 42 | private final ScoreboardObjective objective; 43 | 44 | private TextIterator titleText; 45 | private WrappedTask titleUpdater; 46 | 47 | final Set tasks = new HashSet<>(); 48 | final TextProvider textProvider; 49 | 50 | @Getter 51 | private final Plugin plugin; 52 | 53 | @Getter 54 | private final FoliaLib foliaLib; 55 | 56 | /** 57 | * Construct a new sidebar instance. 58 | * 59 | * @param title a title of sidebar 60 | * @param plugin plugin instance 61 | */ 62 | Sidebar(@NonNull R title, @NonNull Plugin plugin, @NonNull TextProvider textProvider) { 63 | this.plugin = plugin; 64 | this.foliaLib = new FoliaLib(plugin); 65 | this.textProvider = textProvider; 66 | this.objective = new ScoreboardObjective<>(OBJECTIVE_PREFIX + RandomString.generate(3), title, textProvider); 67 | } 68 | 69 | /** 70 | * Construct a new sidebar instance. 71 | * 72 | * @param titleIterator a title iterator of sidebar 73 | * @param plugin plugin instance 74 | */ 75 | Sidebar(@NonNull TextIterator titleIterator, @NonNull Plugin plugin, @NonNull TextProvider textProvider) { 76 | this.plugin = plugin; 77 | this.foliaLib = new FoliaLib(plugin); 78 | this.textProvider = textProvider; 79 | 80 | this.objective = new ScoreboardObjective<>( 81 | OBJECTIVE_PREFIX + RandomString.generate(3), 82 | textProvider.fromLegacyMessage(titleIterator.next()), 83 | textProvider); 84 | 85 | setTitleIter(titleIterator); 86 | } 87 | 88 | /** 89 | * Converts TextIterator to line updater. 90 | * 91 | * @param iterator - iterator 92 | * @return line updater 93 | */ 94 | public ThrowingFunction toLineUpdater(@NonNull TextIterator iterator) { 95 | return player -> textProvider.fromLegacyMessage(iterator.next()); 96 | } 97 | 98 | /** 99 | * Update the title of the sidebar. 100 | * 101 | * @param title title to be updated 102 | */ 103 | public void setTitle(@NonNull R title) { 104 | cancelTitleUpdater(); 105 | 106 | objective.setDisplayName(title); 107 | broadcast(objective::updateValue); 108 | } 109 | 110 | /** 111 | * Update the title of the sidebar. 112 | * 113 | * @param iterator - title iterator 114 | */ 115 | public void setTitle(@NonNull TextIterator iterator) { 116 | setTitleIter(iterator); 117 | } 118 | 119 | private void cancelTitleUpdater() { 120 | if (titleUpdater != null) { 121 | titleUpdater.cancel(); 122 | titleUpdater = null; 123 | } 124 | 125 | this.titleText = null; 126 | } 127 | 128 | private void setTitleIter(@NonNull TextIterator iterator) { 129 | cancelTitleUpdater(); 130 | 131 | this.titleText = iterator; 132 | this.titleUpdater = foliaLib.getScheduler().runTimerAsync(() -> { 133 | String next = titleText.next(); 134 | 135 | objective.setDisplayName(textProvider.fromLegacyMessage(next)); 136 | broadcast(objective::updateValue); 137 | }, 0, 1); 138 | } 139 | 140 | /** 141 | * Updates the index of the line shifting it by an offset. 142 | * 143 | * @param line the line 144 | * @param offset the offset 145 | */ 146 | public void shiftLine(SidebarLine line, int offset) { 147 | synchronized (lines) { 148 | lines.remove(line); 149 | lines.add(offset, line); 150 | } 151 | 152 | updateAllLines(); // recalculate indices 153 | } 154 | 155 | /** 156 | * Binds a bukkit task to this sidebar. 157 | * When the sidebar is destroyed, the task will be cancelled. 158 | * 159 | * @param task - task to bind 160 | * @return the task 161 | * 162 | * @deprecated Use {@link #bindWrappedTask(WrappedTask)} instead. 163 | */ 164 | @Deprecated(since = "6.2.9") 165 | public BukkitTask bindBukkitTask(@NonNull BukkitTask task) { 166 | this.tasks.add(new WrappedBukkitTask(task)); 167 | return task; 168 | } 169 | 170 | /** 171 | * Binds a {@link WrappedTask} to this sidebar. 172 | * When the sidebar is destroyed, the task will be cancelled. 173 | * 174 | * @param task - task to bind 175 | * @return the task 176 | */ 177 | public WrappedTask bindWrappedTask(@NonNull WrappedTask task) { 178 | this.tasks.add(task); 179 | return task; 180 | } 181 | 182 | /** 183 | * Schedules the async task to update all dynamic lines at fixed rate. 184 | * 185 | * @param delay delay in ticks 186 | * @param period period in ticks 187 | * @return the scheduled task 188 | */ 189 | public WrappedTask updateLinesPeriodically(long delay, long period) { 190 | return updateLinesPeriodically(delay, period, true); 191 | } 192 | 193 | /** 194 | * Schedules the task to update all dynamic lines at fixed rate. 195 | * 196 | * @param delay delay in ticks 197 | * @param period period in ticks 198 | * @param async whether the task should be executed asynchronously 199 | * @return the scheduled task 200 | */ 201 | public WrappedTask updateLinesPeriodically(long delay, long period, boolean async) { 202 | return async ? 203 | bindWrappedTask(foliaLib.getScheduler() 204 | .runTimerAsync(this::updateAllLines, delay, period)) : 205 | bindWrappedTask(foliaLib.getScheduler() 206 | .runTimer(this::updateAllLines, delay, period)); 207 | } 208 | 209 | /** 210 | * Add a line with specific display condition. 211 | * If the condition is false, the line will be hidden for player. 212 | *

213 | * 214 | * @param updater - the function that updates the text 215 | * @param condition - the condition 216 | * @return SidebarLine instance 217 | */ 218 | public SidebarLine addConditionalLine(@NonNull ThrowingFunction updater, 219 | @NonNull ThrowingPredicate condition) { 220 | return addLine(updater, false, condition); 221 | } 222 | 223 | /** 224 | * Add a line with static text. 225 | * 226 | * @param text - the text 227 | * @return SidebarLine instance 228 | */ 229 | public SidebarLine addTextLine(@NonNull String text) { 230 | return addLine(textProvider.fromLegacyMessage(text)); 231 | } 232 | 233 | /** 234 | * Add a line to the sidebar with dynamic text. 235 | * 236 | * @param updater - the function that updates the text 237 | * @return SidebarLine instance 238 | */ 239 | public SidebarLine addUpdatableLine(@NonNull ThrowingFunction updater) { 240 | return addLine(updater, false, x -> true); 241 | } 242 | 243 | /** 244 | * Add a line to the sidebar with dynamic text with no player argument. 245 | * 246 | * @param updater - the function that updates the text 247 | * @return SidebarLine instance 248 | */ 249 | public SidebarLine addUpdatableLine(ThrowingSupplier updater) { 250 | return addUpdatableLine(player -> updater.get()); 251 | } 252 | 253 | /** 254 | * Add a line to the sidebar with static text. 255 | * 256 | * @param text the text 257 | * @return SidebarLine instance 258 | */ 259 | public SidebarLine addLine(@NonNull R text) { 260 | return addLine(x -> text, true, x -> true); 261 | } 262 | 263 | /** 264 | * Add a blank line to the sidebar. 265 | * 266 | * @return SidebarLine instance 267 | */ 268 | public SidebarLine addBlankLine() { 269 | return addTextLine(""); 270 | } 271 | 272 | private SidebarLine addLine(@NonNull ThrowingFunction updater, boolean staticText, 273 | @NonNull ThrowingPredicate predicate) { 274 | synchronized (lines) { 275 | Preconditions.checkArgument( 276 | lines.size() <= MAX_LINES_COUNT, "Cannot add more than %s lines to a sidebar", MAX_LINES_COUNT); 277 | 278 | SidebarLine line = new SidebarLine<>( 279 | updater, objective.getName() + lines.size(), 280 | staticText, lines.size(), textProvider, predicate); 281 | 282 | lines.add(line); 283 | return line; 284 | } 285 | } 286 | 287 | /** 288 | * Removes line from sidebar. 289 | * 290 | * @param line the line 291 | */ 292 | public void removeLine(@NonNull SidebarLine line) { 293 | synchronized (lines) { 294 | if (lines.remove(line) && line.getScore() != -1) { 295 | broadcast(p -> line.removeTeam(p, objective.getName())); 296 | updateAllLines(); 297 | } 298 | } 299 | } 300 | 301 | /** 302 | * Get line with maximum score. 303 | * 304 | * @return SidebarLine 305 | */ 306 | public Optional> maxLine() { 307 | synchronized (lines) { 308 | return lines.stream() 309 | .filter(line -> line.getScore() != -1) 310 | .max(Comparator.comparingInt(SidebarLine::getScore)); 311 | } 312 | } 313 | 314 | /** 315 | * Get the line with minimum score. 316 | * 317 | * @return SidebarLine 318 | */ 319 | public Optional> minLine() { 320 | synchronized (lines) { 321 | return lines.stream() 322 | .filter(line -> line.getScore() != -1) 323 | .min(Comparator.comparingInt(SidebarLine::getScore)); 324 | } 325 | } 326 | 327 | /** 328 | * Update the single line. 329 | * 330 | * @param line target line. 331 | */ 332 | public void updateLine(@NonNull SidebarLine line) { 333 | synchronized (lines) { 334 | Preconditions.checkArgument(lines.contains(line), "Line %s is not a part of this sidebar", line); 335 | 336 | broadcast(p -> line.updateTeam(p, objective.getName())); 337 | } 338 | } 339 | 340 | /** 341 | * Update all dynamic lines of the sidebar. 342 | * Except lines with their own update task. (see {@link SidebarLine#updatePeriodically(long, long, Sidebar)}) 343 | */ 344 | public void updateAllLines() { 345 | synchronized (lines) { 346 | int index = lines.size(); 347 | 348 | for (SidebarLine line : lines) { 349 | // if line is not created yet 350 | if (line.getScore() == -1) { 351 | line.setScore(index--); 352 | broadcast(p -> line.createTeam(p, objective.getName())); 353 | continue; 354 | } 355 | 356 | if (line.updateTask != null && !line.updateTask.isCancelled()) { 357 | // Don't update the line if it's already has its own update task 358 | continue; 359 | } 360 | 361 | line.setScore(index--); 362 | 363 | broadcast(p -> line.updateTeam(p, objective.getName())); 364 | } 365 | } 366 | } 367 | 368 | /** 369 | * Remove all viewers currently receiving this sidebar. 370 | */ 371 | public void removeViewers() { 372 | synchronized (viewers) { 373 | for (Iterator iterator = viewers.iterator(); iterator.hasNext(); ) { 374 | UUID uuid = iterator.next(); 375 | Player player = Bukkit.getPlayer(uuid); 376 | 377 | if (player != null) { 378 | removeViewer0(player); 379 | } 380 | 381 | iterator.remove(); 382 | } 383 | } 384 | } 385 | 386 | /** 387 | * Remove all viewers and cancel all tasks. 388 | *

389 | * This method should be called when the sidebar is no longer needed. 390 | * Otherwise, the sidebar will be kept in memory and will be updated 391 | * for all players. 392 | *

393 | */ 394 | public void destroy() { 395 | cancelTitleUpdater(); 396 | 397 | for (WrappedTask task : tasks) { 398 | foliaLib.getScheduler().cancelTask(task); 399 | } 400 | 401 | removeViewers(); 402 | 403 | synchronized (lines) { 404 | lines.clear(); 405 | } 406 | 407 | tasks.clear(); 408 | } 409 | 410 | /** 411 | * Sends this sidebar with all lines to the player. 412 | * 413 | * @param player target player 414 | */ 415 | 416 | @SneakyThrows 417 | public void addViewer(@NonNull Player player) { 418 | if (!viewers.contains(player.getUniqueId())) { 419 | objective.create(player); 420 | 421 | synchronized (lines) { 422 | for (SidebarLine line : lines) { 423 | line.createTeam(player, objective.getName()); 424 | } 425 | } 426 | 427 | objective.display(player); 428 | 429 | viewers.add(player.getUniqueId()); 430 | } 431 | } 432 | 433 | /** 434 | * Removes sidebar for the target player. 435 | * 436 | * @param player target player 437 | */ 438 | public void removeViewer(@NonNull Player player) { 439 | synchronized (viewers) { 440 | if (viewers.remove(player.getUniqueId())) { 441 | removeViewer0(player); 442 | } 443 | } 444 | } 445 | 446 | private void removeViewer0(@NonNull Player player) { 447 | lines.forEach(line -> line.removeTeam(player, objective.getName())); 448 | objective.remove(player); 449 | } 450 | 451 | /** 452 | * Returns the set of all players currently receiving this sidebar. 453 | * 454 | * @return a set with player UUIDs 455 | */ 456 | public Set getViewers() { 457 | return Collections.unmodifiableSet(viewers); 458 | } 459 | 460 | /** 461 | * Returns the list of all lines in this sidebar. 462 | * 463 | * @return a list of lines 464 | */ 465 | public List> getLines() { 466 | synchronized (lines) { 467 | return Collections.unmodifiableList(lines); 468 | } 469 | } 470 | 471 | private void broadcast(@NonNull ThrowingConsumer consumer) { 472 | synchronized (viewers) { 473 | viewers.removeIf(uuid -> Bukkit.getPlayer(uuid) == null); 474 | 475 | for (UUID id : viewers) { 476 | // double check 477 | Player player = Bukkit.getPlayer(id); 478 | if (player == null) { 479 | continue; 480 | } 481 | 482 | try { 483 | consumer.accept(player); 484 | } catch (Throwable e) { 485 | throw new RuntimeException("An error occurred while updating sidebar for player: " + player.getName(), 486 | e); 487 | } 488 | } 489 | } 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/SidebarLine.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar; 2 | 3 | import com.google.common.base.Function; 4 | import com.google.common.base.Preconditions; 5 | import com.tcoded.folialib.wrapper.task.WrappedTask; 6 | import io.netty.buffer.ByteBuf; 7 | import lombok.*; 8 | import me.catcoder.sidebar.protocol.ChannelInjector; 9 | import me.catcoder.sidebar.protocol.ScoreNumberFormat; 10 | import me.catcoder.sidebar.protocol.ScoreboardPackets; 11 | import me.catcoder.sidebar.text.TextProvider; 12 | import me.catcoder.sidebar.util.lang.ThrowingFunction; 13 | import me.catcoder.sidebar.util.lang.ThrowingPredicate; 14 | import me.catcoder.sidebar.util.lang.ThrowingSupplier; 15 | import org.bukkit.entity.Player; 16 | 17 | @Getter 18 | @ToString 19 | public class SidebarLine { 20 | 21 | private final String teamName; 22 | 23 | @Setter(AccessLevel.PACKAGE) 24 | private int score = -1; 25 | 26 | private final int index; 27 | private final boolean staticText; 28 | 29 | // for internal use 30 | WrappedTask updateTask; 31 | 32 | private ThrowingFunction updater; 33 | private ThrowingPredicate displayCondition; 34 | private final TextProvider textProvider; 35 | private ScoreNumberFormat scoreNumberFormat; 36 | private Function scoreNumberFormatter; 37 | 38 | SidebarLine(@NonNull ThrowingFunction updater, 39 | @NonNull String teamName, 40 | boolean staticText, 41 | int index, 42 | @NonNull TextProvider textProvider, 43 | @NonNull ThrowingPredicate displayCondition) { 44 | this.updater = updater; 45 | this.teamName = teamName; 46 | this.staticText = staticText; 47 | this.index = index; 48 | this.displayCondition = displayCondition; 49 | this.textProvider = textProvider; 50 | } 51 | 52 | public SidebarLine scoreNumberFormatBlank() { 53 | this.scoreNumberFormat = null; 54 | this.scoreNumberFormatter = null; 55 | return this; 56 | } 57 | 58 | public SidebarLine scoreNumberFormatFixed(@NonNull Function scoreNumberFormatter) { 59 | this.scoreNumberFormat = ScoreNumberFormat.FIXED; 60 | this.scoreNumberFormatter = scoreNumberFormatter; 61 | return this; 62 | } 63 | 64 | public SidebarLine scoreNumberFormatStyled(@NonNull Function scoreNumberFormatter) { 65 | this.scoreNumberFormat = ScoreNumberFormat.STYLED; 66 | this.scoreNumberFormatter = scoreNumberFormatter; 67 | return this; 68 | } 69 | 70 | public WrappedTask updatePeriodically(long delay, long period, @NonNull Sidebar sidebar) { 71 | Preconditions.checkState(!isStaticText(), "Cannot set updater for static text line"); 72 | 73 | if (updateTask != null) { 74 | Preconditions.checkState(updateTask.isCancelled(), 75 | "Update task for line %s is already running. Cancel it first.", this); 76 | sidebar.getFoliaLib().getScheduler().cancelTask(updateTask); 77 | sidebar.tasks.remove(updateTask); 78 | } 79 | 80 | WrappedTask task = sidebar.getFoliaLib().getScheduler().runTimerAsync(() -> sidebar.updateLine(this), delay, period); 81 | 82 | this.updateTask = task; 83 | 84 | sidebar.bindWrappedTask(task); 85 | 86 | return task; 87 | } 88 | 89 | /** 90 | * Sets visibility predicate for this line. Visibility predicate is a function that takes player 91 | * as an argument and returns boolean value. If predicate returns true, line will be visible for 92 | * this player, otherwise - invisible. 93 | * 94 | * @param displayCondition - visibility predicate 95 | */ 96 | public void setDisplayCondition(@NonNull ThrowingPredicate displayCondition) { 97 | this.displayCondition = displayCondition; 98 | } 99 | 100 | /** 101 | * Sets updater for this line. Updater is a function that takes player as an argument and returns 102 | * text that will be displayed for this player. 103 | * 104 | * @param updater - updater function 105 | */ 106 | public void setUpdater(@NonNull ThrowingFunction updater) { 107 | Preconditions.checkState(!isStaticText(), "Cannot set updater for static text line"); 108 | this.updater = updater; 109 | } 110 | 111 | /** 112 | * Sets updater for this line without player parameter 113 | * 114 | * @param updater - updater function 115 | */ 116 | public void setUpdater(@NonNull ThrowingSupplier updater) { 117 | Preconditions.checkState(!isStaticText(), "Cannot set updater for static text line"); 118 | this.updater = player -> updater.get(); 119 | } 120 | 121 | void updateTeam(@NonNull Player player, @NonNull String objective) throws Throwable { 122 | boolean visible = displayCondition.test(player); 123 | 124 | if (!isStaticText() && visible) { 125 | R text = updater.apply(player); 126 | sendPacket(player, ScoreboardPackets.createTeamPacket( 127 | ScoreboardPackets.TEAM_UPDATED, index, teamName, 128 | player, text, textProvider)); 129 | } 130 | 131 | if (!visible) { 132 | // if player doesn't meet display condition, remove score 133 | sendPacket(player, ScoreboardPackets.createScorePacket( 134 | player, 1, objective, score, index, textProvider, scoreNumberFormat, null)); 135 | return; 136 | } 137 | 138 | sendPacket(player, ScoreboardPackets.createScorePacket( 139 | player, 0, objective, score, index, textProvider, scoreNumberFormat, scoreNumberFormatter)); 140 | } 141 | 142 | void removeTeam(@NonNull Player player, @NonNull String objective) { 143 | sendPacket(player, ScoreboardPackets.createScorePacket( 144 | player, 1, objective, score, index, textProvider, null, null)); 145 | 146 | sendPacket(player, ScoreboardPackets.createTeamPacket(ScoreboardPackets.TEAM_REMOVED, index, teamName, 147 | player, null, textProvider)); 148 | } 149 | 150 | void createTeam(@NonNull Player player, @NonNull String objective) throws Throwable { 151 | boolean visible = displayCondition.test(player); 152 | 153 | R text = visible ? updater.apply(player) : textProvider.emptyMessage(); 154 | 155 | sendPacket(player, ScoreboardPackets.createTeamPacket(ScoreboardPackets.TEAM_CREATED, index, teamName, 156 | player, text, textProvider)); 157 | 158 | if (visible) { 159 | sendPacket(player, ScoreboardPackets.createScorePacket( 160 | player, 0, objective, score, index, textProvider, scoreNumberFormat, scoreNumberFormatter)); 161 | } 162 | } 163 | 164 | @SneakyThrows 165 | static void sendPacket(@NonNull Player player, @NonNull ByteBuf packet) { 166 | ChannelInjector.IMP.sendPacket(player, packet); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/pager/PageConsumer.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.pager; 2 | 3 | 4 | import me.catcoder.sidebar.Sidebar; 5 | 6 | @FunctionalInterface 7 | public interface PageConsumer { 8 | 9 | void accept(int page, int maxPage, Sidebar sidebar); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/pager/SidebarPager.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.pager; 2 | 3 | import com.google.common.collect.Iterators; 4 | import lombok.NonNull; 5 | import me.catcoder.sidebar.Sidebar; 6 | import org.bukkit.Bukkit; 7 | import org.bukkit.entity.Player; 8 | import org.bukkit.plugin.Plugin; 9 | import org.bukkit.scheduler.BukkitTask; 10 | 11 | import java.util.*; 12 | import java.util.function.Consumer; 13 | 14 | public class SidebarPager { 15 | 16 | private final List> sidebars; 17 | private final Iterator> pageIterator; 18 | private final Set viewers; 19 | private final BukkitTask switchTask; 20 | private Sidebar currentPage; 21 | 22 | /** 23 | * Creates a new sidebar pager. 24 | * 25 | * @param sidebars - list of sidebars to use 26 | * @param switchDelayTicks - delay between page switches in ticks (if value is 0, pages will not be switched automatically) 27 | * @param plugin - plugin instance 28 | */ 29 | public SidebarPager(@NonNull List> sidebars, long switchDelayTicks, @NonNull Plugin plugin) { 30 | this.sidebars = sidebars; 31 | this.viewers = new HashSet<>(); 32 | this.pageIterator = Iterators.cycle(sidebars); 33 | this.currentPage = pageIterator.next(); 34 | 35 | if (switchDelayTicks > 0) { 36 | this.switchTask = plugin.getServer().getScheduler().runTaskTimer(plugin, this::switchPage, switchDelayTicks, switchDelayTicks); 37 | } else { 38 | this.switchTask = null; 39 | } 40 | } 41 | 42 | public void applyToAll(Consumer> consumer) { 43 | sidebars.forEach(consumer); 44 | } 45 | 46 | /** 47 | * Switches to the next page. 48 | * Note: this method is called automatically by the scheduler. 49 | */ 50 | public void switchPage() { 51 | currentPage.removeViewers(); 52 | 53 | currentPage = pageIterator.next(); 54 | 55 | for (UUID viewer : viewers) { 56 | Player player = Bukkit.getPlayer(viewer); 57 | if (player != null) { 58 | currentPage.addViewer(player); 59 | } 60 | } 61 | } 62 | 63 | public Sidebar getCurrentPage() { 64 | return currentPage; 65 | } 66 | 67 | public Set getViewers() { 68 | return Collections.unmodifiableSet(viewers); 69 | } 70 | 71 | public List> getSidebars() { 72 | return Collections.unmodifiableList(sidebars); 73 | } 74 | 75 | /** 76 | * Adds a page status line to all sidebars in pager. 77 | */ 78 | public void addPageLine(PageConsumer consumer) { 79 | int page = 1; 80 | int maxPage = sidebars.size(); 81 | 82 | for (Sidebar sidebar : sidebars) { 83 | consumer.accept(page, maxPage, sidebar); 84 | page++; 85 | } 86 | } 87 | 88 | /** 89 | * Destroy all sidebars in pager. 90 | * Note: pager object will be unusable after this method call. 91 | */ 92 | public void destroy() { 93 | if (switchTask != null) { 94 | switchTask.cancel(); 95 | } 96 | for (Sidebar sidebar : sidebars) { 97 | sidebar.destroy(); 98 | } 99 | sidebars.clear(); 100 | viewers.clear(); 101 | } 102 | 103 | /** 104 | * Start showing all sidebars in pager to the player. 105 | * 106 | * @param player - player to show sidebars to 107 | */ 108 | public void show(@NonNull Player player) { 109 | synchronized (viewers) { 110 | viewers.add(player.getUniqueId()); 111 | } 112 | currentPage.addViewer(player); 113 | } 114 | 115 | /** 116 | * Stop showing all sidebars in pager to the player. 117 | * 118 | * @param player - player to stop showing sidebars to 119 | */ 120 | public void hide(@NonNull Player player) { 121 | synchronized (viewers) { 122 | viewers.remove(player.getUniqueId()); 123 | } 124 | currentPage.removeViewer(player); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/protocol/ChannelInjector.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.protocol; 2 | 3 | import com.google.common.collect.MapMaker; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.ChannelFuture; 6 | import lombok.NonNull; 7 | import lombok.SneakyThrows; 8 | import me.catcoder.sidebar.util.Reflection; 9 | import org.bukkit.entity.Player; 10 | 11 | import java.lang.invoke.MethodHandle; 12 | import java.lang.invoke.MethodHandles; 13 | import java.util.Map; 14 | 15 | public class ChannelInjector { 16 | 17 | private static final MethodHandle GET_PLAYER_HANDLE; 18 | private static final MethodHandle GET_CONNECTION; 19 | private static final MethodHandle GET_MANAGER; 20 | private static final MethodHandle GET_CHANNEL; 21 | 22 | static { 23 | MethodHandles.Lookup lookup = MethodHandles.lookup(); 24 | 25 | try { 26 | GET_PLAYER_HANDLE = lookup.unreflect( 27 | Reflection.getMethod("{obc}.entity.CraftPlayer", "getHandle").handle()); 28 | 29 | Class entityPlayer = Reflection.getClass( 30 | "net.minecraft.server.level.EntityPlayer", 31 | "net.minecraft.server.level.ServerPlayer", 32 | "{nms}.EntityPlayer"); 33 | 34 | Class playerConnection = Reflection.getClass( 35 | "{nms}.PlayerConnection", 36 | "net.minecraft.server.network.PlayerConnection", 37 | "net.minecraft.server.network.ServerGamePacketListenerImpl"); 38 | Class networkManager = Reflection.getClass( 39 | "{nms}.NetworkManager", 40 | "net.minecraft.network.NetworkManager", 41 | "net.minecraft.network.Connection"); 42 | 43 | GET_CONNECTION = lookup.unreflectGetter( 44 | Reflection.getField(entityPlayer, playerConnection, 0).handle()); 45 | GET_MANAGER = lookup.unreflectGetter( 46 | Reflection.getField(playerConnection, networkManager, 0).handle()); 47 | 48 | GET_CHANNEL = lookup.unreflectGetter( 49 | Reflection.getField(networkManager, Channel.class, 0).handle()); 50 | 51 | } catch (Throwable throwable) { 52 | throw new ExceptionInInitializerError(throwable); 53 | } 54 | } 55 | 56 | // weakValues means that channel will be removed from map when player disconnects 57 | private final Map channelLookup = new MapMaker().weakValues().makeMap(); 58 | 59 | public static final ChannelInjector IMP = new ChannelInjector(); 60 | 61 | private ChannelInjector() { 62 | // Seal class 63 | } 64 | 65 | @SneakyThrows 66 | public Channel getChannel(Player player) { 67 | Channel channel = channelLookup.get(player.getName()); 68 | 69 | // Lookup channel again 70 | if (channel == null || !channel.isOpen()) { 71 | Object connection = GET_CONNECTION.invoke(GET_PLAYER_HANDLE.invoke(player)); 72 | Object manager = GET_MANAGER.invoke(connection); 73 | 74 | channelLookup.put(player.getName(), channel = (Channel) GET_CHANNEL.invoke(manager)); 75 | } 76 | 77 | return channel; 78 | } 79 | 80 | public ChannelFuture sendPacket(@NonNull Player player, @NonNull Object packet) { 81 | return getChannel(player).writeAndFlush(packet); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/protocol/PacketIds.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.protocol; 2 | 3 | import me.catcoder.sidebar.util.version.VersionUtil; 4 | 5 | import static me.catcoder.sidebar.protocol.ProtocolConstants.map; 6 | 7 | 8 | public enum PacketIds { 9 | 10 | UPDATE_TEAMS( 11 | map(ProtocolConstants.MINECRAFT_1_12_2, 0x44), 12 | map(ProtocolConstants.MINECRAFT_1_13, 0x47), 13 | map(ProtocolConstants.MINECRAFT_1_14, 0x4B), 14 | map(ProtocolConstants.MINECRAFT_1_15, 0x4C), 15 | map(ProtocolConstants.MINECRAFT_1_17, 0x55), 16 | map(ProtocolConstants.MINECRAFT_1_19_1, 0x58), 17 | map(ProtocolConstants.MINECRAFT_1_19_3, 0x56), 18 | map(ProtocolConstants.MINECRAFT_1_19_4, 0x5A), 19 | map(ProtocolConstants.MINECRAFT_1_20_2, 0x5C), 20 | map(ProtocolConstants.MINECRAFT_1_20_4, 0x5E), 21 | map(ProtocolConstants.MINECRAFT_1_20_4, 0x5E), 22 | map(ProtocolConstants.MINECRAFT_1_20_6, 0x60), 23 | map(ProtocolConstants.MINECRAFT_1_21_2, 0x67) 24 | ), 25 | UPDATE_SCORE( 26 | map(ProtocolConstants.MINECRAFT_1_12_2, 0x45), 27 | map(ProtocolConstants.MINECRAFT_1_13, 0x48), 28 | map(ProtocolConstants.MINECRAFT_1_14, 0x4C), 29 | map(ProtocolConstants.MINECRAFT_1_15, 0x4D), 30 | map(ProtocolConstants.MINECRAFT_1_17, 0x56), 31 | map(ProtocolConstants.MINECRAFT_1_19_1, 0x59), 32 | map(ProtocolConstants.MINECRAFT_1_19_3, 0x57), 33 | map(ProtocolConstants.MINECRAFT_1_19_4, 0x5B), 34 | map(ProtocolConstants.MINECRAFT_1_20_2, 0x5D), 35 | map(ProtocolConstants.MINECRAFT_1_20_4, 0x5F), 36 | map(ProtocolConstants.MINECRAFT_1_20_6, 0x61), 37 | map(ProtocolConstants.MINECRAFT_1_21_2, 0x68) 38 | 39 | 40 | ), 41 | RESET_SCORE( 42 | map(ProtocolConstants.MINECRAFT_1_20_3, 0x42), 43 | map(ProtocolConstants.MINECRAFT_1_20_4, 0x44), 44 | map(ProtocolConstants.MINECRAFT_1_21_2, 0x49) 45 | ), 46 | OBJECTIVE_DISPLAY( 47 | map(ProtocolConstants.MINECRAFT_1_12_2, 0x3B), 48 | map(ProtocolConstants.MINECRAFT_1_13, 0x3E), 49 | map(ProtocolConstants.MINECRAFT_1_14, 0x42), 50 | map(ProtocolConstants.MINECRAFT_1_15, 0x43), 51 | map(ProtocolConstants.MINECRAFT_1_17, 0x4C), 52 | map(ProtocolConstants.MINECRAFT_1_19_1, 0x4F), 53 | map(ProtocolConstants.MINECRAFT_1_19_3, 0x4D), 54 | map(ProtocolConstants.MINECRAFT_1_19_4, 0x51), 55 | map(ProtocolConstants.MINECRAFT_1_20_2, 0x53), 56 | map(ProtocolConstants.MINECRAFT_1_20_4, 0x55), 57 | map(ProtocolConstants.MINECRAFT_1_20_6, 0x57), 58 | map(ProtocolConstants.MINECRAFT_1_21_2, 0x5C) 59 | 60 | ), 61 | OBJECTIVE( 62 | map(ProtocolConstants.MINECRAFT_1_12_2, 0x42), 63 | map(ProtocolConstants.MINECRAFT_1_13, 0x45), 64 | map(ProtocolConstants.MINECRAFT_1_14, 0x49), 65 | map(ProtocolConstants.MINECRAFT_1_15, 0x4A), 66 | map(ProtocolConstants.MINECRAFT_1_17, 0x53), 67 | map(ProtocolConstants.MINECRAFT_1_19_1, 0x56), 68 | map(ProtocolConstants.MINECRAFT_1_19_3, 0x54), 69 | map(ProtocolConstants.MINECRAFT_1_19_4, 0x58), 70 | map(ProtocolConstants.MINECRAFT_1_20_2, 0x5A), 71 | map(ProtocolConstants.MINECRAFT_1_20_4, 0x5C), 72 | map(ProtocolConstants.MINECRAFT_1_20_6, 0x5E), 73 | map(ProtocolConstants.MINECRAFT_1_21_2, 0x64) 74 | ); 75 | 76 | private final ProtocolConstants.ProtocolMapping[] mappings; 77 | 78 | PacketIds(ProtocolConstants.ProtocolMapping... mappings) { 79 | this.mappings = mappings; 80 | } 81 | 82 | public int getServerPacketId() { 83 | return getPacketId(VersionUtil.SERVER_VERSION); 84 | } 85 | 86 | public int getPacketId(int protocolVersion) { 87 | 88 | for (int protocol = ProtocolConstants.MINIMUM_SUPPORTED_VERSION; 89 | protocol <= ProtocolConstants.MAXIMUM_SUPPORTED_VERSION; protocol++) { 90 | int index = 0; 91 | 92 | for (ProtocolConstants.ProtocolMapping mapping : mappings) { 93 | if (mapping.getProtocol() == protocol 94 | && mapping.getProtocol() <= protocolVersion 95 | && (index == mappings.length - 1 || mappings[index + 1].getProtocol() > protocolVersion)) 96 | return mapping.getPacketId(); 97 | 98 | index++; 99 | } 100 | 101 | } 102 | 103 | throw new IllegalArgumentException("Unsupported protocol version: " + protocolVersion); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/protocol/ProtocolConstants.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.protocol; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | public class ProtocolConstants { 7 | public static final int MINECRAFT_1_12_2 = 340; 8 | public static final int MINECRAFT_1_13 = 393; 9 | public static final int MINECRAFT_1_13_1 = 401; 10 | public static final int MINECRAFT_1_13_2 = 404; 11 | public static final int MINECRAFT_1_14 = 477; 12 | public static final int MINECRAFT_1_14_1 = 480; 13 | public static final int MINECRAFT_1_14_2 = 485; 14 | public static final int MINECRAFT_1_14_3 = 490; 15 | public static final int MINECRAFT_1_14_4 = 498; 16 | public static final int MINECRAFT_1_15 = 573; 17 | public static final int MINECRAFT_1_15_1 = 575; 18 | public static final int MINECRAFT_1_15_2 = 578; 19 | 20 | public static final int MINECRAFT_1_16 = 735; 21 | public static final int MINECRAFT_1_16_1 = 736; 22 | public static final int MINECRAFT_1_16_2 = 751; 23 | public static final int MINECRAFT_1_16_3 = 753; 24 | public static final int MINECRAFT_1_16_4 = 754; 25 | public static final int MINECRAFT_1_16_5 = 754; 26 | 27 | public static final int MINECRAFT_1_17 = 755; 28 | public static final int MINECRAFT_1_17_1 = 756; 29 | 30 | public static final int MINECRAFT_1_18 = 757; 31 | public static final int MINECRAFT_1_18_1 = 757; 32 | public static final int MINECRAFT_1_18_2 = 758; 33 | 34 | public static final int MINECRAFT_1_19 = 759; 35 | 36 | public static final int MINECRAFT_1_19_1 = 760; 37 | public static final int MINECRAFT_1_19_2 = 760; 38 | 39 | public static final int MINECRAFT_1_19_3 = 761; 40 | 41 | public static final int MINECRAFT_1_19_4 = 762; 42 | 43 | public static final int MINECRAFT_1_20 = 763; 44 | public static final int MINECRAFT_1_20_1 = 763; 45 | public static final int MINECRAFT_1_20_2 = 764; 46 | public static final int MINECRAFT_1_20_3 = 765; 47 | public static final int MINECRAFT_1_20_4 = 765; 48 | public static final int MINECRAFT_1_20_5 = 766; 49 | public static final int MINECRAFT_1_20_6 = 766; 50 | 51 | public static final int MINECRAFT_1_21 = 767; 52 | 53 | public static final int MINECRAFT_1_21_2 = 768; 54 | 55 | public static final int MINIMUM_SUPPORTED_VERSION = MINECRAFT_1_12_2; 56 | public static final int MAXIMUM_SUPPORTED_VERSION = MINECRAFT_1_21_2; 57 | 58 | @Getter 59 | @RequiredArgsConstructor 60 | public static class ProtocolMapping { 61 | public final int protocol; 62 | public final int packetId; 63 | } 64 | 65 | 66 | public static ProtocolMapping map(int protocol, int packetId) { 67 | return new ProtocolMapping(protocol, packetId); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/protocol/ScoreNumberFormat.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.protocol; 2 | 3 | import com.google.common.base.Preconditions; 4 | import me.catcoder.sidebar.util.buffer.NetOutput; 5 | 6 | import java.util.function.BiConsumer; 7 | 8 | public enum ScoreNumberFormat implements BiConsumer { 9 | 10 | BLANK { 11 | @Override 12 | public void accept(NetOutput out, String value) { 13 | out.writeVarInt(0); 14 | } 15 | }, 16 | STYLED { 17 | @Override 18 | public void accept(NetOutput netOutput, String value) { 19 | netOutput.writeVarInt(1); 20 | Preconditions.checkArgument(value != null, "Value cannot be null for STYLED format"); 21 | netOutput.writeComponent(value); 22 | } 23 | }, 24 | FIXED { 25 | @Override 26 | public void accept(NetOutput out, String value) { 27 | out.writeVarInt(2); 28 | Preconditions.checkArgument(value != null, "Value cannot be null for FIXED format"); 29 | out.writeComponent(value); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/protocol/ScoreboardPackets.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.protocol; 2 | 3 | import com.google.common.base.Function; 4 | import com.google.common.base.Preconditions; 5 | import com.google.common.base.Splitter; 6 | import io.netty.buffer.ByteBuf; 7 | import lombok.NonNull; 8 | import lombok.SneakyThrows; 9 | import lombok.experimental.UtilityClass; 10 | import me.catcoder.sidebar.text.TextProvider; 11 | import me.catcoder.sidebar.util.buffer.ByteBufNetOutput; 12 | import me.catcoder.sidebar.util.buffer.NetOutput; 13 | import me.catcoder.sidebar.util.version.VersionUtil; 14 | import org.bukkit.ChatColor; 15 | import org.bukkit.entity.Player; 16 | 17 | import javax.annotation.Nullable; 18 | import java.util.Iterator; 19 | 20 | @UtilityClass 21 | public class ScoreboardPackets { 22 | 23 | private final Splitter SPLITTER = Splitter.fixedLength(16); 24 | 25 | public final ChatColor[] COLORS = ChatColor.values(); 26 | 27 | public final int TEAM_CREATED = 0; 28 | public final int TEAM_REMOVED = 1; 29 | public final int TEAM_UPDATED = 2; 30 | 31 | public ByteBuf createTeamPacket(int mode, int index, 32 | @NonNull String teamName, 33 | @NonNull Player player, 34 | R text, 35 | @NonNull TextProvider textProvider) { 36 | return createTeamPacket(mode, index, teamName, VersionUtil.SERVER_VERSION, player, text, textProvider); 37 | } 38 | 39 | public ByteBuf createScorePacket(@NonNull Player player, 40 | int action, 41 | String objectiveName, 42 | int score, 43 | int index, 44 | TextProvider textProvider, 45 | @Nullable ScoreNumberFormat numberFormat, 46 | @Nullable Function scoreNumberFormatter) { 47 | ByteBuf buf = ChannelInjector.IMP.getChannel(player).alloc().buffer(); 48 | 49 | NetOutput output = new ByteBufNetOutput(buf); 50 | 51 | if (VersionUtil.SERVER_VERSION >= ProtocolConstants.MINECRAFT_1_20_3) { 52 | if (action == 1) { 53 | output.writeVarInt(PacketIds.RESET_SCORE.getServerPacketId()); 54 | output.writeString(ScoreboardPackets.COLORS[index].toString()); 55 | output.writeBoolean(true); // has objective name 56 | output.writeString(objectiveName); 57 | return buf; 58 | } 59 | 60 | output.writeVarInt(PacketIds.UPDATE_SCORE.getServerPacketId()); 61 | output.writeString(ScoreboardPackets.COLORS[index].toString()); 62 | output.writeString(objectiveName); 63 | output.writeVarInt(score); 64 | 65 | output.writeBoolean(false); // has display name 66 | output.writeBoolean(numberFormat != null); 67 | 68 | if (numberFormat != null) { 69 | numberFormat.accept(output, scoreNumberFormatter == null ? 70 | null : 71 | textProvider.asJsonMessage(player, scoreNumberFormatter.apply(player))); 72 | } 73 | 74 | return buf; 75 | } 76 | 77 | output.writeVarInt(PacketIds.UPDATE_SCORE.getServerPacketId()); 78 | 79 | output.writeString(ScoreboardPackets.COLORS[index].toString()); 80 | 81 | if (VersionUtil.SERVER_VERSION >= ProtocolConstants.MINECRAFT_1_13) { 82 | output.writeVarInt(action); 83 | } else { 84 | output.writeByte(action); 85 | } 86 | 87 | output.writeString(objectiveName); 88 | 89 | if (action != 1) { 90 | output.writeVarInt(score); 91 | } 92 | 93 | return buf; 94 | } 95 | 96 | @SneakyThrows 97 | public ByteBuf createTeamPacket(int mode, int index, 98 | @NonNull String teamName, 99 | int serverVersion, 100 | @NonNull Player player, 101 | R text, 102 | @NonNull TextProvider provider) { 103 | Preconditions.checkArgument(mode >= TEAM_CREATED && mode <= TEAM_UPDATED, "Invalid team mode"); 104 | 105 | String teamEntry = COLORS[index].toString(); 106 | int clientVersion = VersionUtil.getPlayerVersion(player.getUniqueId()); 107 | 108 | ByteBuf buf = ChannelInjector.IMP.getChannel(player).alloc().buffer(); 109 | 110 | NetOutput packet = new ByteBufNetOutput(buf); 111 | 112 | // construct the packet on lowest level for future compatibility 113 | 114 | packet.writeVarInt(PacketIds.UPDATE_TEAMS.getServerPacketId()); 115 | 116 | packet.writeString(teamName); 117 | packet.writeByte(mode); 118 | 119 | if (mode == TEAM_REMOVED) { 120 | return buf; 121 | } 122 | 123 | if (clientVersion >= ProtocolConstants.MINECRAFT_1_13) { 124 | if (serverVersion >= ProtocolConstants.MINECRAFT_1_20_3) { 125 | packet.writeComponent("{\"text\":\"\"}"); 126 | } else { 127 | packet.writeString("{\"text\":\"\"}"); // team display name 128 | } 129 | } else { 130 | packet.writeString(""); 131 | } 132 | 133 | // Since 1.13 character limit for prefix/suffix was removed 134 | if (clientVersion >= ProtocolConstants.MINECRAFT_1_13) { 135 | 136 | if (serverVersion >= ProtocolConstants.MINECRAFT_1_20_3) { 137 | writeDefaults(serverVersion, packet); 138 | packet.writeComponent(provider.asJsonMessage(player, text)); 139 | packet.writeComponent("{\"text\":\"\"}"); 140 | 141 | } else if (serverVersion >= ProtocolConstants.MINECRAFT_1_13) { 142 | writeDefaults(serverVersion, packet); 143 | packet.writeString(provider.asJsonMessage(player, text)); 144 | packet.writeString("{\"text\":\"\"}"); 145 | } else { 146 | String legacyText = provider.asLegacyMessage(player, text); 147 | 148 | packet.writeString(legacyText); 149 | packet.writeString(ChatColor.WHITE.toString()); 150 | writeDefaults(serverVersion, packet); 151 | } 152 | 153 | if (mode == TEAM_CREATED) { 154 | packet.writeVarInt(1); // number of players 155 | packet.writeString(teamEntry); // entries 156 | } 157 | 158 | return buf; 159 | } 160 | 161 | // 1.12 and below stuff :( 162 | // I'll remove it in future 163 | 164 | String legacyText = provider.asLegacyMessage(player, text); 165 | 166 | Iterator iterator = SPLITTER.split(legacyText).iterator(); 167 | String prefix = iterator.next(); 168 | String suffix = ""; 169 | 170 | if (legacyText.length() > 16) { 171 | String prefixColor = ChatColor.getLastColors(prefix); 172 | suffix = iterator.next(); 173 | 174 | if (prefix.endsWith(String.valueOf(ChatColor.COLOR_CHAR))) { 175 | prefix = prefix.substring(0, prefix.length() - 1); 176 | 177 | prefixColor = ChatColor.getByChar(suffix.charAt(0)).toString(); 178 | suffix = suffix.substring(1); 179 | } 180 | 181 | suffix = ((prefixColor.equals("") ? ChatColor.RESET : prefixColor) + suffix); 182 | 183 | if (suffix.length() > 16) { 184 | suffix = suffix.substring(0, 13) + "..."; 185 | } 186 | } 187 | 188 | if (serverVersion < ProtocolConstants.MINECRAFT_1_13) { 189 | packet.writeString(prefix); 190 | packet.writeString(suffix); 191 | writeDefaults(serverVersion, packet); 192 | 193 | } else { 194 | writeDefaults(serverVersion, packet); 195 | packet.writeString(provider.asJsonMessage(player, provider.fromLegacyMessage(prefix))); // prefix 196 | packet.writeString(provider.asJsonMessage(player, provider.fromLegacyMessage(suffix))); // suffix 197 | } 198 | 199 | if (mode == TEAM_CREATED) { 200 | packet.writeVarInt(1); // number of players 201 | packet.writeString(teamEntry); // entries 202 | } 203 | 204 | return buf; 205 | } 206 | 207 | private static void writeDefaults(int serverVersion, @NonNull NetOutput packet) { 208 | packet.writeByte(10); // friendly tags 209 | packet.writeString("always"); // name tag visibility 210 | packet.writeString("always"); // collision rule 211 | if (serverVersion < ProtocolConstants.MINECRAFT_1_13) { 212 | packet.writeByte(-1); // reset color 213 | } else { 214 | packet.writeVarInt(21); 215 | } 216 | } 217 | 218 | 219 | } 220 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/text/FrameIterator.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.text; 2 | 3 | import com.google.common.collect.Iterators; 4 | import lombok.NonNull; 5 | 6 | import java.util.Iterator; 7 | 8 | public final class FrameIterator implements Iterator { 9 | 10 | private final Iterator frameIterator; 11 | 12 | private long currentFrameDelayTicks = 0; 13 | private TextFrame currentFrame; 14 | 15 | public FrameIterator(@NonNull Iterable frames) { 16 | this.frameIterator = Iterators.cycle(frames); 17 | } 18 | 19 | 20 | @Override 21 | public String next() { 22 | if (currentFrame == null || --currentFrameDelayTicks <= 0) { 23 | currentFrame = frameIterator.next(); 24 | currentFrameDelayTicks = currentFrame.getDelay(); 25 | } 26 | 27 | return currentFrame.getText(); 28 | } 29 | 30 | 31 | @Override 32 | public boolean hasNext() { 33 | return true; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/text/TextFrame.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.text; 2 | 3 | import lombok.Value; 4 | 5 | @Value(staticConstructor = "of") 6 | public class TextFrame { 7 | 8 | String text; 9 | long delay; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/text/TextIterator.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.text; 2 | 3 | import java.util.Iterator; 4 | import java.util.List; 5 | 6 | public abstract class TextIterator implements Iterator { 7 | 8 | @Override 9 | public abstract String next(); 10 | 11 | protected void end(List frames) { 12 | 13 | } 14 | 15 | protected void start(List frames) { 16 | 17 | } 18 | 19 | @Override 20 | public boolean hasNext() { 21 | return true; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/text/TextIterators.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.text; 2 | 3 | import lombok.NonNull; 4 | import lombok.experimental.UtilityClass; 5 | import me.catcoder.sidebar.text.impl.TextFadeAnimation; 6 | import me.catcoder.sidebar.text.impl.TextTypingAnimation; 7 | import org.bukkit.ChatColor; 8 | 9 | 10 | @UtilityClass 11 | public class TextIterators { 12 | 13 | /** 14 | * Creates new text typing animation. 15 | * 16 | * @param text - text to type 17 | * @return new text typing animation 18 | */ 19 | public static TextIterator textTypingOldSchool(@NonNull String text) { 20 | return textTyping(text, "_", 10, 2); 21 | } 22 | 23 | /** 24 | * Creates new text typing animation. 25 | * 26 | * @param text - text to type 27 | * @param cursor - cursor to flick 28 | * @param idleTicks - idle ticks between typing 29 | * @param typingSpeedTicks - ticks between each character 30 | * @return new text typing animation 31 | */ 32 | public static TextIterator textTyping(@NonNull String text, @NonNull String cursor, int idleTicks, int typingSpeedTicks) { 33 | return new TextTypingAnimation(text, cursor, idleTicks, typingSpeedTicks); 34 | } 35 | 36 | /** 37 | * Creates new text fade animation. 38 | * 39 | * @param text - text to fade 40 | * @param primaryColor - primary color 41 | * @param fadeColor - fade color 42 | * @param secondaryColor - secondary color 43 | * @return new text fade animation 44 | */ 45 | public TextIterator textFade(@NonNull String text, @NonNull ChatColor primaryColor, @NonNull ChatColor fadeColor, @NonNull ChatColor secondaryColor) { 46 | return new TextFadeAnimation(text, primaryColor, fadeColor, secondaryColor); 47 | } 48 | 49 | /** 50 | * Creates new text fade animation. Just like on Hypixel server sidebars title. 51 | * 52 | * @param text - text to fade 53 | * @return new text fade animation 54 | */ 55 | public TextIterator textFadeHypixel(@NonNull String text) { 56 | return textFade(text, ChatColor.YELLOW, ChatColor.GOLD, ChatColor.WHITE); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/text/TextProvider.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.text; 2 | 3 | import lombok.NonNull; 4 | import org.bukkit.entity.Player; 5 | 6 | public interface TextProvider { 7 | 8 | String asJsonMessage(@NonNull Player player, @NonNull T component); 9 | 10 | String asLegacyMessage(@NonNull Player player, @NonNull T component); 11 | 12 | T emptyMessage(); 13 | 14 | T fromLegacyMessage(@NonNull String message); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/text/impl/TextFadeAnimation.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.text.impl; 2 | 3 | import com.google.common.base.Preconditions; 4 | import lombok.NonNull; 5 | import lombok.experimental.Delegate; 6 | import me.catcoder.sidebar.text.FrameIterator; 7 | import me.catcoder.sidebar.text.TextFrame; 8 | import me.catcoder.sidebar.text.TextIterator; 9 | import org.bukkit.ChatColor; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | /** 15 | * Simple text animation with 3 colors. 16 | * Just like on Hypixel server sidebars title. 17 | * 18 | * @author CatCoder 19 | */ 20 | public class TextFadeAnimation extends TextIterator { 21 | 22 | private final String text; 23 | private final ChatColor primaryColor; 24 | private final ChatColor fadeColor; 25 | private final ChatColor secondaryColor; 26 | 27 | private final String colorCodes; 28 | 29 | @Delegate(types = { FrameIterator.class }) 30 | private final FrameIterator frameIterator; 31 | 32 | 33 | public TextFadeAnimation( 34 | @NonNull String text, 35 | @NonNull ChatColor primaryColor, 36 | @NonNull ChatColor fadeColor, 37 | @NonNull ChatColor secondaryColor) { 38 | Preconditions.checkArgument(text.length() >= 3, "Text length must be at least 3 characters"); 39 | 40 | this.text = ChatColor.stripColor(text); 41 | this.primaryColor = primaryColor; 42 | this.fadeColor = fadeColor; 43 | this.secondaryColor = secondaryColor; 44 | this.colorCodes = ChatColor.getLastColors(text); 45 | 46 | this.frameIterator = new FrameIterator(createAnimationFrames()); 47 | 48 | } 49 | 50 | @Override 51 | protected void start(List frames) { 52 | frames.add(TextFrame.of(primaryColor + colorCodes + text, 3 * 20)); 53 | } 54 | 55 | @Override 56 | protected void end(List frames) { 57 | // flick 58 | TextFrame secondary = TextFrame.of(secondaryColor + colorCodes + text, 8); 59 | TextFrame primary = TextFrame.of(primaryColor + colorCodes + text, 8); 60 | 61 | frames.add(secondary); 62 | frames.add(primary); 63 | frames.add(secondary); 64 | frames.add(primary); 65 | } 66 | 67 | public List createAnimationFrames() { 68 | List frames = new ArrayList<>(); 69 | 70 | start(frames); 71 | 72 | int primaryColorLength = primaryColor.toString().length(); 73 | 74 | for (int i = 0; i < text.length(); i++) { 75 | StringBuilder builder = new StringBuilder(text); 76 | 77 | int offset = 0; 78 | 79 | // put secondary color in front if fadeColor was used 80 | if (i > 0) { 81 | builder.insert(0, secondaryColor); 82 | 83 | offset += colorCodes.length(); 84 | builder.insert(offset, colorCodes); 85 | 86 | offset += primaryColorLength; 87 | } 88 | 89 | 90 | // fadeColor + colorCodes 91 | builder.insert(i + offset, colorCodes); 92 | builder.insert(i + 1 + offset + colorCodes.length(), primaryColor); 93 | 94 | // primaryColor + colorCodes 95 | builder.insert(i + offset + primaryColorLength + colorCodes.length() + 1, colorCodes); 96 | builder.insert(i + offset, fadeColor); 97 | 98 | 99 | if (i + 1 == text.length()) { 100 | // remove primary color and other color codes from the end 101 | builder.setLength(builder.length() - primaryColorLength - colorCodes.length()); 102 | } 103 | 104 | frames.add(TextFrame.of(builder.toString(), 2)); 105 | 106 | } 107 | 108 | end(frames); 109 | 110 | return frames; 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/text/impl/TextSlideAnimation.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.text.impl; 2 | 3 | import lombok.NonNull; 4 | import lombok.experimental.Delegate; 5 | import me.catcoder.sidebar.text.FrameIterator; 6 | import me.catcoder.sidebar.text.TextFrame; 7 | import me.catcoder.sidebar.text.TextIterator; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * Text sliding animation. WIP 13 | * 14 | * @author CatCoder 15 | */ 16 | public class TextSlideAnimation extends TextIterator { 17 | 18 | private final String text; 19 | 20 | @Delegate(types = {FrameIterator.class}) 21 | private final FrameIterator frameIterator; 22 | 23 | public TextSlideAnimation(@NonNull String text) { 24 | this.text = text; 25 | this.frameIterator = new FrameIterator(createAnimationFrames()); 26 | } 27 | 28 | private List createAnimationFrames() { 29 | // TODO: 17.11.2022 contributions are welcome :) 30 | throw new UnsupportedOperationException("Unsupported yet"); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/text/impl/TextTypingAnimation.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.text.impl; 2 | 3 | import lombok.NonNull; 4 | import lombok.experimental.Delegate; 5 | import me.catcoder.sidebar.text.FrameIterator; 6 | import me.catcoder.sidebar.text.TextFrame; 7 | import me.catcoder.sidebar.text.TextIterator; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class TextTypingAnimation extends TextIterator { 13 | 14 | private final String text; 15 | private final String cursor; 16 | private final int idleTimeTicks; 17 | private final int typingSpeed; 18 | 19 | @Delegate(types = {FrameIterator.class}) 20 | private final FrameIterator frameIterator; 21 | 22 | public TextTypingAnimation(@NonNull String text, @NonNull String cursor, int idleTicks, int typingSpeedTicks) { 23 | this.text = text; 24 | this.cursor = cursor; 25 | this.idleTimeTicks = idleTicks; 26 | this.typingSpeed = typingSpeedTicks; 27 | 28 | this.frameIterator = new FrameIterator(createAnimationFrames()); 29 | } 30 | 31 | @Override 32 | protected void start(List frames) { 33 | flickWithCursor(idleTimeTicks, frames, ""); 34 | } 35 | 36 | private void flickWithCursor(int times, List frames, String text) { 37 | for (int i = 0; i <= times; i++) { 38 | frames.add( 39 | i % 2 == 0 40 | ? TextFrame.of(text + cursor, 10) 41 | : TextFrame.of(text, 10) 42 | ); 43 | } 44 | } 45 | 46 | public List createAnimationFrames() { 47 | // flick with cursor 48 | 49 | List frames = new ArrayList<>(); 50 | 51 | start(frames); 52 | 53 | for (int i = 0; i < text.length(); i++) { 54 | String frame = text.substring(0, i + 1); 55 | frames.add(TextFrame.of(frame + cursor, typingSpeed)); 56 | } 57 | 58 | flickWithCursor(idleTimeTicks, frames, text); 59 | 60 | // erase text simulating backspace 61 | 62 | for (int i = text.length() - 1; i >= 0; i--) { 63 | String frame = text.substring(0, i); 64 | frames.add(TextFrame.of(frame + cursor, typingSpeed)); 65 | } 66 | 67 | return frames; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/text/provider/AdventureTextProvider.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.text.provider; 2 | 3 | import lombok.NonNull; 4 | import me.catcoder.sidebar.text.TextProvider; 5 | import net.kyori.adventure.text.Component; 6 | import net.kyori.adventure.text.TextComponent; 7 | import net.kyori.adventure.text.serializer.ComponentSerializer; 8 | import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; 9 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; 10 | import org.bukkit.entity.Player; 11 | 12 | public class AdventureTextProvider implements TextProvider { 13 | 14 | public static final ComponentSerializer GSON_SERIALIZER = 15 | GsonComponentSerializer.gson(); 16 | public static final ComponentSerializer LEGACY_SERIALIZER = 17 | LegacyComponentSerializer.legacySection(); 18 | 19 | 20 | @Override 21 | public String asJsonMessage(@NonNull Player player, @NonNull Component component) { 22 | return GSON_SERIALIZER.serialize(component); 23 | } 24 | 25 | @Override 26 | public Component emptyMessage() { 27 | return Component.empty(); 28 | } 29 | 30 | @Override 31 | public Component fromLegacyMessage(@NonNull String message) { 32 | return LEGACY_SERIALIZER.deserialize(message); 33 | } 34 | 35 | @Override 36 | public String asLegacyMessage(@NonNull Player player, @NonNull Component component) { 37 | return LEGACY_SERIALIZER.serialize(component); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/text/provider/BungeeCordChatTextProvider.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.text.provider; 2 | 3 | import lombok.NonNull; 4 | import me.catcoder.sidebar.text.TextProvider; 5 | import net.md_5.bungee.api.chat.BaseComponent; 6 | import net.md_5.bungee.api.chat.TextComponent; 7 | import net.md_5.bungee.chat.ComponentSerializer; 8 | import org.bukkit.entity.Player; 9 | 10 | public class BungeeCordChatTextProvider implements TextProvider { 11 | 12 | private static final BaseComponent[] EMPTY = TextComponent.fromLegacyText(""); 13 | 14 | @Override 15 | public String asJsonMessage(@NonNull Player player, BaseComponent @NonNull [] components) { 16 | if (components.length > 0 && components[0] instanceof TextComponent textComponent) { 17 | textComponent.setColor(textComponent.getColor()); 18 | } 19 | return ComponentSerializer.toString(components); 20 | } 21 | 22 | @Override 23 | public BaseComponent[] emptyMessage() { 24 | return EMPTY; 25 | } 26 | 27 | @Override 28 | public BaseComponent[] fromLegacyMessage(@NonNull String message) { 29 | return TextComponent.fromLegacyText(message); 30 | } 31 | 32 | @Override 33 | public String asLegacyMessage(@NonNull Player player, BaseComponent @NonNull [] component) { 34 | return TextComponent.toLegacyText(component); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/text/provider/MiniMessageTextProvider.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.text.provider; 2 | 3 | import lombok.NonNull; 4 | import lombok.RequiredArgsConstructor; 5 | import me.catcoder.sidebar.text.TextProvider; 6 | import net.kyori.adventure.text.minimessage.MiniMessage; 7 | import org.bukkit.entity.Player; 8 | 9 | @RequiredArgsConstructor 10 | public class MiniMessageTextProvider implements TextProvider { 11 | 12 | protected final MiniMessage miniMessage; 13 | 14 | @Override 15 | public String asJsonMessage(@NonNull Player player, @NonNull String message) { 16 | return AdventureTextProvider.GSON_SERIALIZER.serialize(miniMessage.deserialize(message)); 17 | } 18 | 19 | @Override 20 | public String emptyMessage() { 21 | return ""; 22 | } 23 | 24 | @Override 25 | public String asLegacyMessage(@NonNull Player player, @NonNull String component) { 26 | return AdventureTextProvider.LEGACY_SERIALIZER.serialize(miniMessage.deserialize(component)); 27 | } 28 | 29 | @Override 30 | public String fromLegacyMessage(@NonNull String message) { 31 | return message; // will be transformed on packet level 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/text/provider/MiniPlaceholdersTextProvider.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.text.provider; 2 | 3 | import io.github.miniplaceholders.api.MiniPlaceholders; 4 | import lombok.NonNull; 5 | import net.kyori.adventure.text.minimessage.MiniMessage; 6 | import org.bukkit.entity.Player; 7 | 8 | public class MiniPlaceholdersTextProvider extends MiniMessageTextProvider { 9 | 10 | public MiniPlaceholdersTextProvider(MiniMessage miniMessage) { 11 | super(miniMessage); 12 | } 13 | 14 | @Override 15 | public String asJsonMessage(@NonNull Player player, @NonNull String message) { 16 | return AdventureTextProvider.GSON_SERIALIZER.serialize( 17 | miniMessage.deserialize(message, MiniPlaceholders.getAudienceGlobalPlaceholders(player))); 18 | } 19 | 20 | 21 | @Override 22 | public String asLegacyMessage(@NonNull Player player, @NonNull String component) { 23 | return AdventureTextProvider.LEGACY_SERIALIZER.serialize( 24 | miniMessage.deserialize(component, MiniPlaceholders.getAudienceGlobalPlaceholders(player))); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/util/NbtComponentSerializer.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.util; 2 | 3 | import com.viaversion.nbt.tag.*; 4 | import com.google.gson.JsonArray; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonObject; 7 | import com.google.gson.JsonPrimitive; 8 | import com.google.gson.internal.LazilyParsedNumber; 9 | import lombok.AllArgsConstructor; 10 | import org.jetbrains.annotations.Contract; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | import java.util.Arrays; 14 | import java.util.HashSet; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.Set; 18 | import java.util.UUID; 19 | 20 | /** 21 | * Taken from ViaVersion's ComponentConverter 22 | */ 23 | public class NbtComponentSerializer { 24 | 25 | private static final Set BOOLEAN_TYPES = new HashSet<>(Arrays.asList( 26 | "interpret", 27 | "bold", 28 | "italic", 29 | "underlined", 30 | "strikethrough", 31 | "obfuscated" 32 | )); 33 | // Order is important 34 | private static final List> COMPONENT_TYPES = Arrays.asList( 35 | new Pair<>("text", "text"), 36 | new Pair<>("translatable", "translate"), 37 | new Pair<>("score", "score"), 38 | new Pair<>("selector", "selector"), 39 | new Pair<>("keybind", "keybind"), 40 | new Pair<>("nbt", "nbt") 41 | ); 42 | 43 | private NbtComponentSerializer() { 44 | 45 | } 46 | 47 | @Contract("null -> null") 48 | public static JsonElement tagComponentToJson(@Nullable final Tag tag) { 49 | return convertToJson(null, tag); 50 | } 51 | 52 | 53 | public static @Nullable Tag jsonComponentToTag(@Nullable final JsonElement component) { 54 | return convertToTag(component); 55 | } 56 | 57 | @Contract("_, null -> null") 58 | private static Tag convertToTag(final @Nullable JsonElement element) { 59 | if (element == null || element.isJsonNull()) { 60 | return null; 61 | } else if (element.isJsonObject()) { 62 | final CompoundTag tag = new CompoundTag(); 63 | final JsonObject jsonObject = element.getAsJsonObject(); 64 | for (final Map.Entry entry : jsonObject.entrySet()) { 65 | convertObjectEntry(entry.getKey(), entry.getValue(), tag); 66 | } 67 | 68 | addComponentType(jsonObject, tag); 69 | return tag; 70 | } else if (element.isJsonArray()) { 71 | return convertJsonArray(element.getAsJsonArray()); 72 | } else if (element.isJsonPrimitive()) { 73 | final JsonPrimitive primitive = element.getAsJsonPrimitive(); 74 | if (primitive.isString()) { 75 | return new StringTag(primitive.getAsString()); 76 | } else if (primitive.isBoolean()) { 77 | return new ByteTag((byte) (primitive.getAsBoolean() ? 1 : 0)); 78 | } 79 | 80 | final Number number = primitive.getAsNumber(); 81 | if (number instanceof Integer) { 82 | return new IntTag(number.intValue()); 83 | } else if (number instanceof Byte) { 84 | return new ByteTag(number.byteValue()); 85 | } else if (number instanceof Short) { 86 | return new ShortTag(number.shortValue()); 87 | } else if (number instanceof Long) { 88 | return new LongTag(number.longValue()); 89 | } else if (number instanceof Double) { 90 | return new DoubleTag(number.doubleValue()); 91 | } else if (number instanceof Float) { 92 | return new FloatTag(number.floatValue()); 93 | } else if (number instanceof LazilyParsedNumber) { 94 | // TODO: This might need better handling 95 | return new IntTag(number.intValue()); 96 | } 97 | return new IntTag(number.intValue()); // ??? 98 | } 99 | throw new IllegalArgumentException("Unhandled json type " + element.getClass().getSimpleName() + " with value " + element.getAsString()); 100 | } 101 | 102 | private static ListTag convertJsonArray(final JsonArray array) { 103 | // TODO Number arrays? 104 | final ListTag listTag = new ListTag(); 105 | boolean singleType = true; 106 | for (final JsonElement entry : array) { 107 | final Tag convertedEntryTag = convertToTag(entry); 108 | if (listTag.getElementType() != null && listTag.getElementType() != convertedEntryTag.getClass()) { 109 | singleType = false; 110 | break; 111 | } 112 | 113 | listTag.add(convertedEntryTag); 114 | } 115 | 116 | if (singleType) { 117 | return listTag; 118 | } 119 | 120 | // Generally, vanilla-esque serializers should not produce this format, so it should be rare 121 | // Lists are only used for lists of components ("extra" and "with") 122 | final ListTag processedListTag = new ListTag(); 123 | for (final JsonElement entry : array) { 124 | final Tag convertedTag = convertToTag(entry); 125 | if (convertedTag instanceof CompoundTag) { 126 | processedListTag.add(convertedTag); 127 | continue; 128 | } 129 | 130 | // Wrap all entries in compound tags, as lists can only consist of one type of tag 131 | final CompoundTag compoundTag = new CompoundTag(); 132 | compoundTag.put("type", new StringTag("text")); 133 | if (convertedTag instanceof ListTag) { 134 | compoundTag.put("text", new StringTag("")); 135 | compoundTag.put("extra", new ListTag(((ListTag) convertedTag).getValue())); 136 | } else { 137 | compoundTag.put("text", new StringTag(stringValue(convertedTag))); 138 | } 139 | processedListTag.add(compoundTag); 140 | } 141 | return processedListTag; 142 | } 143 | 144 | /** 145 | * Converts a json object entry to a tag entry. 146 | * 147 | * @param key key of the entry 148 | * @param value value of the entry 149 | * @param tag the resulting compound tag 150 | */ 151 | private static void convertObjectEntry(final String key, final JsonElement value, final CompoundTag tag) { 152 | if ((key.equals("contents")) && value.isJsonObject()) { 153 | // Store show_entity id as int array instead of uuid string 154 | // Not really required, but we might as well make it more compact 155 | final JsonObject hoverEvent = value.getAsJsonObject(); 156 | final JsonElement id = hoverEvent.get("id"); 157 | final UUID uuid; 158 | if (id != null && id.isJsonPrimitive() && (uuid = parseUUID(id.getAsString())) != null) { 159 | final CompoundTag convertedTag = (CompoundTag) convertToTag(value); 160 | convertedTag.remove("id"); 161 | convertedTag.put("id", new IntArrayTag(toIntArray(uuid))); 162 | tag.put(key, convertedTag); 163 | return; 164 | } 165 | } 166 | 167 | tag.put(key, convertToTag(value)); 168 | } 169 | 170 | private static void addComponentType(final JsonObject object, final CompoundTag tag) { 171 | if (object.has("type")) { 172 | return; 173 | } 174 | 175 | // Add the type to speed up deserialization and make DFU errors slightly more useful 176 | for (final Pair pair : COMPONENT_TYPES) { 177 | if (object.has(pair.value)) { 178 | tag.put("type", new StringTag(pair.key)); 179 | return; 180 | } 181 | } 182 | } 183 | 184 | private static @Nullable JsonElement convertToJson(final @Nullable String key, final @Nullable Tag tag) { 185 | if (tag == null) { 186 | return null; 187 | } else if (tag instanceof CompoundTag) { 188 | final JsonObject object = new JsonObject(); 189 | if (!"value".equals(key)) { 190 | removeComponentType(object); 191 | } 192 | 193 | for (final Map.Entry entry : ((CompoundTag) tag)) { 194 | convertCompoundTagEntry(entry.getKey(), entry.getValue(), object); 195 | } 196 | return object; 197 | } else if (tag instanceof ListTag) { 198 | final ListTag list = (ListTag) tag; 199 | final JsonArray array = new JsonArray(); 200 | for (final Tag listEntry : list) { 201 | array.add(convertToJson(null, listEntry)); 202 | } 203 | return array; 204 | } else if (tag.getValue() instanceof Number) { 205 | final Number number = (Number) tag.getValue(); 206 | if (key != null && BOOLEAN_TYPES.contains(key)) { 207 | // Booleans don't have a direct representation in nbt 208 | return new JsonPrimitive(number.byteValue() != 0); 209 | } 210 | return new JsonPrimitive(number); 211 | } else if (tag instanceof StringTag) { 212 | return new JsonPrimitive(((StringTag) tag).getValue()); 213 | } else if (tag instanceof ByteArrayTag) { 214 | final ByteArrayTag arrayTag = (ByteArrayTag) tag; 215 | final JsonArray array = new JsonArray(); 216 | for (final byte num : arrayTag.getValue()) { 217 | array.add(num); 218 | } 219 | return array; 220 | } else if (tag instanceof IntArrayTag) { 221 | final IntArrayTag arrayTag = (IntArrayTag) tag; 222 | final JsonArray array = new JsonArray(); 223 | for (final int num : arrayTag.getValue()) { 224 | array.add(num); 225 | } 226 | return array; 227 | } else if (tag instanceof LongArrayTag) { 228 | final LongArrayTag arrayTag = (LongArrayTag) tag; 229 | final JsonArray array = new JsonArray(); 230 | for (final long num : arrayTag.getValue()) { 231 | array.add(num); 232 | } 233 | return array; 234 | } 235 | throw new IllegalArgumentException("Unhandled tag type " + tag.getClass().getSimpleName()); 236 | } 237 | 238 | private static void convertCompoundTagEntry(final String key, final Tag tag, final JsonObject object) { 239 | if ((key.equals("contents")) && tag instanceof CompoundTag) { 240 | // Back to a UUID string 241 | final CompoundTag showEntity = (CompoundTag) tag; 242 | final Tag idTag = showEntity.get("id"); 243 | if (idTag instanceof IntArrayTag) { 244 | showEntity.remove("id"); 245 | 246 | final JsonObject convertedElement = (JsonObject) convertToJson(key, tag); 247 | final UUID uuid = fromIntArray(((IntArrayTag) idTag).getValue()); 248 | convertedElement.addProperty("id", uuid.toString()); 249 | object.add(key, convertedElement); 250 | return; 251 | } 252 | } 253 | 254 | // "":1 is a valid tag, but not a valid json component 255 | object.add(key.isEmpty() ? "text" : key, convertToJson(key, tag)); 256 | } 257 | 258 | private static void removeComponentType(final JsonObject object) { 259 | final JsonElement type = object.remove("type"); 260 | if (type == null || !type.isJsonPrimitive()) { 261 | return; 262 | } 263 | 264 | // Remove the other fields 265 | final String typeString = type.getAsString(); 266 | for (final Pair pair : COMPONENT_TYPES) { 267 | if (!pair.key.equals(typeString)) { 268 | object.remove(pair.value); 269 | } 270 | } 271 | } 272 | 273 | // Last adopted from https://github.com/ViaVersion/ViaVersion/blob/8e38e25cbad1798abb628b4994f4047eaf64640d/common/src/main/java/com/viaversion/viaversion/util/UUIDUtil.java 274 | public static UUID fromIntArray(final int[] parts) { 275 | if (parts.length != 4) { 276 | return new UUID(0, 0); 277 | } 278 | return new UUID((long) parts[0] << 32 | (parts[1] & 0xFFFFFFFFL), (long) parts[2] << 32 | (parts[3] & 0xFFFFFFFFL)); 279 | } 280 | 281 | public static int[] toIntArray(final UUID uuid) { 282 | return toIntArray(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()); 283 | } 284 | 285 | public static int[] toIntArray(final long msb, final long lsb) { 286 | return new int[]{(int) (msb >> 32), (int) msb, (int) (lsb >> 32), (int) lsb}; 287 | } 288 | 289 | public static @Nullable UUID parseUUID(final String uuidString) { 290 | try { 291 | return UUID.fromString(uuidString); 292 | } catch (final IllegalArgumentException e) { 293 | return null; 294 | } 295 | } 296 | 297 | // Last adopted from https://github.com/ViaVersion/ViaNBT/commit/ad8ac024c48c2fc25e18dc689b3ca62602420ab9 298 | private static String stringValue(Tag tag) { 299 | if (tag instanceof ByteArrayTag) { 300 | return Arrays.toString(((ByteArrayTag) tag).getValue()); 301 | } else if (tag instanceof ByteTag) { 302 | return Byte.toString(((ByteTag) tag).asByte()); 303 | } else if (tag instanceof DoubleTag) { 304 | return Double.toString(((DoubleTag) tag).asDouble()); 305 | } else if (tag instanceof FloatTag) { 306 | return Float.toString(((FloatTag) tag).asFloat()); 307 | } else if (tag instanceof IntArrayTag) { 308 | return Arrays.toString(((IntArrayTag) tag).getValue()); 309 | } else if (tag instanceof IntTag) { 310 | return Integer.toString(((IntTag) tag).asInt()); 311 | } else if (tag instanceof LongArrayTag) { 312 | return Arrays.toString(((LongArrayTag) tag).getValue()); 313 | } else if (tag instanceof LongTag) { 314 | return Long.toString(((LongTag) tag).asLong()); 315 | } else if (tag instanceof ShortTag) { 316 | return Short.toString(((ShortTag) tag).asShort()); 317 | } else if (tag instanceof StringTag) { 318 | return ((StringTag) tag).getValue(); 319 | } else { 320 | return tag.getValue().toString(); 321 | } 322 | } 323 | 324 | // Implemented in the same way as ViaVersion's custom Pair class in order to reduce diff 325 | @AllArgsConstructor 326 | private static class Pair { 327 | private final K key; 328 | private final V value; 329 | } 330 | } -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/util/RandomString.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | import java.util.concurrent.ThreadLocalRandom; 6 | 7 | @UtilityClass 8 | public class RandomString { 9 | 10 | private final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 11 | 12 | public String generate(int length) { 13 | StringBuilder sb = new StringBuilder(length); 14 | ThreadLocalRandom random = ThreadLocalRandom.current(); 15 | 16 | for (int i = 0; i < length; i++) { 17 | sb.append(ALPHABET.charAt(random.nextInt(ALPHABET.length()))); 18 | } 19 | return sb.toString(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/util/Reflection.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.util; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.Field; 5 | import java.lang.reflect.Method; 6 | import java.util.Arrays; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | import org.bukkit.Bukkit; 11 | 12 | /** 13 | * An utility class that simplifies reflection in Bukkit plugins. 14 | * 15 | * @author Kristian 16 | */ 17 | public final class Reflection { 18 | /** 19 | * An interface for invoking a specific constructor. 20 | */ 21 | public interface ConstructorInvoker { 22 | /** 23 | * Invoke a constructor for a specific class. 24 | * 25 | * @param arguments - the arguments to pass to the constructor. 26 | * @return The constructed object. 27 | */ 28 | public Object invoke(Object... arguments); 29 | } 30 | 31 | /** 32 | * An interface for invoking a specific method. 33 | */ 34 | public interface MethodInvoker { 35 | /** 36 | * Invoke a method on a specific target object. 37 | * 38 | * @param target - the target object, or NULL for a static method. 39 | * @param arguments - the arguments to pass to the method. 40 | * @return The return value, or NULL if is void. 41 | */ 42 | public Object invoke(Object target, Object... arguments); 43 | 44 | public Method handle(); 45 | } 46 | 47 | /** 48 | * An interface for retrieving the field content. 49 | * 50 | * @param - field type. 51 | */ 52 | public interface FieldAccessor { 53 | /** 54 | * Retrieve the content of a field. 55 | * 56 | * @param target - the target object, or NULL for a static field. 57 | * @return The value of the field. 58 | */ 59 | public T get(Object target); 60 | 61 | /** 62 | * Set the content of a field. 63 | * 64 | * @param target - the target object, or NULL for a static field. 65 | * @param value - the new value of the field. 66 | */ 67 | public void set(Object target, Object value); 68 | 69 | /** 70 | * Determine if the given object has this field. 71 | * 72 | * @param target - the object to test. 73 | * @return TRUE if it does, FALSE otherwise. 74 | */ 75 | public boolean hasField(Object target); 76 | 77 | 78 | public Field handle(); 79 | } 80 | 81 | // Deduce the net.minecraft.server.v* package 82 | private static String OBC_PREFIX = Bukkit.getServer().getClass().getPackage().getName(); 83 | private static String NMS_PREFIX = OBC_PREFIX.replace("org.bukkit.craftbukkit", "net.minecraft.server"); 84 | private static String VERSION = OBC_PREFIX.replace("org.bukkit.craftbukkit", "").replace(".", ""); 85 | 86 | // Variable replacement 87 | private static Pattern MATCH_VARIABLE = Pattern.compile("\\{([^\\}]+)\\}"); 88 | 89 | private Reflection() { 90 | // Seal class 91 | } 92 | 93 | /** 94 | * Retrieve a field accessor for a specific field type and name. 95 | * 96 | * @param target - the target type. 97 | * @param name - the name of the field, or NULL to ignore. 98 | * @param fieldType - a compatible field type. 99 | * @return The field accessor. 100 | */ 101 | public static FieldAccessor getField(Class target, String name, Class fieldType) { 102 | return getField(target, name, fieldType, 0); 103 | } 104 | 105 | /** 106 | * Retrieve a field accessor for a specific field type and name. 107 | * 108 | * @param className - lookup name of the class, see {@link #getClass(String)}. 109 | * @param name - the name of the field, or NULL to ignore. 110 | * @param fieldType - a compatible field type. 111 | * @return The field accessor. 112 | */ 113 | public static FieldAccessor getField(String className, String name, Class fieldType) { 114 | return getField(getClass(className), name, fieldType, 0); 115 | } 116 | 117 | /** 118 | * Retrieve a field accessor for a specific field type and name. 119 | * 120 | * @param target - the target type. 121 | * @param fieldType - a compatible field type. 122 | * @param index - the number of compatible fields to skip. 123 | * @return The field accessor. 124 | */ 125 | public static FieldAccessor getField(Class target, Class fieldType, int index) { 126 | return getField(target, null, fieldType, index); 127 | } 128 | 129 | /** 130 | * Retrieve a field accessor for a specific field type and name. 131 | * 132 | * @param className - lookup name of the class, see {@link #getClass(String)}. 133 | * @param fieldType - a compatible field type. 134 | * @param index - the number of compatible fields to skip. 135 | * @return The field accessor. 136 | */ 137 | public static FieldAccessor getField(String className, Class fieldType, int index) { 138 | return getField(getClass(className), fieldType, index); 139 | } 140 | 141 | // Common method 142 | private static FieldAccessor getField(Class target, String name, Class fieldType, int index) { 143 | for (final Field field : target.getDeclaredFields()) { 144 | if ((name == null || field.getName().equals(name)) && fieldType.isAssignableFrom(field.getType()) && index-- <= 0) { 145 | field.setAccessible(true); 146 | 147 | // A function for retrieving a specific field value 148 | return new FieldAccessor() { 149 | 150 | @Override 151 | public Field handle() { 152 | return field; 153 | } 154 | 155 | @Override 156 | @SuppressWarnings("unchecked") 157 | public T get(Object target) { 158 | try { 159 | return (T) field.get(target); 160 | } catch (IllegalAccessException e) { 161 | throw new RuntimeException("Cannot access reflection.", e); 162 | } 163 | } 164 | 165 | @Override 166 | public void set(Object target, Object value) { 167 | try { 168 | field.set(target, value); 169 | } catch (IllegalAccessException e) { 170 | throw new RuntimeException("Cannot access reflection.", e); 171 | } 172 | } 173 | 174 | @Override 175 | public boolean hasField(Object target) { 176 | // target instanceof DeclaringClass 177 | return field.getDeclaringClass().isAssignableFrom(target.getClass()); 178 | } 179 | }; 180 | } 181 | } 182 | 183 | // Search in parent classes 184 | if (target.getSuperclass() != null) 185 | return getField(target.getSuperclass(), name, fieldType, index); 186 | 187 | throw new IllegalArgumentException("Cannot find field with type " + fieldType); 188 | } 189 | 190 | /** 191 | * Search for the first publicly and privately defined method of the given name and parameter count. 192 | * 193 | * @param className - lookup name of the class, see {@link #getClass(String)}. 194 | * @param methodName - the method name, or NULL to skip. 195 | * @param params - the expected parameters. 196 | * @return An object that invokes this specific method. 197 | * @throws IllegalStateException If we cannot find this method. 198 | */ 199 | public static MethodInvoker getMethod(String className, String methodName, Class... params) { 200 | return getTypedMethod(getClass(className), methodName, null, params); 201 | } 202 | 203 | /** 204 | * Search for the first publicly and privately defined method of the given name and parameter count. 205 | * 206 | * @param clazz - a class to start with. 207 | * @param methodName - the method name, or NULL to skip. 208 | * @param params - the expected parameters. 209 | * @return An object that invokes this specific method. 210 | * @throws IllegalStateException If we cannot find this method. 211 | */ 212 | public static MethodInvoker getMethod(Class clazz, String methodName, Class... params) { 213 | return getTypedMethod(clazz, methodName, null, params); 214 | } 215 | 216 | /** 217 | * Search for the first publicly and privately defined method of the given name and parameter count. 218 | * 219 | * @param clazz - a class to start with. 220 | * @param methodName - the method name, or NULL to skip. 221 | * @param returnType - the expected return type, or NULL to ignore. 222 | * @param params - the expected parameters. 223 | * @return An object that invokes this specific method. 224 | * @throws IllegalStateException If we cannot find this method. 225 | */ 226 | public static MethodInvoker getTypedMethod(Class clazz, String methodName, Class returnType, Class... params) { 227 | for (final Method method : clazz.getDeclaredMethods()) { 228 | if ((methodName == null || method.getName().equals(methodName)) 229 | && (returnType == null || method.getReturnType().equals(returnType)) 230 | && Arrays.equals(method.getParameterTypes(), params)) { 231 | method.setAccessible(true); 232 | 233 | return new MethodInvoker() { 234 | 235 | @Override 236 | public Object invoke(Object target, Object... arguments) { 237 | try { 238 | return method.invoke(target, arguments); 239 | } catch (Exception e) { 240 | throw new RuntimeException("Cannot invoke method " + method, e); 241 | } 242 | } 243 | 244 | @Override 245 | public Method handle() { 246 | return method; 247 | } 248 | }; 249 | } 250 | } 251 | 252 | // Search in every superclass 253 | if (clazz.getSuperclass() != null) 254 | return getMethod(clazz.getSuperclass(), methodName, params); 255 | 256 | throw new IllegalStateException(String.format("Unable to find method %s (%s).", methodName, Arrays.asList(params))); 257 | } 258 | 259 | /** 260 | * Search for the first publically and privately defined constructor of the given name and parameter count. 261 | * 262 | * @param className - lookup name of the class, see {@link #getClass(String)}. 263 | * @param params - the expected parameters. 264 | * @return An object that invokes this constructor. 265 | * @throws IllegalStateException If we cannot find this method. 266 | */ 267 | public static ConstructorInvoker getConstructor(String className, Class... params) { 268 | return getConstructor(getClass(className), params); 269 | } 270 | 271 | /** 272 | * Search for the first publically and privately defined constructor of the given name and parameter count. 273 | * 274 | * @param clazz - a class to start with. 275 | * @param params - the expected parameters. 276 | * @return An object that invokes this constructor. 277 | * @throws IllegalStateException If we cannot find this method. 278 | */ 279 | public static ConstructorInvoker getConstructor(Class clazz, Class... params) { 280 | for (final Constructor constructor : clazz.getDeclaredConstructors()) { 281 | if (Arrays.equals(constructor.getParameterTypes(), params)) { 282 | constructor.setAccessible(true); 283 | 284 | return new ConstructorInvoker() { 285 | 286 | @Override 287 | public Object invoke(Object... arguments) { 288 | try { 289 | return constructor.newInstance(arguments); 290 | } catch (Exception e) { 291 | throw new RuntimeException("Cannot invoke constructor " + constructor, e); 292 | } 293 | } 294 | 295 | }; 296 | } 297 | } 298 | 299 | throw new IllegalStateException(String.format("Unable to find constructor for %s (%s).", clazz, Arrays.asList(params))); 300 | } 301 | 302 | /** 303 | * Retrieve a class from its full name, without knowing its type on compile time. 304 | *

305 | * This is useful when looking up fields by a NMS or OBC type. 306 | *

307 | * 308 | * @param lookupName - the class name with variables. 309 | * @return The class. 310 | * @see {@link #getClass()} for more information. 311 | */ 312 | public static Class getUntypedClass(String lookupName) { 313 | @SuppressWarnings({"rawtypes", "unchecked"}) 314 | Class clazz = (Class) getClass(lookupName); 315 | return clazz; 316 | } 317 | 318 | /** 319 | * Retrieve a class from its full name. 320 | *

321 | * Strings enclosed with curly brackets - such as {TEXT} - will be replaced according to the following table: 322 | *

323 | * 324 | * 325 | * 326 | * 327 | * 328 | * 329 | * 330 | * 331 | * 332 | * 333 | * 334 | * 335 | * 336 | * 337 | * 338 | * 339 | * 340 | *
VariableContent
{nms}Actual package name of net.minecraft.server.VERSION
{obc}Actual pacakge name of org.bukkit.craftbukkit.VERSION
{version}The current Minecraft package VERSION, if any.
341 | * 342 | * @param lookupName - the class name with variables. 343 | * @return The looked up class. 344 | * @throws IllegalArgumentException If a variable or class could not be found. 345 | */ 346 | public static Class getClass(String lookupName) { 347 | return getCanonicalClass(expandVariables(lookupName)); 348 | } 349 | 350 | public static Class getClass(String... aliases) { 351 | for (String alias : aliases) { 352 | try { 353 | return getClass(alias); 354 | } catch (IllegalArgumentException e) { 355 | // Ignore 356 | } 357 | } 358 | throw new IllegalArgumentException("Cannot find class " + Arrays.toString(aliases)); 359 | } 360 | 361 | /** 362 | * Retrieve a class in the net.minecraft.server.VERSION.* package. 363 | * 364 | * @param name - the name of the class, excluding the package. 365 | * @throws IllegalArgumentException If the class doesn't exist. 366 | */ 367 | public static Class getMinecraftClass(String name) { 368 | return getCanonicalClass(NMS_PREFIX + "." + name); 369 | } 370 | 371 | /** 372 | * Retrieve a class in the org.bukkit.craftbukkit.VERSION.* package. 373 | * 374 | * @param name - the name of the class, excluding the package. 375 | * @throws IllegalArgumentException If the class doesn't exist. 376 | */ 377 | public static Class getCraftBukkitClass(String name) { 378 | return getCanonicalClass(OBC_PREFIX + "." + name); 379 | } 380 | 381 | /** 382 | * Retrieve a class by its canonical name. 383 | * 384 | * @param canonicalName - the canonical name. 385 | * @return The class. 386 | */ 387 | private static Class getCanonicalClass(String canonicalName) { 388 | try { 389 | return Class.forName(canonicalName); 390 | } catch (ClassNotFoundException e) { 391 | throw new IllegalArgumentException("Cannot find " + canonicalName, e); 392 | } 393 | } 394 | 395 | /** 396 | * Expand variables such as "{nms}" and "{obc}" to their corresponding packages. 397 | * 398 | * @param name - the full name of the class. 399 | * @return The expanded string. 400 | */ 401 | private static String expandVariables(String name) { 402 | StringBuffer output = new StringBuffer(); 403 | Matcher matcher = MATCH_VARIABLE.matcher(name); 404 | 405 | while (matcher.find()) { 406 | String variable = matcher.group(1); 407 | String replacement; 408 | 409 | // Expand all detected variables 410 | if ("nms".equalsIgnoreCase(variable)) 411 | replacement = NMS_PREFIX; 412 | else if ("obc".equalsIgnoreCase(variable)) 413 | replacement = OBC_PREFIX; 414 | else if ("version".equalsIgnoreCase(variable)) 415 | replacement = VERSION; 416 | else 417 | throw new IllegalArgumentException("Unknown variable: " + variable); 418 | 419 | // Assume the expanded variables are all packages, and append a dot 420 | if (replacement.length() > 0 && matcher.end() < name.length() && name.charAt(matcher.end()) != '.') 421 | replacement += "."; 422 | matcher.appendReplacement(output, Matcher.quoteReplacement(replacement)); 423 | } 424 | 425 | matcher.appendTail(output); 426 | return output.toString(); 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/util/buffer/ByteBufNetOutput.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.util.buffer; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonParser; 5 | import com.viaversion.nbt.io.NBTIO; 6 | import com.viaversion.nbt.tag.Tag; 7 | import io.netty.buffer.ByteBuf; 8 | import lombok.SneakyThrows; 9 | import me.catcoder.sidebar.util.NbtComponentSerializer; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | import java.io.DataOutputStream; 13 | import java.io.OutputStream; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.UUID; 16 | 17 | /** 18 | * A NetOutput implementation using a ByteBuf as a backend. 19 | */ 20 | public class ByteBufNetOutput implements NetOutput { 21 | private ByteBuf buf; 22 | 23 | public ByteBufNetOutput(ByteBuf buf) { 24 | this.buf = buf; 25 | } 26 | 27 | @Override 28 | public void writeBoolean(boolean b) { 29 | this.buf.writeBoolean(b); 30 | } 31 | 32 | @Override 33 | public void writeComponent(String json) { 34 | JsonElement jsonElement = JsonParser.parseString(json); 35 | Tag tag = NbtComponentSerializer.jsonComponentToTag(jsonElement); 36 | 37 | writeAnyTag(tag, false); 38 | } 39 | 40 | @SneakyThrows 41 | @Override 42 | public void writeAnyTag(@Nullable T tag, boolean named) { 43 | NBTIO.writeTag(new DataOutputStream(new OutputStream() { 44 | @Override 45 | public void write(int b) { 46 | buf.writeByte(b); 47 | } 48 | }), tag, named); 49 | } 50 | 51 | @Override 52 | public void writeByte(int b) { 53 | this.buf.writeByte(b); 54 | } 55 | 56 | @Override 57 | public void writeShort(int s) { 58 | this.buf.writeShort(s); 59 | } 60 | 61 | @Override 62 | public void writeChar(int c) { 63 | this.buf.writeChar(c); 64 | } 65 | 66 | @Override 67 | public void writeInt(int i) { 68 | this.buf.writeInt(i); 69 | } 70 | 71 | @Override 72 | public void writeVarInt(int i) { 73 | while ((i & ~0x7F) != 0) { 74 | this.writeByte((i & 0x7F) | 0x80); 75 | i >>>= 7; 76 | } 77 | 78 | this.writeByte(i); 79 | } 80 | 81 | @Override 82 | public void writeLong(long l) { 83 | this.buf.writeLong(l); 84 | } 85 | 86 | @Override 87 | public void writeVarLong(long l) { 88 | while ((l & ~0x7F) != 0) { 89 | this.writeByte((int) (l & 0x7F) | 0x80); 90 | l >>>= 7; 91 | } 92 | 93 | this.writeByte((int) l); 94 | } 95 | 96 | @Override 97 | public void writeFloat(float f) { 98 | this.buf.writeFloat(f); 99 | } 100 | 101 | @Override 102 | public void writeDouble(double d) { 103 | this.buf.writeDouble(d); 104 | } 105 | 106 | @Override 107 | public void writeBytes(byte b[]) { 108 | this.buf.writeBytes(b); 109 | } 110 | 111 | @Override 112 | public void writeBytes(byte b[], int length) { 113 | this.buf.writeBytes(b, 0, length); 114 | } 115 | 116 | @Override 117 | public void writeShorts(short[] s) { 118 | this.writeShorts(s, s.length); 119 | } 120 | 121 | @Override 122 | public void writeShorts(short[] s, int length) { 123 | for (int index = 0; index < length; index++) { 124 | this.writeShort(s[index]); 125 | } 126 | } 127 | 128 | @Override 129 | public void writeInts(int[] i) { 130 | this.writeInts(i, i.length); 131 | } 132 | 133 | @Override 134 | public void writeInts(int[] i, int length) { 135 | for (int index = 0; index < length; index++) { 136 | this.writeInt(i[index]); 137 | } 138 | } 139 | 140 | @Override 141 | public void writeLongs(long[] l) { 142 | this.writeLongs(l, l.length); 143 | } 144 | 145 | @Override 146 | public void writeLongs(long[] l, int length) { 147 | for (int index = 0; index < length; index++) { 148 | this.writeLong(l[index]); 149 | } 150 | } 151 | 152 | @Override 153 | public byte[] toByteArray() { 154 | byte[] bytes = new byte[this.buf.readableBytes()]; 155 | this.buf.readBytes(bytes); 156 | return bytes; 157 | } 158 | 159 | @Override 160 | public void writeString(String s) { 161 | if (s == null) { 162 | throw new IllegalArgumentException("String cannot be null!"); 163 | } 164 | 165 | byte[] bytes = s.getBytes(StandardCharsets.UTF_8); 166 | if (bytes.length > 32767) { 167 | throw new RuntimeException("String too big (was " + s.length() + " bytes encoded, max " + 32767 + ")"); 168 | } else { 169 | this.writeVarInt(bytes.length); 170 | this.writeBytes(bytes); 171 | } 172 | } 173 | 174 | @Override 175 | public void writeUUID(UUID uuid) { 176 | this.writeLong(uuid.getMostSignificantBits()); 177 | this.writeLong(uuid.getLeastSignificantBits()); 178 | } 179 | 180 | @Override 181 | public void flush() { 182 | } 183 | } -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/util/buffer/NetOutput.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.util.buffer; 2 | 3 | import com.viaversion.nbt.tag.Tag; 4 | 5 | import javax.annotation.Nullable; 6 | import java.util.UUID; 7 | 8 | /** 9 | * An interface for writing network data. 10 | */ 11 | public interface NetOutput { 12 | /** 13 | * Writes a boolean. 14 | * 15 | * @param b Boolean to write. 16 | */ 17 | public void writeBoolean(boolean b); 18 | 19 | public void writeAnyTag(@Nullable T tag, boolean named); 20 | 21 | public void writeComponent(String json); 22 | 23 | /** 24 | * Writes a byte. 25 | * 26 | * @param b Byte to write. 27 | */ 28 | public void writeByte(int b); 29 | 30 | /** 31 | * Writes a short. 32 | * 33 | * @param s Short to write. 34 | */ 35 | public void writeShort(int s); 36 | 37 | /** 38 | * Writes a char. 39 | * 40 | * @param c Char to write. 41 | */ 42 | public void writeChar(int c); 43 | 44 | /** 45 | * Writes a integer. 46 | * 47 | * @param i Integer to write. 48 | */ 49 | public void writeInt(int i); 50 | 51 | /** 52 | * Writes a varint. A varint is a form of integer where only necessary bytes are written. This is done to save bandwidth. 53 | * 54 | * @param i Varint to write. 55 | */ 56 | public void writeVarInt(int i); 57 | 58 | /** 59 | * Writes a long. 60 | * 61 | * @param l Long to write. 62 | */ 63 | public void writeLong(long l); 64 | 65 | /** 66 | * Writes a varlong. A varlong is a form of long where only necessary bytes are written. This is done to save bandwidth. 67 | * 68 | * @param l Varlong to write. 69 | */ 70 | public void writeVarLong(long l); 71 | 72 | /** 73 | * Writes a float. 74 | * 75 | * @param f Float to write. 76 | */ 77 | public void writeFloat(float f); 78 | 79 | /** 80 | * Writes a double. 81 | * 82 | * @param d Double to write. 83 | */ 84 | public void writeDouble(double d); 85 | 86 | /** 87 | * Writes a byte array. 88 | * 89 | * @param b Byte array to write. 90 | */ 91 | public void writeBytes(byte b[]); 92 | 93 | /** 94 | * Writes a byte array, using the given amount of bytes. 95 | * 96 | * @param b Byte array to write. 97 | * @param length Bytes to write. 98 | */ 99 | public void writeBytes(byte b[], int length); 100 | 101 | /** 102 | * Writes a short array. 103 | * 104 | * @param s Short array to write. 105 | */ 106 | public void writeShorts(short s[]); 107 | 108 | /** 109 | * Writes a short array, using the given amount of bytes. 110 | * 111 | * @param s Short array to write. 112 | * @param length Shorts to write. 113 | */ 114 | public void writeShorts(short s[], int length); 115 | 116 | /** 117 | * Writes an int array. 118 | * 119 | * @param i Int array to write. 120 | */ 121 | public void writeInts(int i[]); 122 | 123 | /** 124 | * Writes an int array, using the given amount of bytes. 125 | * 126 | * @param i Int array to write. 127 | * @param length Ints to write. 128 | */ 129 | public void writeInts(int i[], int length); 130 | 131 | /** 132 | * Writes a long array. 133 | * 134 | * @param l Long array to write. 135 | */ 136 | public void writeLongs(long l[]); 137 | 138 | /** 139 | * Writes a long array, using the given amount of bytes. 140 | * 141 | * @param l Long array to write. 142 | * @param length Longs to write. 143 | */ 144 | public void writeLongs(long l[], int length); 145 | 146 | /** 147 | * Writes a string. 148 | * 149 | * @param s String to write. 150 | */ 151 | public void writeString(String s); 152 | 153 | /** 154 | * Writes a UUID. 155 | * 156 | * @param uuid UUID to write. 157 | */ 158 | public void writeUUID(UUID uuid); 159 | 160 | /** 161 | * Flushes the output. 162 | * 163 | */ 164 | public void flush(); 165 | 166 | /** 167 | * Creates a new byte array with current data. 168 | * 169 | * @return New byte array. 170 | */ 171 | public byte[] toByteArray(); 172 | } -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/util/lang/ThrowingConsumer.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.util.lang; 2 | 3 | @FunctionalInterface 4 | public interface ThrowingConsumer { 5 | void accept(T t) throws E; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/util/lang/ThrowingFunction.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.util.lang; 2 | 3 | @FunctionalInterface 4 | public interface ThrowingFunction { 5 | R apply(T t) throws E; 6 | 7 | default ThrowingFunction compose(ThrowingFunction before) { 8 | return (V v) -> apply(before.apply(v)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/util/lang/ThrowingPredicate.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.util.lang; 2 | 3 | @FunctionalInterface 4 | public interface ThrowingPredicate { 5 | 6 | boolean test(T t) throws E; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/util/lang/ThrowingSupplier.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.util.lang; 2 | 3 | @FunctionalInterface 4 | public interface ThrowingSupplier { 5 | 6 | T get() throws E; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/util/version/MinecraftProtocolVersion.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.util.version; 2 | 3 | import me.catcoder.sidebar.protocol.ProtocolConstants; 4 | 5 | import java.util.Map.Entry; 6 | import java.util.NavigableMap; 7 | import java.util.TreeMap; 8 | 9 | /** 10 | * A lookup of the associated protocol version of a given Minecraft server. 11 | * 12 | * @author Kristian 13 | */ 14 | public final class MinecraftProtocolVersion { 15 | 16 | private static final NavigableMap LOOKUP = createLookup(); 17 | 18 | private static NavigableMap createLookup() { 19 | TreeMap map = new TreeMap<>(); 20 | 21 | // Source: http://wiki.vg/Protocol_version_numbers 22 | // Doesn't include pre-releases 23 | 24 | map.put(new MinecraftVersion(1, 12, 2), ProtocolConstants.MINECRAFT_1_12_2); 25 | 26 | map.put(new MinecraftVersion(1, 13, 0), ProtocolConstants.MINECRAFT_1_13); 27 | map.put(new MinecraftVersion(1, 13, 1), ProtocolConstants.MINECRAFT_1_13_1); 28 | map.put(new MinecraftVersion(1, 13, 2), ProtocolConstants.MINECRAFT_1_13_2); 29 | 30 | map.put(new MinecraftVersion(1, 14, 0), ProtocolConstants.MINECRAFT_1_14); 31 | map.put(new MinecraftVersion(1, 14, 1), ProtocolConstants.MINECRAFT_1_14_1); 32 | map.put(new MinecraftVersion(1, 14, 2), ProtocolConstants.MINECRAFT_1_14_2); 33 | map.put(new MinecraftVersion(1, 14, 3), ProtocolConstants.MINECRAFT_1_14_3); 34 | map.put(new MinecraftVersion(1, 14, 4), ProtocolConstants.MINECRAFT_1_14_4); 35 | 36 | map.put(new MinecraftVersion(1, 15, 0), ProtocolConstants.MINECRAFT_1_15); 37 | map.put(new MinecraftVersion(1, 15, 1), ProtocolConstants.MINECRAFT_1_15_1); 38 | map.put(new MinecraftVersion(1, 15, 2), ProtocolConstants.MINECRAFT_1_15_2); 39 | 40 | map.put(new MinecraftVersion(1, 16, 0), ProtocolConstants.MINECRAFT_1_16); 41 | map.put(new MinecraftVersion(1, 16, 1), ProtocolConstants.MINECRAFT_1_16_1); 42 | map.put(new MinecraftVersion(1, 16, 2), ProtocolConstants.MINECRAFT_1_16_2); 43 | map.put(new MinecraftVersion(1, 16, 3), ProtocolConstants.MINECRAFT_1_16_3); 44 | map.put(new MinecraftVersion(1, 16, 4), ProtocolConstants.MINECRAFT_1_16_4); 45 | map.put(new MinecraftVersion(1, 16, 5), ProtocolConstants.MINECRAFT_1_16_5); 46 | 47 | map.put(new MinecraftVersion(1, 17, 0), ProtocolConstants.MINECRAFT_1_17); 48 | map.put(new MinecraftVersion(1, 17, 1), ProtocolConstants.MINECRAFT_1_17_1); 49 | 50 | map.put(new MinecraftVersion(1, 18, 0), ProtocolConstants.MINECRAFT_1_18); 51 | map.put(new MinecraftVersion(1, 18, 1), ProtocolConstants.MINECRAFT_1_18_1); 52 | map.put(new MinecraftVersion(1, 18, 2), ProtocolConstants.MINECRAFT_1_18_2); 53 | 54 | map.put(new MinecraftVersion(1, 19, 0), ProtocolConstants.MINECRAFT_1_19); 55 | map.put(new MinecraftVersion(1, 19, 2), ProtocolConstants.MINECRAFT_1_19_2); 56 | map.put(new MinecraftVersion(1, 19, 3), ProtocolConstants.MINECRAFT_1_19_3); 57 | map.put(new MinecraftVersion(1, 19, 4), ProtocolConstants.MINECRAFT_1_19_4); 58 | 59 | map.put(new MinecraftVersion(1, 20, 0), ProtocolConstants.MINECRAFT_1_20); 60 | 61 | map.put(new MinecraftVersion(1, 20, 1), ProtocolConstants.MINECRAFT_1_20_1); 62 | 63 | map.put(new MinecraftVersion(1, 20, 2), ProtocolConstants.MINECRAFT_1_20_2); 64 | map.put(new MinecraftVersion(1, 20, 3), ProtocolConstants.MINECRAFT_1_20_3); 65 | map.put(new MinecraftVersion(1, 20, 4), ProtocolConstants.MINECRAFT_1_20_4); 66 | map.put(new MinecraftVersion(1, 20, 5), ProtocolConstants.MINECRAFT_1_20_5); 67 | map.put(new MinecraftVersion(1, 20, 6), ProtocolConstants.MINECRAFT_1_20_6); 68 | 69 | map.put(new MinecraftVersion(1, 21, 0), ProtocolConstants.MINECRAFT_1_21); 70 | map.put(new MinecraftVersion(1, 21, 1), ProtocolConstants.MINECRAFT_1_21); 71 | map.put(new MinecraftVersion(1, 21, 2), ProtocolConstants.MINECRAFT_1_21_2); 72 | 73 | 74 | return map; 75 | } 76 | 77 | /** 78 | * Retrieve the version of the Minecraft protocol for the current version of Minecraft. 79 | * 80 | * @return The version number. 81 | */ 82 | public static int getCurrentVersion() { 83 | return getVersion(MinecraftVersion.getCurrentVersion()); 84 | } 85 | 86 | /** 87 | * Retrieve the version of the Minecraft protocol for this version of Minecraft. 88 | * 89 | * @param version - the version. 90 | * @return The version number. 91 | */ 92 | public static int getVersion(MinecraftVersion version) { 93 | Entry result = LOOKUP.floorEntry(version); 94 | return result != null ? result.getValue() : Integer.MIN_VALUE; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/util/version/MinecraftVersion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. 3 | * Copyright (C) 2012 Kristian S. Stangeland 4 | * 5 | * This program is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU General Public License as published by the Free Software Foundation; either version 2 of 7 | * the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 10 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 11 | * See the GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU General Public License along with this program; 14 | * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 15 | * 02111-1307 USA 16 | */ 17 | 18 | package me.catcoder.sidebar.util.version; 19 | 20 | import com.google.common.collect.ComparisonChain; 21 | import com.google.common.collect.Ordering; 22 | import org.bukkit.Bukkit; 23 | 24 | import java.io.Serializable; 25 | import java.util.Objects; 26 | import java.util.regex.Matcher; 27 | import java.util.regex.Pattern; 28 | 29 | /** 30 | * Determine the current Minecraft version. 31 | * 32 | * @author Kristian 33 | */ 34 | public final class MinecraftVersion implements Comparable, Serializable { 35 | 36 | /** 37 | * Regular expression used to parse version strings. 38 | */ 39 | private static final Pattern VERSION_PATTERN = Pattern.compile(".*\\(.*MC.\\s*([a-zA-z0-9\\-.]+).*"); 40 | 41 | /** 42 | * The current version of minecraft, lazy initialized by MinecraftVersion.currentVersion() 43 | */ 44 | private static MinecraftVersion currentVersion; 45 | 46 | private final int major; 47 | private final int minor; 48 | private final int build; 49 | // The development stage 50 | private final String development; 51 | 52 | private volatile Boolean atCurrentOrAbove; 53 | 54 | /** 55 | * Construct a version format from the standard release version or the snapshot verison. 56 | * 57 | * @param versionOnly - the version. 58 | */ 59 | private MinecraftVersion(String versionOnly) { 60 | String[] section = versionOnly.split("-"); 61 | int[] numbers; 62 | 63 | numbers = this.parseVersion(section[0]); 64 | 65 | this.major = numbers[0]; 66 | this.minor = numbers[1]; 67 | this.build = numbers[2]; 68 | this.development = section.length > 1 ? section[1] : null; 69 | } 70 | 71 | /** 72 | * Construct a version object directly. 73 | * 74 | * @param major - major version number. 75 | * @param minor - minor version number. 76 | * @param build - build version number. 77 | */ 78 | public MinecraftVersion(int major, int minor, int build) { 79 | this(major, minor, build, null); 80 | } 81 | 82 | /** 83 | * Construct a version object directly. 84 | * 85 | * @param major - major version number. 86 | * @param minor - minor version number. 87 | * @param build - build version number. 88 | * @param development - development stage. 89 | */ 90 | public MinecraftVersion(int major, int minor, int build, String development) { 91 | this.major = major; 92 | this.minor = minor; 93 | this.build = build; 94 | this.development = development; 95 | } 96 | 97 | /** 98 | * Extract the Minecraft version from CraftBukkit itself. 99 | * 100 | * @param text - the server version in text form. 101 | * @return The underlying MC version. 102 | * @throws IllegalStateException If we could not parse the version string. 103 | */ 104 | public static String extractVersion(String text) { 105 | Matcher version = VERSION_PATTERN.matcher(text); 106 | 107 | if (version.matches() && version.group(1) != null) { 108 | return version.group(1); 109 | } else { 110 | throw new IllegalStateException("Cannot parse version String '" + text + "'"); 111 | } 112 | } 113 | 114 | /** 115 | * Parse the given server version into a Minecraft version. 116 | * 117 | * @param serverVersion - the server version. 118 | * @return The resulting Minecraft version. 119 | */ 120 | public static MinecraftVersion fromServerVersion(String serverVersion) { 121 | return new MinecraftVersion(extractVersion(serverVersion)); 122 | } 123 | 124 | public static MinecraftVersion getCurrentVersion() { 125 | if (currentVersion == null) { 126 | currentVersion = fromServerVersion(Bukkit.getVersion()); 127 | } 128 | 129 | return currentVersion; 130 | } 131 | 132 | private static boolean atOrAbove(MinecraftVersion version) { 133 | return getCurrentVersion().isAtLeast(version); 134 | } 135 | 136 | private int[] parseVersion(String version) { 137 | String[] elements = version.split("\\."); 138 | int[] numbers = new int[3]; 139 | 140 | // Make sure it's even a valid version 141 | if (elements.length < 1) { 142 | throw new IllegalStateException("Corrupt MC version: " + version); 143 | } 144 | 145 | // The String 1 or 1.2 is interpreted as 1.0.0 and 1.2.0 respectively. 146 | for (int i = 0; i < Math.min(numbers.length, elements.length); i++) { 147 | numbers[i] = Integer.parseInt(elements[i].trim()); 148 | } 149 | return numbers; 150 | } 151 | 152 | /** 153 | * Major version number 154 | * 155 | * @return Current major version number. 156 | */ 157 | public int getMajor() { 158 | return this.major; 159 | } 160 | 161 | /** 162 | * Minor version number 163 | * 164 | * @return Current minor version number. 165 | */ 166 | public int getMinor() { 167 | return this.minor; 168 | } 169 | 170 | /** 171 | * Build version number 172 | * 173 | * @return Current build version number. 174 | */ 175 | public int getBuild() { 176 | return this.build; 177 | } 178 | 179 | /** 180 | * Retrieve the development stage. 181 | * 182 | * @return Development stage, or NULL if this is a release. 183 | */ 184 | public String getDevelopmentStage() { 185 | return this.development; 186 | } 187 | 188 | /** 189 | * Checks if this version is at or above the current version the server is running. 190 | * 191 | * @return true if this version is equal or newer than the server version, false otherwise. 192 | */ 193 | public boolean atOrAbove() { 194 | if (this.atCurrentOrAbove == null) { 195 | this.atCurrentOrAbove = atOrAbove(this); 196 | } 197 | 198 | return this.atCurrentOrAbove; 199 | } 200 | 201 | /** 202 | * Retrieve the version String (major.minor.build) only. 203 | * 204 | * @return A normal version string. 205 | */ 206 | public String getVersion() { 207 | if (this.getDevelopmentStage() == null) { 208 | return String.format("%s.%s.%s", this.getMajor(), this.getMinor(), this.getBuild()); 209 | } else { 210 | return String.format("%s.%s.%s-%s%s", this.getMajor(), this.getMinor(), this.getBuild(), 211 | this.getDevelopmentStage(), ""); 212 | } 213 | } 214 | 215 | @Override 216 | public int compareTo(MinecraftVersion o) { 217 | if (o == null) { 218 | return 1; 219 | } 220 | 221 | return ComparisonChain.start() 222 | .compare(this.getMajor(), o.getMajor()) 223 | .compare(this.getMinor(), o.getMinor()) 224 | .compare(this.getBuild(), o.getBuild()) 225 | .compare(this.getDevelopmentStage(), o.getDevelopmentStage(), Ordering.natural().nullsLast()) 226 | .result(); 227 | } 228 | 229 | public boolean isAtLeast(MinecraftVersion other) { 230 | if (other == null) { 231 | return false; 232 | } 233 | 234 | return this.compareTo(other) >= 0; 235 | } 236 | 237 | @Override 238 | public boolean equals(Object obj) { 239 | if (obj == null) { 240 | return false; 241 | } 242 | if (obj == this) { 243 | return true; 244 | } 245 | 246 | if (obj instanceof MinecraftVersion other) { 247 | return this.getMajor() == other.getMajor() && 248 | this.getMinor() == other.getMinor() && 249 | this.getBuild() == other.getBuild() && 250 | Objects.equals(this.getDevelopmentStage(), other.getDevelopmentStage()); 251 | } 252 | 253 | return false; 254 | } 255 | 256 | @Override 257 | public int hashCode() { 258 | return Objects.hash(this.getMajor(), this.getMinor(), this.getBuild()); 259 | } 260 | 261 | @Override 262 | public String toString() { 263 | // Convert to a String that we can parse back again 264 | return String.format("(MC: %s)", this.getVersion()); 265 | } 266 | } -------------------------------------------------------------------------------- /src/main/java/me/catcoder/sidebar/util/version/VersionUtil.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar.util.version; 2 | 3 | import com.viaversion.viaversion.ViaVersionPlugin; 4 | import lombok.NonNull; 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.plugin.java.JavaPlugin; 7 | 8 | import java.util.UUID; 9 | 10 | public final class VersionUtil { 11 | 12 | public static final int SERVER_VERSION = MinecraftProtocolVersion.getCurrentVersion(); 13 | 14 | static { 15 | Bukkit.getLogger().info("[ProtocolSidebar] Server version: " 16 | + MinecraftVersion.getCurrentVersion() + " (protocol " + SERVER_VERSION + ")"); 17 | Bukkit.getLogger().info("[ProtocolSidebar] Please report any bugs to the developer: https://github.com/CatCoderr/ProtocolSidebar/issues"); 18 | } 19 | 20 | public static int getPlayerVersion(@NonNull UUID id) { 21 | boolean isVia = Bukkit.getPluginManager().isPluginEnabled("ViaVersion"); 22 | return isVia ? JavaPlugin.getPlugin(ViaVersionPlugin.class).getApi().getPlayerProtocolVersion(id).getVersion() : SERVER_VERSION; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/me/catcoder/sidebar/PacketIdsTest.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar; 2 | 3 | import me.catcoder.sidebar.protocol.PacketIds; 4 | import me.catcoder.sidebar.protocol.ProtocolConstants; 5 | import org.junit.Test; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | 9 | public class PacketIdsTest { 10 | 11 | 12 | @Test 13 | public void testPacketIds() { 14 | assertEquals(0x44, PacketIds.UPDATE_TEAMS.getPacketId(ProtocolConstants.MINECRAFT_1_12_2)); 15 | assertEquals(0x47, PacketIds.UPDATE_TEAMS.getPacketId(ProtocolConstants.MINECRAFT_1_13)); 16 | assertEquals(0x47, PacketIds.UPDATE_TEAMS.getPacketId(ProtocolConstants.MINECRAFT_1_13_2)); 17 | assertEquals(0x47, PacketIds.UPDATE_TEAMS.getPacketId(ProtocolConstants.MINECRAFT_1_13_1)); 18 | 19 | assertEquals(0x4B, PacketIds.UPDATE_TEAMS.getPacketId(ProtocolConstants.MINECRAFT_1_14)); 20 | assertEquals(0x4B, PacketIds.UPDATE_TEAMS.getPacketId(ProtocolConstants.MINECRAFT_1_14_1)); 21 | assertEquals(0x4B, PacketIds.UPDATE_TEAMS.getPacketId(ProtocolConstants.MINECRAFT_1_14_2)); 22 | assertEquals(0x4B, PacketIds.UPDATE_TEAMS.getPacketId(ProtocolConstants.MINECRAFT_1_14_3)); 23 | assertEquals(0x4B, PacketIds.UPDATE_TEAMS.getPacketId(ProtocolConstants.MINECRAFT_1_14_4)); 24 | 25 | 26 | assertEquals(0x56, PacketIds.UPDATE_TEAMS.getPacketId(ProtocolConstants.MINECRAFT_1_19_3)); 27 | assertEquals(0x5C, PacketIds.UPDATE_TEAMS.getPacketId(ProtocolConstants.MINECRAFT_1_20_2)); 28 | } 29 | 30 | @Test(expected = IllegalArgumentException.class) 31 | public void testUnsupportedVersion() { 32 | PacketIds.UPDATE_TEAMS.getPacketId(47); // 1.8 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /standalone-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store -------------------------------------------------------------------------------- /standalone-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar 2 | 3 | plugins { 4 | id("java") 5 | id("com.github.johnrengelman.shadow") version "8.1.1" 6 | } 7 | 8 | dependencies { 9 | implementation(project(":")) 10 | } 11 | 12 | tasks.withType { 13 | archiveFileName.set("ProtocolSidebar-${rootProject.version}.jar") 14 | relocate("com.tcoded.folialib", "me.catcoder.protocolsidebar.lib.folialib") 15 | 16 | // create final jar in project root dir 17 | destinationDirectory.set(rootProject.rootDir.resolve("bin")) 18 | } 19 | 20 | tasks { 21 | withType { 22 | 23 | val tokens = mapOf( 24 | "projectDescription" to rootProject.description, 25 | "projectVersion" to rootProject.version 26 | ) 27 | 28 | filesMatching("*.yml") { 29 | expand(tokens) 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /standalone-plugin/src/main/java/me/catcoder/sidebar/ProtocolSidebarPlugin.java: -------------------------------------------------------------------------------- 1 | package me.catcoder.sidebar; 2 | 3 | import org.bukkit.plugin.java.JavaPlugin; 4 | 5 | public class ProtocolSidebarPlugin extends JavaPlugin { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /standalone-plugin/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ProtocolSidebar 2 | version: ${projectVersion} 3 | main: me.catcoder.sidebar.ProtocolSidebarPlugin 4 | author: CatCoder 5 | description: ${projectDescription} 6 | website: https://github.com/CatCoderr/ProtocolSidebar 7 | softdepend: [ViaVersion] 8 | 9 | api-version: 1.13 10 | folia-supported: true --------------------------------------------------------------------------------