├── .gitignore ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src └── main └── kotlin └── dev └── y4irr └── coir ├── ItemBuilder.kt ├── hotbar ├── HotbarActionType.kt ├── HotbarItem.kt ├── HotbarListener.kt ├── HotbarManager.kt └── HotbarSetup.kt ├── menu ├── Button.kt ├── Menu.kt ├── MenuListener.kt ├── MenuManager.kt └── impl │ ├── AsynchronousTask.kt │ ├── AutoUpdate.kt │ ├── CancelPlayerInventory.kt │ ├── MoveItemToPlayer.kt │ ├── TickUpdate.kt │ ├── UpdateAfterClick.kt │ └── UsePlayerInventory.kt └── scoreboard ├── Wai.kt ├── WaiAdapter.kt ├── annotations ├── BoardDescending.kt ├── BoardStartingNumber.kt ├── BoardStyle.kt ├── DebugMode.kt ├── SbOptions.kt ├── Tickable.kt └── Title.kt ├── board ├── Board.kt ├── BoardEntry.kt └── BoardUpdateThread.kt ├── enums └── BoardStyleType.kt ├── events ├── BoardChangeEvent.kt ├── BoardCreateEvent.kt └── BoardDestroyEvent.kt ├── listener ├── BoardEventLogger.kt └── BoardListener.kt └── utils └── AnnotationUtils.kt /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .gradle/** 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### IntelliJ IDEA ### 9 | .idea/** 10 | .idea/* 11 | .idea/modules.xml 12 | .idea/jarRepositories.xml 13 | .idea/compiler.xml 14 | .idea/libraries/ 15 | *.iws 16 | *.iml 17 | *.ipr 18 | out/ 19 | !**/src/main/**/out/ 20 | !**/src/test/**/out/ 21 | 22 | ### Kotlin ### 23 | .kotlin 24 | 25 | ### Eclipse ### 26 | .apt_generated 27 | .classpath 28 | .factorypath 29 | .project 30 | .settings 31 | .springBeans 32 | .sts4-cache 33 | bin/ 34 | !**/src/main/**/bin/ 35 | !**/src/test/**/bin/ 36 | 37 | ### NetBeans ### 38 | /nbproject/private/ 39 | /nbbuild/ 40 | /dist/ 41 | /nbdist/ 42 | /.nb-gradle/ 43 | 44 | libs 45 | lib 46 | .lib 47 | 48 | ### VS Code ### 49 | .vscode/ 50 | 51 | ### Mac OS ### 52 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoirLibrary 2 | 3 | **CoirLibrary** is a modular, *from-scratch* Kotlin library by [y4irr](https://github.com/y4irr), built to streamline Minecraft Java plugin development. It provides clean, extensible, and high-performance APIs for creating scoreboards, menus, hotbars, and (coming soon) commands. 4 | 5 | ## ✨ Features 6 | 7 | ### ⚔️ ScoreboardAPI 8 | A modern scoreboard system with full annotation support: 9 | - `@Title`, `@Tickable`, `@SbOptions`, `@BoardStyle`, and more 10 | - Score ordering (ascending, descending, or custom) 11 | - Async updates and optimized entry rendering 12 | - Up to 32-character lines with color-safe spacing 13 | 14 | ### 🧩 MenuAPI 15 | A powerful GUI framework for menus with deep customization: 16 | - Full annotation support: `@updateAfterClick`, `@CancelPlayerInventory`, `@UsePlayerInventory`, `@MoveItemToPlayer` 17 | - Visual inventory replacement and item protection 18 | - Smooth transitions between menus and player inventory integration 19 | - Automatic restoration of inventory and state on close 20 | 21 | ### 🧠 Hotbar System 22 | - Assign custom actions to LEFT, RIGHT, and MIDDLE click 23 | - Per-player cooldowns with automatic XP bar + level display 24 | - Optional `.blockUse()` disables default item behavior (like throwing pearls) but keeps executing code 25 | - Hotbar automatically reapplied on join, death, and respawn 26 | - Cooldown messages with per-player delay to prevent spam 27 | - Items are not dropped on death 28 | - Hotbar items can be dynamically updated or replaced at runtime 29 | 30 | ### 🧪 CommandAPI *(Coming Soon)* 31 | A fully annotated command system with: 32 | - Async command execution 33 | - Custom tab completion 34 | - Argument mapping and validation 35 | 36 | ## 🛠 Installation 37 | 38 | CoirLibrary is not yet published to a public repository. For now, add it manually as a local dependency: 39 | 40 | ```kotlin 41 | dependencies { 42 | implementation(files("libs/CoirLibrary.jar")) 43 | } 44 | ``` 45 | 46 | ## 🧪 Example Usages 47 | 48 | ### Scoreboard 49 | - Kotlin 50 | ```kotlin 51 | @SbOptions(isAsync = true) 52 | @Tickable(tick = 2L) 53 | @BoardStyle(type = BoardStyleType.ASCENDING) 54 | @Title("EMPTY") // IF THIS IS SET AS EMPTY STRING, THE METHOD getTitle() WILL WORK 55 | class ScoreAdapter : WaiAdapter { 56 | 57 | override fun getTitle(player: Player): String { 58 | return "&6&lSCOREBOARD" // IF @Title IS NOT ADDED THIS WILL 59 | } // WORK INSTEAD THE ANNOTATION 60 | 61 | override fun getLines(player: Player): List { 62 | return listOf( 63 | "&7&m--------------------------", 64 | "&eOnline:", 65 | "&f0", 66 | "", 67 | "&eRank:", 68 | "&aDefault", 69 | "", 70 | "&etesting.scoreboard!", 71 | "&7&m--------------------------") 72 | } 73 | } 74 | ``` 75 | - Java 76 | ```java 77 | @SbOptions(isAsync = true) 78 | @Tickable(tick = 2L) 79 | @BoardStyle(type = BoardStyleType.ASCENDING) 80 | @Title("EMPTY") // If this is set as "EMPTY", the method getTitle() will be used instead 81 | public class ScoreAdapter implements WaiAdapter { 82 | 83 | @Override 84 | public String getTitle(Player player) { 85 | return "&6&lSCOREBOARD"; // Only used if @Title is "EMPTY" 86 | } 87 | 88 | @Override 89 | public List getLines(Player player) { 90 | return Arrays.asList( 91 | "&7&m--------------------------", 92 | "&eOnline:", 93 | "&f0", 94 | "", 95 | "&eRank:", 96 | "&aDefault", 97 | "", 98 | "&etesting.scoreboard!", 99 | "&7&m--------------------------" 100 | ); 101 | } 102 | } 103 | ``` 104 | ### Menu 105 | - Kotlin 106 | ```kotlin 107 | @CancelPlayerInventory(false) 108 | @UpdateAfterClick(true) 109 | class SimpleMenu : Menu() { 110 | 111 | override fun getTitle(player: Player): String = "&bSimple Menu" 112 | 113 | override fun getButtons(player: Player): Map { 114 | return mapOf( 115 | 2 to Button.of(Material.STONE, "&aItem 1") { 116 | player.sendMessage("&aYou clicked Item 1!") 117 | }, 118 | 4 to Button.of(Material.DIRT, "&eItem 2") { 119 | player.sendMessage("&eYou clicked Item 2!") 120 | }, 121 | 6 to Button.of(Material.GRASS, "&cItem 3") { 122 | player.sendMessage("&cYou clicked Item 3!") 123 | } 124 | ) 125 | } 126 | } 127 | ``` 128 | - Java 129 | ```java 130 | @CancelPlayerInventory(false) 131 | @UpdateAfterClick(true) 132 | public class SimpleMenu extends Menu { 133 | 134 | @Override 135 | public String getTitle(Player player) { 136 | return "&bSimple Menu"; 137 | } 138 | 139 | @Override 140 | public Map getButtons(Player player) { 141 | Map buttons = new HashMap<>(); 142 | 143 | buttons.put(2, new Button() { 144 | @Override 145 | public ItemStack getItem(Player player) { 146 | return new ItemBuilder(Material.STONE) 147 | .name("&aItem 1") 148 | .build(); 149 | } 150 | 151 | @Override 152 | public void onClick(Player player, int slot, ItemStack item) { 153 | player.sendMessage("&aYou clicked Item 1!"); 154 | } 155 | }); 156 | 157 | buttons.put(4, new Button() { 158 | @Override 159 | public ItemStack getItem(Player player) { 160 | return new ItemBuilder(Material.DIRT) 161 | .name("&eItem 2") 162 | .build(); 163 | } 164 | 165 | @Override 166 | public void onClick(Player player, int slot, ItemStack item) { 167 | player.sendMessage("&eYou clicked Item 2!"); 168 | } 169 | }); 170 | 171 | buttons.put(6, new Button() { 172 | @Override 173 | public ItemStack getItem(Player player) { 174 | return new ItemBuilder(Material.GRASS) 175 | .name("&cItem 3") 176 | .build(); 177 | } 178 | 179 | @Override 180 | public void onClick(Player player, int slot, ItemStack item) { 181 | player.sendMessage("&cYou clicked Item 3!"); 182 | } 183 | }); 184 | 185 | return buttons; 186 | } 187 | } 188 | ``` 189 | 190 | ### HotbarAPI 191 | ```kotlin 192 | object HotbarLayout { 193 | fun getItems(): List { 194 | return listOf( 195 | // Example 1: Launch with ender pearl on right click 196 | HotbarItem( 197 | ItemBuilder(Material.ENDER_PEARL) 198 | .setName("&6Ender Butt") 199 | .setLore("&7Right click to dash forward") 200 | .build(), 201 | 0 202 | ) 203 | .blockUse() // This blocks the use, it just executes the code 204 | .putCooldown(5) 205 | .setAction(HotbarActionType.RIGHT_CLICK) { player -> 206 | player.velocity = player.location.direction.multiply(1.6) 207 | player.world.playSound(player.location, Sound.ENDERMAN_TELEPORT, 1f, 1f) 208 | player.sendMessage("&aYou dashed forward!") 209 | }, 210 | 211 | // Example 2: Heal effect on left click 212 | HotbarItem( 213 | ItemBuilder(Material.BLAZE_ROD) 214 | .setName("&cHeal Wand") 215 | .setLore("&7Left click to heal", "&8(10s cooldown)") 216 | .build(), 217 | 1 218 | ) 219 | .putCooldown(10) 220 | .setAction(HotbarActionType.LEFT_CLICK) { player -> 221 | player.addPotionEffect(PotionEffect(PotionEffectType.REGENERATION, 20 * 4, 1)) // 4 seconds 222 | player.sendMessage("&aYou used your healing wand!") 223 | player.world.playSound(player.location, Sound.LEVEL_UP, 1f, 1.2f) 224 | }, 225 | 226 | // Example 3: Debug message on middle click 227 | HotbarItem( 228 | ItemBuilder(Material.BOOK) 229 | .setName("&eInfo Tool") 230 | .setLore("&7Middle click to see info") 231 | .build(), 232 | 2 233 | ) 234 | .setAction(HotbarActionType.MIDDLE_CLICK) { player -> 235 | player.sendMessage("&bYour current location: &f${player.location.blockX}, ${player.location.blockY}, ${player.location.blockZ}") 236 | player.world.playSound(player.location, Sound.NOTE_PLING, 1f, 2f) 237 | } 238 | ) 239 | } 240 | } 241 | ``` 242 | - Java 243 | ```java 244 | public class HotbarLayout { 245 | 246 | public static List getItems() { 247 | return Arrays.asList( 248 | // Example 1: Launch with ender pearl on right click 249 | new HotbarItem( 250 | new ItemBuilder(Material.ENDER_PEARL) 251 | .setName("&6Ender Butt") 252 | .setLore("&7Right click to dash forward") 253 | .build(), 254 | 0 255 | ) 256 | .blockUse() 257 | .putCooldown(5) 258 | .setAction(HotbarActionType.RIGHT_CLICK, (Player player) -> { 259 | player.setVelocity(player.getLocation().getDirection().multiply(1.6)); 260 | player.getWorld().playSound(player.getLocation(), Sound.ENDERMAN_TELEPORT, 1f, 1f); 261 | player.sendMessage("§aYou dashed forward!"); 262 | }), 263 | 264 | // Example 2: Heal effect on left click 265 | new HotbarItem( 266 | new ItemBuilder(Material.BLAZE_ROD) 267 | .setName("&cHeal Wand") 268 | .setLore("&7Left click to heal", "&8(10s cooldown)") 269 | .build(), 270 | 1 271 | ) 272 | .putCooldown(10) 273 | .setAction(HotbarActionType.LEFT_CLICK, (Player player) -> { 274 | player.addPotionEffect(new PotionEffect(PotionEffectType.REGENERATION, 20 * 4, 1)); 275 | player.sendMessage("§aYou used your healing wand!"); 276 | player.getWorld().playSound(player.getLocation(), Sound.LEVEL_UP, 1f, 1.2f); 277 | }), 278 | 279 | // Example 3: Debug message on middle click 280 | new HotbarItem( 281 | new ItemBuilder(Material.BOOK) 282 | .setName("&eInfo Tool") 283 | .setLore("&7Middle click to see info") 284 | .build(), 285 | 2 286 | ) 287 | .setAction(HotbarActionType.MIDDLE_CLICK, (Player player) -> { 288 | player.sendMessage("§bYour current location: §f" + 289 | player.getLocation().getBlockX() + ", " + 290 | player.getLocation().getBlockY() + ", " + 291 | player.getLocation().getBlockZ()); 292 | player.getWorld().playSound(player.getLocation(), Sound.NOTE_PLING, 1f, 2f); 293 | }) 294 | ); 295 | } 296 | } 297 | ``` 298 | 299 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") version "1.9.24" 3 | id("com.gradleup.shadow") version "8.3.0" 4 | } 5 | 6 | group = "dev.y4irr" 7 | version = "1.0-SNAPSHOT" 8 | 9 | repositories { 10 | mavenCentral() 11 | 12 | maven("https://maven.elmakers.com/repository/") { 13 | name = "elmakers-maven" 14 | } 15 | } 16 | 17 | dependencies { 18 | compileOnly("org.spigotmc:spigot:1.8.8-R0.1-SNAPSHOT") 19 | 20 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") 21 | implementation(kotlin("reflect")) 22 | } 23 | 24 | val targetJavaVersion = 17 25 | 26 | kotlin { 27 | jvmToolchain(targetJavaVersion) 28 | } 29 | 30 | tasks.build { 31 | dependsOn("shadowJar") 32 | } 33 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y4irr/CoirLibrary/3f41619f5300e0c596221fac0bac5cdb6b7f214c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat May 03 21:53:30 CLT 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" 3 | } 4 | rootProject.name = "CoirLibrary" 5 | 6 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/ItemBuilder.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir 2 | 3 | import com.mojang.authlib.GameProfile 4 | import com.mojang.authlib.properties.Property 5 | import net.minecraft.server.v1_8_R3.NBTTagCompound 6 | import org.bukkit.ChatColor 7 | import org.bukkit.Color 8 | import org.bukkit.FireworkEffect 9 | import org.bukkit.Material 10 | import org.bukkit.craftbukkit.v1_8_R3.inventory.CraftItemStack 11 | import org.bukkit.enchantments.Enchantment 12 | import org.bukkit.inventory.ItemFlag 13 | import org.bukkit.inventory.ItemStack 14 | import org.bukkit.inventory.meta.* 15 | import java.lang.reflect.Field 16 | import java.util.* 17 | import java.util.concurrent.ThreadLocalRandom 18 | 19 | /* 20 | * This project can't be redistributed without 21 | * authorization of the developer 22 | * 23 | * Project @ CoirLibrary 24 | * @author Yair Soto @ 2025 25 | * Date: month:05 - day:02 26 | */ 27 | 28 | class ItemBuilder(val item: ItemStack) { 29 | 30 | constructor(material: Material, amount: Int = 1) : this(ItemStack(material, amount)) 31 | constructor(material: Material, amount: Int, durability: Byte) : this(ItemStack(material, amount, durability.toShort())) 32 | constructor(material: Material, amount: Int, durability: Int) : this(ItemStack(material, amount, durability.toShort())) 33 | 34 | fun clone(): ItemBuilder = ItemBuilder(item.clone()) 35 | 36 | fun getMeta(): ItemMeta = item.itemMeta 37 | 38 | fun setDurability(durability: Short): ItemBuilder { 39 | item.durability = durability 40 | return this 41 | } 42 | 43 | fun setDurability(durability: Int): ItemBuilder { 44 | item.durability = durability.toShort() 45 | return this 46 | } 47 | 48 | fun setAmount(amount: Int): ItemBuilder { 49 | item.amount = amount.coerceAtMost(64) 50 | return this 51 | } 52 | 53 | fun setName(name: String?): ItemBuilder { 54 | name?.let { 55 | item.itemMeta = item.itemMeta.apply { displayName = color(it) } 56 | } 57 | return this 58 | } 59 | 60 | fun setFireworkColor(color: Color): ItemBuilder { 61 | val meta = item.itemMeta 62 | if (meta is FireworkEffectMeta) { 63 | meta.effect = FireworkEffect.builder().withColor(color).build() 64 | item.itemMeta = meta 65 | } 66 | return this 67 | } 68 | 69 | fun setSkullSkin(value: String, signature: String): ItemBuilder { 70 | if (item.type != Material.SKULL_ITEM) return this 71 | val gameProfile = GameProfile(UUID.randomUUID(), null) 72 | gameProfile.properties.put("textures", Property("textures", value, signature)) 73 | 74 | val meta = item.itemMeta as? SkullMeta ?: return this 75 | try { 76 | val field: Field = meta.javaClass.getDeclaredField("profile") 77 | field.isAccessible = true 78 | field.set(meta, gameProfile) 79 | item.itemMeta = meta 80 | } catch (_: Exception) { 81 | } 82 | return this 83 | } 84 | 85 | fun addEnchant(enchant: Enchantment, level: Int): ItemBuilder { 86 | if (level >= 1) { 87 | item.itemMeta = item.itemMeta.apply { addEnchant(enchant, level, true) } 88 | } 89 | return this 90 | } 91 | 92 | fun unEnchant(enchant: Enchantment): ItemBuilder { 93 | item.removeEnchantment(enchant) 94 | return this 95 | } 96 | 97 | fun setLore(vararg lines: String): ItemBuilder { 98 | return setLore(lines.toList()) 99 | } 100 | 101 | fun setLore(lines: List?): ItemBuilder { 102 | if (lines != null) { 103 | val processed = lines 104 | .map(::color) 105 | .filter { it.isNotEmpty() } 106 | 107 | val meta = item.itemMeta 108 | meta.lore = processed 109 | item.itemMeta = meta 110 | } 111 | return this 112 | } 113 | 114 | fun addLore(lines: List): ItemBuilder { 115 | lines.forEach(this::addLore) 116 | return this 117 | } 118 | 119 | fun addLore(line: String): ItemBuilder { 120 | val meta = item.itemMeta 121 | val lore = meta.lore?.toMutableList() ?: mutableListOf() 122 | lore.add(color(line)) 123 | meta.lore = lore 124 | item.itemMeta = meta 125 | return this 126 | } 127 | 128 | fun addLore(index: Int, line: String): ItemBuilder { 129 | val meta = item.itemMeta 130 | val lore = meta.lore?.toMutableList() ?: mutableListOf() 131 | val clampedIndex = index.coerceIn(0, lore.size) 132 | lore.add(clampedIndex, color(line)) 133 | meta.lore = lore 134 | item.itemMeta = meta 135 | return this 136 | } 137 | 138 | fun clearLore(): ItemBuilder { 139 | item.itemMeta = item.itemMeta.apply { lore = null } 140 | return this 141 | } 142 | 143 | fun addFlags(vararg flags: ItemFlag): ItemBuilder { 144 | item.itemMeta = item.itemMeta.apply { addItemFlags(*flags) } 145 | return this 146 | } 147 | 148 | fun removeFlags(vararg flags: ItemFlag): ItemBuilder { 149 | item.itemMeta = item.itemMeta.apply { removeItemFlags(*flags) } 150 | return this 151 | } 152 | 153 | fun removeAllFlags(): ItemBuilder { 154 | val meta = item.itemMeta 155 | meta.itemFlags.forEach { meta.removeItemFlags(it) } 156 | item.itemMeta = meta 157 | return this 158 | } 159 | 160 | fun hideEnchants(): ItemBuilder = addFlags(ItemFlag.HIDE_ENCHANTS) 161 | fun hideAttributes(): ItemBuilder = addFlags(ItemFlag.HIDE_ATTRIBUTES) 162 | fun hideEffects(): ItemBuilder = addFlags(ItemFlag.HIDE_POTION_EFFECTS) 163 | 164 | fun setColor(color: Color): ItemBuilder { 165 | if (item.type in setOf( 166 | Material.LEATHER_BOOTS, 167 | Material.LEATHER_CHESTPLATE, 168 | Material.LEATHER_HELMET, 169 | Material.LEATHER_LEGGINGS 170 | )) { 171 | val meta = item.itemMeta as? LeatherArmorMeta ?: return this 172 | meta.setColor(color) 173 | item.itemMeta = meta 174 | } else { 175 | throw IllegalArgumentException("color() only applicable for leather armor!") 176 | } 177 | return this 178 | } 179 | 180 | fun setNBTTag(tag: String, value: Any): ItemBuilder { 181 | val copyItem = CraftItemStack.asNMSCopy(item) 182 | val compound = copyItem.tag ?: NBTTagCompound() 183 | 184 | when (value) { 185 | is String -> compound.setString(tag, value) 186 | is Int -> compound.setInt(tag, value) 187 | is Boolean -> compound.setBoolean(tag, value) 188 | } 189 | 190 | copyItem.tag = compound 191 | return this 192 | } 193 | 194 | fun setupPrivateTracker(): ItemBuilder { 195 | return setLore("&8#${ThreadLocalRandom.current().nextInt(1000, 9999)}") 196 | } 197 | 198 | fun build(): ItemStack = item 199 | 200 | 201 | private fun color(input: String): String = 202 | ChatColor.translateAlternateColorCodes('&', input) 203 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/hotbar/HotbarActionType.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.hotbar 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | enum class HotbarActionType { 13 | LEFT_CLICK, 14 | RIGHT_CLICK, 15 | MIDDLE_CLICK 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/hotbar/HotbarItem.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.hotbar 2 | 3 | import org.bukkit.inventory.ItemStack 4 | import org.bukkit.entity.Player 5 | import java.util.* 6 | 7 | /* 8 | * This project can't be redistributed without 9 | * authorization of the developer 10 | * 11 | * Project @ CoirLibrary 12 | * @author Yair Soto @ 2025 13 | * Date: month:05 - day:02 14 | */ 15 | 16 | class HotbarItem( 17 | var item: ItemStack, 18 | val slot: Int, 19 | private val actions: MutableMap Unit> = mutableMapOf() 20 | ) { 21 | var blockUse: Boolean = false 22 | private set 23 | 24 | private var cooldownSeconds: Int = 0 25 | private val cooldownMap: MutableMap = mutableMapOf() 26 | 27 | private val cooldownMessageMap: MutableMap = mutableMapOf() 28 | private val cooldownMessageSeconds: Long = 1 29 | 30 | fun blockUse(): HotbarItem { 31 | this.blockUse = true 32 | return this 33 | } 34 | 35 | fun putCooldown(seconds: Int): HotbarItem { 36 | this.cooldownSeconds = seconds 37 | return this 38 | } 39 | 40 | fun setAction(type: HotbarActionType, action: (Player) -> Unit): HotbarItem { 41 | actions[type] = action 42 | return this 43 | } 44 | 45 | fun getAction(type: HotbarActionType): ((Player) -> Unit)? { 46 | if (actions.isEmpty()) return null 47 | if (actions.size == 1) return actions.values.first() 48 | return actions[type] 49 | } 50 | 51 | fun tryExecute(player: Player, type: HotbarActionType) { 52 | if (isOnCooldown(player)) { 53 | if (canSendCooldownMessage(player)) { 54 | val remaining = getRemainingCooldown(player) 55 | player.sendMessage("§cYou're on cooldown for this item for $remaining seconds.") 56 | cooldownMessageMap[player.uniqueId] = System.currentTimeMillis() 57 | } 58 | return 59 | } 60 | 61 | getAction(type)?.invoke(player) 62 | markCooldown(player) 63 | } 64 | 65 | fun isOnCooldown(player: Player): Boolean { 66 | val lastUse = cooldownMap[player.uniqueId] ?: return false 67 | val elapsed = (System.currentTimeMillis() - lastUse) / 1000 68 | return elapsed < cooldownSeconds 69 | } 70 | 71 | fun getRemainingCooldown(player: Player): Long { 72 | val lastUse = cooldownMap[player.uniqueId] ?: return 0 73 | val elapsed = (System.currentTimeMillis() - lastUse) / 1000 74 | return (cooldownSeconds - elapsed).coerceAtLeast(1) 75 | } 76 | 77 | fun canSendCooldownMessage(player: Player): Boolean { 78 | val lastMsg = cooldownMessageMap[player.uniqueId] ?: return true 79 | val elapsed = (System.currentTimeMillis() - lastMsg) / 1000 80 | return elapsed >= cooldownMessageSeconds 81 | } 82 | 83 | fun getCooldownProgress(player: Player): Float { 84 | val lastUse = cooldownMap[player.uniqueId] ?: return 0f 85 | val elapsed = (System.currentTimeMillis() - lastUse) / 1000f 86 | return if (elapsed >= cooldownSeconds) 0f else (1f - (elapsed / cooldownSeconds)).coerceIn(0f, 1f) 87 | } 88 | 89 | fun markCooldown(player: Player) { 90 | if (cooldownSeconds > 0) { 91 | cooldownMap[player.uniqueId] = System.currentTimeMillis() 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/hotbar/HotbarListener.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.hotbar 2 | 3 | import org.bukkit.Bukkit 4 | import org.bukkit.event.EventHandler 5 | import org.bukkit.event.Listener 6 | import org.bukkit.event.player.PlayerInteractEvent 7 | import org.bukkit.event.block.Action 8 | import org.bukkit.event.entity.PlayerDeathEvent 9 | import org.bukkit.event.player.PlayerJoinEvent 10 | import org.bukkit.event.player.PlayerQuitEvent 11 | import org.bukkit.event.player.PlayerRespawnEvent 12 | import org.bukkit.plugin.Plugin 13 | 14 | /* 15 | * This project can't be redistributed without 16 | * authorization of the developer 17 | * 18 | * Project @ CoirLibrary 19 | * @author Yair Soto @ 2025 20 | * Date: month:05 - day:02 21 | */ 22 | 23 | class HotbarListener(private val plugin: Plugin) : Listener { 24 | 25 | @EventHandler 26 | fun onJoin(event: PlayerJoinEvent) { 27 | HotbarSetup.applyTo(event.player) 28 | } 29 | 30 | @EventHandler 31 | fun onRespawn(event: PlayerRespawnEvent) { 32 | Bukkit.getScheduler().runTaskLater(plugin, { 33 | HotbarSetup.applyTo(event.player) 34 | }, 2L) 35 | } 36 | 37 | @EventHandler 38 | fun onDeath(event: PlayerDeathEvent) { 39 | val hotbar = HotbarManager.getHotbarItem(event.entity, event.entity.inventory.heldItemSlot) 40 | event.drops.removeIf { drop -> hotbar != null && drop.isSimilar(hotbar.item) } 41 | } 42 | 43 | @EventHandler 44 | fun onQuit(event: PlayerQuitEvent) { 45 | HotbarManager.clear(event.player) 46 | } 47 | 48 | @EventHandler 49 | fun onClick(event: PlayerInteractEvent) { 50 | val player = event.player 51 | val slot = player.inventory.heldItemSlot 52 | val hotbarItem = HotbarManager.getHotbarItem(player, slot) ?: return 53 | val handItem = player.inventory.getItem(slot) ?: return 54 | 55 | if (!handItem.isSimilar(hotbarItem.item)) return 56 | 57 | val actionType = when (event.action) { 58 | Action.LEFT_CLICK_AIR, Action.LEFT_CLICK_BLOCK -> HotbarActionType.LEFT_CLICK 59 | Action.RIGHT_CLICK_AIR, Action.RIGHT_CLICK_BLOCK -> HotbarActionType.RIGHT_CLICK 60 | else -> null 61 | } 62 | 63 | if (hotbarItem.blockUse) { 64 | event.isCancelled = true 65 | player.updateInventory() 66 | } 67 | 68 | if (event.isBlockInHand && player.isSneaking) { 69 | hotbarItem.getAction(HotbarActionType.MIDDLE_CLICK)?.invoke(player) 70 | return 71 | } 72 | 73 | actionType?.let { 74 | hotbarItem.tryExecute(player, it) 75 | } 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/hotbar/HotbarManager.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.hotbar 2 | 3 | import org.bukkit.entity.Player 4 | 5 | /* 6 | * This project can't be redistributed without 7 | * authorization of the developer 8 | * 9 | * Project @ CoirLibrary 10 | * @author Yair Soto @ 2025 11 | * Date: month:05 - day:02 12 | */ 13 | 14 | object HotbarManager { 15 | private val playerHotbars = mutableMapOf>() 16 | 17 | fun setHotbar(player: Player, items: List) { 18 | playerHotbars[player.name] = items 19 | for (item in items) { 20 | player.inventory.setItem(item.slot, item.item) 21 | } 22 | } 23 | 24 | fun getHotbarItem(player: Player, slot: Int): HotbarItem? { 25 | return playerHotbars[player.name]?.firstOrNull { it.slot == slot } 26 | } 27 | 28 | fun clear(player: Player) { 29 | playerHotbars.remove(player.name) 30 | } 31 | 32 | fun updateHotbarItemStack(player: Player, updated: HotbarItem) { 33 | player.inventory.setItem(updated.slot, updated.item) 34 | playerHotbars[player.name]?.let { list -> 35 | val updatedList = list.toMutableList() 36 | val index = updatedList.indexOfFirst { it.slot == updated.slot } 37 | if (index != -1) updatedList[index] = updated 38 | playerHotbars[player.name] = updatedList 39 | } 40 | } 41 | 42 | fun updateHotbarSlot(player: Player, item: HotbarItem) { 43 | val items = playerHotbars[player.name]?.toMutableList() ?: return 44 | val index = items.indexOfFirst { it.slot == item.slot } 45 | if (index != -1) { 46 | items[index] = item 47 | playerHotbars[player.name] = items 48 | player.inventory.setItem(item.slot, item.item) 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/hotbar/HotbarSetup.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.hotbar 2 | 3 | import org.bukkit.Bukkit 4 | import org.bukkit.entity.Player 5 | import org.bukkit.plugin.Plugin 6 | import org.bukkit.scheduler.BukkitRunnable 7 | 8 | 9 | /* 10 | * This project can't be redistributed without 11 | * authorization of the developer 12 | * 13 | * Project @ CoirLibrary 14 | * @author Yair Soto @ 2025 15 | * Date: month:05 - day:02 16 | */ 17 | 18 | object HotbarSetup { 19 | 20 | private val registeredItems = mutableListOf() 21 | 22 | fun initialize(plugin: Plugin) { 23 | Bukkit.getPluginManager().registerEvents(HotbarListener(plugin), plugin) 24 | 25 | registeredItems.clear() 26 | start(plugin) 27 | } 28 | 29 | fun getRegisteredItems(): List = registeredItems 30 | 31 | fun registerItem(item: HotbarItem) { 32 | registeredItems.add(item) 33 | } 34 | 35 | private fun start(plugin: Plugin) { 36 | object : BukkitRunnable() { 37 | override fun run() { 38 | for (player in Bukkit.getOnlinePlayers()) { 39 | val slot = player.inventory.heldItemSlot 40 | val item = HotbarManager.getHotbarItem(player, slot) 41 | 42 | if (item != null) { 43 | val progress = item.getCooldownProgress(player) 44 | player.exp = progress 45 | player.level = if (progress > 0f) item.getRemainingCooldown(player).toInt() else 0 46 | } else { 47 | player.exp = 0f 48 | player.level = 0 49 | } 50 | } 51 | } 52 | }.runTaskTimerAsynchronously(plugin, 0L, 1L) 53 | } 54 | 55 | fun applyTo(player: Player) { 56 | HotbarManager.setHotbar(player, registeredItems) 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/menu/Button.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.menu 2 | 3 | import org.bukkit.entity.Player 4 | import org.bukkit.event.inventory.ClickType 5 | import org.bukkit.inventory.ItemStack 6 | 7 | /* 8 | * This project can't be redistributed without 9 | * authorization of the developer 10 | * 11 | * Project @ CoirLibrary 12 | * @author Yair Soto @ 2025 13 | * Date: month:05 - day:02 14 | */ 15 | 16 | abstract class Button { 17 | abstract fun getItem(player: Player): ItemStack? 18 | 19 | open fun clicked(player: Player, slot: Int, click: ClickType, hotbar: Int) {} 20 | open fun shouldUpdate(player: Player, slot: Int, click: ClickType): Boolean = false 21 | open fun shouldCancel(player: Player, slot: Int, click: ClickType): Boolean = true 22 | open fun shouldShift(player: Player, slot: Int, click: ClickType): Boolean = true 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/menu/Menu.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.menu 2 | 3 | import dev.y4irr.coir.menu.impl.* 4 | import org.bukkit.Bukkit 5 | import org.bukkit.Material 6 | import org.bukkit.entity.Player 7 | import org.bukkit.event.inventory.InventoryCloseEvent 8 | import org.bukkit.inventory.Inventory 9 | import org.bukkit.inventory.ItemStack 10 | 11 | /* 12 | * This project can't be redistributed without 13 | * authorization of the developer 14 | * 15 | * Project @ CoirLibrary 16 | * @author Yair Soto @ 2025 17 | * Date: month:05 - day:02 18 | */ 19 | 20 | abstract class Menu { 21 | val buttons = mutableMapOf() 22 | val playerButtons = mutableMapOf() 23 | private val originalItems = mutableMapOf() 24 | private var inventory: Inventory? = null 25 | 26 | abstract fun getTitle(player: Player): String 27 | open fun getSize(): Int = 27 28 | open fun onOpen(player: Player) {} 29 | open fun onClose(player: Player, event: InventoryCloseEvent) {} 30 | open fun getPlaceholder(): ItemStack = ItemStack(Material.STAINED_GLASS_PANE, 1, 7) 31 | 32 | open fun getInventoryButtons(player: Player): Map = emptyMap() 33 | 34 | fun open(player: Player) { 35 | val clazz = this::class 36 | val updateAfterClick = clazz.annotations.filterIsInstance().firstOrNull()?.value ?: false 37 | val cancelInventory = clazz.annotations.filterIsInstance().firstOrNull()?.value ?: true 38 | val usePlayerInventory = clazz.annotations.filterIsInstance().firstOrNull()?.value ?: false 39 | val title = getTitle(player).translated().take(32) 40 | 41 | val size = getSize() 42 | val inv = Bukkit.createInventory(player, size, title) 43 | inventory = inv 44 | 45 | buttons.clear() 46 | buttons.putAll(getButtons(player)) 47 | 48 | for (i in 0 until size) { 49 | val button = buttons[i] 50 | inv.setItem(i, button?.getItem(player)?.translatedMeta() ?: getPlaceholder().translatedMeta()) 51 | } 52 | 53 | if (usePlayerInventory) { 54 | originalItems.clear() 55 | playerButtons.clear() 56 | playerButtons.putAll(getInventoryButtons(player)) 57 | 58 | for (slot in 9..35) { 59 | originalItems[slot] = player.inventory.getItem(slot) ?: ItemStack(Material.AIR) 60 | player.inventory.setItem(slot, null) 61 | } 62 | 63 | for ((slot, button) in playerButtons) { 64 | player.inventory.setItem(slot, button.getItem(player)?.translatedMeta()) 65 | } 66 | } 67 | 68 | player.openInventory(inv) 69 | onOpen(player) 70 | 71 | MenuManager.open(player, this, updateAfterClick, cancelInventory, usePlayerInventory) 72 | } 73 | 74 | fun restoreInventory(player: Player) { 75 | if (originalItems.isNotEmpty()) { 76 | for ((slot, item) in originalItems) { 77 | player.inventory.setItem(slot, item) 78 | } 79 | originalItems.clear() 80 | } 81 | } 82 | 83 | abstract fun getButtons(player: Player): Map 84 | } 85 | 86 | private fun String.translated(): String = replace('&', '§') 87 | 88 | private fun ItemStack.translatedMeta(): ItemStack { 89 | val clone = this.clone() 90 | val meta = clone.itemMeta ?: return clone 91 | if (meta.hasDisplayName()) meta.displayName = meta.displayName.translated() 92 | if (meta.hasLore()) meta.lore = meta.lore.map { it.translated() } 93 | clone.itemMeta = meta 94 | return clone 95 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/menu/MenuListener.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.menu 2 | 3 | import org.bukkit.Bukkit 4 | import org.bukkit.event.EventHandler 5 | import org.bukkit.event.EventPriority 6 | import org.bukkit.event.Listener 7 | import org.bukkit.event.inventory.InventoryClickEvent 8 | import org.bukkit.event.inventory.InventoryCloseEvent 9 | import org.bukkit.entity.Player 10 | import org.bukkit.event.inventory.ClickType 11 | import org.bukkit.plugin.Plugin 12 | import java.util.* 13 | 14 | /* 15 | * This project can't be redistributed without 16 | * authorization of the developer 17 | * 18 | * Project @ CoirLibrary 19 | * @author Yair Soto @ 2025 20 | * Date: month:05 - day:02 21 | */ 22 | 23 | class MenuListener(private val plugin: Plugin) : Listener { 24 | 25 | private val clickCooldown = mutableMapOf() 26 | 27 | @EventHandler(priority = EventPriority.HIGH) 28 | fun onClick(event: InventoryClickEvent) { 29 | val player = event.whoClicked as? Player ?: return 30 | val session = MenuManager.getSession(player) ?: return 31 | val currentMenu = session.menu 32 | 33 | val top = player.openInventory.topInventory 34 | val rawSlot = event.rawSlot 35 | val slot = event.slot 36 | 37 | val isTop = rawSlot == slot 38 | val isBottom = rawSlot >= top.size 39 | 40 | if (session.usePlayerInventory) { 41 | if (isBottom) { 42 | val button = currentMenu.playerButtons[slot] ?: return 43 | button.clicked(player, slot, event.click, event.hotbarButton) 44 | event.isCancelled = true 45 | return 46 | } 47 | 48 | if (!session.cancelInventory && isBottom.not()) { 49 | val moveToPlayer = session.moveItemToPlayer 50 | if (!moveToPlayer) { 51 | if (event.click == ClickType.DROP || event.click.name.contains("OUTSIDE")) { 52 | Bukkit.getScheduler().runTaskLater(plugin, { 53 | player.openInventory.topInventory.setItem(slot, event.currentItem) 54 | }, 1L) 55 | event.isCancelled = true 56 | } 57 | } 58 | return 59 | } 60 | } 61 | 62 | event.isCancelled = session.cancelInventory 63 | 64 | if (!isTop || slot < 0 || slot >= top.size) return 65 | val button = currentMenu.buttons[slot] ?: return 66 | 67 | if (button.shouldCancel(player, slot, event.click) && button.shouldShift(player, slot, event.click)) { 68 | event.isCancelled = true 69 | } 70 | 71 | try { 72 | button.clicked(player, slot, event.click, event.hotbarButton) 73 | } finally { 74 | val afterClickMenu = MenuManager.get(player) 75 | if (afterClickMenu != null && afterClickMenu !== currentMenu) { 76 | Bukkit.getScheduler().runTask(plugin) { 77 | afterClickMenu.open(player) 78 | } 79 | return 80 | } 81 | 82 | if (session.updateAfterClick || button.shouldUpdate(player, slot, event.click)) { 83 | Bukkit.getScheduler().runTask(plugin) { 84 | currentMenu.open(player) 85 | } 86 | } 87 | } 88 | } 89 | 90 | @EventHandler(priority = EventPriority.HIGH) 91 | fun onClose(event: InventoryCloseEvent) { 92 | val player = event.player as? Player ?: return 93 | val session = MenuManager.getSession(player) ?: return 94 | 95 | session.menu.onClose(player, event) 96 | MenuManager.remove(player) 97 | } 98 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/menu/MenuManager.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.menu 2 | 3 | import dev.y4irr.coir.menu.impl.MoveItemToPlayer 4 | import org.bukkit.entity.Player 5 | import java.util.* 6 | 7 | /* 8 | * This project can't be redistributed without 9 | * authorization of the developer 10 | * 11 | * Project @ CoirLibrary 12 | * @author Yair Soto @ 2025 13 | * Date: month:05 - day:02 14 | */ 15 | 16 | object MenuManager { 17 | 18 | private val menus = mutableMapOf() 19 | private val activeClickLocks = mutableSetOf() 20 | 21 | fun open( 22 | player: Player, 23 | menu: Menu, 24 | updateAfterClick: Boolean, 25 | cancelInventory: Boolean, 26 | usePlayerInventory: Boolean = false 27 | ) { 28 | val moveItemToPlayer = menu::class.annotations.filterIsInstance().firstOrNull()?.value ?: false 29 | menus[player.uniqueId] = MenuSession(menu, updateAfterClick, cancelInventory, usePlayerInventory, moveItemToPlayer) 30 | } 31 | 32 | fun isClickLocked(player: Player): Boolean { 33 | return activeClickLocks.contains(player.uniqueId) 34 | } 35 | 36 | fun lockClick(player: Player) { 37 | activeClickLocks.add(player.uniqueId) 38 | } 39 | 40 | fun unlockClick(player: Player) { 41 | activeClickLocks.remove(player.uniqueId) 42 | } 43 | 44 | fun get(player: Player): Menu? = menus[player.uniqueId]?.menu 45 | fun getSession(player: Player): MenuSession? = menus[player.uniqueId] 46 | fun remove(player: Player) { 47 | menus.remove(player.uniqueId) 48 | } 49 | 50 | data class MenuSession( 51 | val menu: Menu, 52 | val updateAfterClick: Boolean, 53 | val cancelInventory: Boolean, 54 | val usePlayerInventory: Boolean, 55 | val moveItemToPlayer: Boolean 56 | ) 57 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/menu/impl/AsynchronousTask.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.menu.impl 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | annotation class AsynchronousTask(val value: Boolean = false) -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/menu/impl/AutoUpdate.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.menu.impl 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | annotation class AutoUpdate(val value: Boolean = false) -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/menu/impl/CancelPlayerInventory.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.menu.impl 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | annotation class CancelPlayerInventory(val value: Boolean = true) -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/menu/impl/MoveItemToPlayer.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.menu.impl 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | annotation class MoveItemToPlayer(val value: Boolean = false) -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/menu/impl/TickUpdate.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.menu.impl 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | annotation class TickUpdate(val interval: Long = 20L) -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/menu/impl/UpdateAfterClick.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.menu.impl 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | annotation class UpdateAfterClick(val value: Boolean = false) -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/menu/impl/UsePlayerInventory.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.menu.impl 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | annotation class UsePlayerInventory(val value: Boolean = false) -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/Wai.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard 2 | 3 | import dev.y4irr.coir.scoreboard.annotations.DebugMode 4 | import dev.y4irr.coir.scoreboard.annotations.SbOptions 5 | import dev.y4irr.coir.scoreboard.annotations.Tickable 6 | import dev.y4irr.coir.scoreboard.board.Board 7 | import dev.y4irr.coir.scoreboard.board.BoardUpdateThread 8 | import dev.y4irr.coir.scoreboard.events.BoardCreateEvent 9 | import dev.y4irr.coir.scoreboard.listener.BoardEventLogger 10 | import dev.y4irr.coir.scoreboard.listener.BoardListener 11 | import dev.y4irr.coir.scoreboard.utils.getAnnotation 12 | import org.bukkit.Bukkit 13 | import org.bukkit.entity.Player 14 | import org.bukkit.plugin.Plugin 15 | import java.util.concurrent.ConcurrentHashMap 16 | 17 | /* 18 | * This project can't be redistributed without 19 | * authorization of the developer 20 | * 21 | * Project @ CoirLibrary 22 | * @author Yair Soto @ 2025 23 | * Date: month:05 - day:02 24 | */ 25 | 26 | class Wai( 27 | val plugin: Plugin, 28 | private val provider: WaiAdapter 29 | ) { 30 | 31 | private val boards = ConcurrentHashMap() 32 | val lastUpdateMap = ConcurrentHashMap() 33 | 34 | val debugMode: Boolean = provider.getAnnotation()?.bool ?: false 35 | private val async: Boolean = provider.getAnnotation()?.isAsync ?: false 36 | private val tick: Long = provider.getAnnotation()?.tick ?: 20L 37 | 38 | private val updateThread = BoardUpdateThread(this) 39 | 40 | fun start() { 41 | updateThread.start() 42 | Bukkit.getPluginManager().registerEvents(BoardListener(this), plugin) 43 | 44 | if (debugMode) { 45 | Bukkit.getPluginManager().registerEvents(BoardEventLogger(), plugin) 46 | plugin.logger.info("[Wai] Debug mode enabled via @DebugMode(true)") 47 | } 48 | } 49 | 50 | fun stop() { 51 | updateThread.stopRunning() 52 | } 53 | 54 | fun createBoard(player: Player) { 55 | if (boards.containsKey(player)) return 56 | 57 | val board = Board(player, this, provider, async, tick) 58 | boards[player] = board 59 | lastUpdateMap[player] = System.currentTimeMillis() 60 | 61 | if (debugMode) { 62 | plugin.logger.info("[Wai] Created board for ${player.name} (async=$async, tick=$tick)") 63 | } 64 | 65 | Bukkit.getPluginManager().callEvent(BoardCreateEvent(board, player)) 66 | } 67 | 68 | fun removeBoard(player: Player) { 69 | if (debugMode) { 70 | plugin.logger.info("[Wai] Removing board for ${player.name}") 71 | } 72 | boards.remove(player) 73 | lastUpdateMap.remove(player) 74 | } 75 | 76 | fun clearBoards() { 77 | boards.clear() 78 | lastUpdateMap.clear() 79 | } 80 | 81 | fun getBoards(): Map = boards 82 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/WaiAdapter.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard 2 | 3 | import org.bukkit.entity.Player 4 | 5 | /* 6 | * This project can't be redistributed without 7 | * authorization of the developer 8 | * 9 | * Project @ CoirLibrary 10 | * @author Yair Soto @ 2025 11 | * Date: month:05 - day:02 12 | */ 13 | 14 | interface WaiAdapter { 15 | fun getTitle(player: Player): String 16 | fun getLines(player: Player): List 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/annotations/BoardDescending.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.annotations 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | annotation class BoardDescending(val value: Boolean = false) -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/annotations/BoardStartingNumber.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.annotations 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | annotation class BoardStartingNumber(val value: Int = 0) -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/annotations/BoardStyle.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.annotations 2 | 3 | import dev.y4irr.coir.scoreboard.enums.BoardStyleType 4 | 5 | /* 6 | * This project can't be redistributed without 7 | * authorization of the developer 8 | * 9 | * Project @ CoirLibrary 10 | * @author Yair Soto @ 2025 11 | * Date: month:05 - day:02 12 | */ 13 | 14 | @Target(AnnotationTarget.CLASS) 15 | @Retention(AnnotationRetention.RUNTIME) 16 | annotation class BoardStyle(val type: BoardStyleType = BoardStyleType.ASCENDING) 17 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/annotations/DebugMode.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.annotations 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | annotation class DebugMode(val bool: Boolean = false) -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/annotations/SbOptions.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.annotations 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | annotation class SbOptions(val isAsync: Boolean = false) -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/annotations/Tickable.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.annotations 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | annotation class Tickable(val tick: Long = 20L) -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/annotations/Title.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.annotations 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.RUNTIME) 14 | annotation class Title(val title: String = "EMPTY") -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/board/Board.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.board 2 | 3 | import dev.y4irr.coir.scoreboard.Wai 4 | import dev.y4irr.coir.scoreboard.WaiAdapter 5 | import dev.y4irr.coir.scoreboard.annotations.BoardDescending 6 | import dev.y4irr.coir.scoreboard.annotations.BoardStartingNumber 7 | import dev.y4irr.coir.scoreboard.annotations.BoardStyle 8 | import dev.y4irr.coir.scoreboard.annotations.Title 9 | import dev.y4irr.coir.scoreboard.enums.BoardStyleType 10 | import dev.y4irr.coir.scoreboard.events.BoardChangeEvent 11 | import dev.y4irr.coir.scoreboard.events.BoardDestroyEvent 12 | import dev.y4irr.coir.scoreboard.utils.getAnnotation 13 | import org.bukkit.Bukkit 14 | import org.bukkit.ChatColor 15 | import org.bukkit.entity.Player 16 | import org.bukkit.scoreboard.DisplaySlot 17 | import org.bukkit.scoreboard.Objective 18 | import org.bukkit.scoreboard.Scoreboard 19 | 20 | /* 21 | * This project can't be redistributed without 22 | * authorization of the developer 23 | * 24 | * Project @ CoirLibrary 25 | * @author Yair Soto @ 2025 26 | * Date: month:05 - day:02 27 | */ 28 | 29 | class Board( 30 | val player: Player, 31 | val wai: Wai, 32 | private val provider: WaiAdapter, 33 | val isAsync: Boolean, 34 | val tick: Long 35 | ) { 36 | 37 | private var lastLines: List = emptyList() 38 | 39 | val scoreboard: Scoreboard = Bukkit.getScoreboardManager().newScoreboard 40 | val objective: Objective = scoreboard.registerNewObjective("wai", "dummy") 41 | private val entries = mutableListOf() 42 | 43 | init { 44 | objective.displaySlot = DisplaySlot.SIDEBAR 45 | player.scoreboard = scoreboard 46 | } 47 | 48 | fun update() { 49 | val newLines = provider.getLines(player).take(15) 50 | 51 | if (newLines == lastLines) return 52 | 53 | if (wai.debugMode) { 54 | wai.plugin.logger.info("[Wai] Updating board for ${player.name}") 55 | wai.plugin.logger.info("[Wai] New lines: $newLines") 56 | } 57 | 58 | Bukkit.getPluginManager().callEvent(BoardChangeEvent(player, this, lastLines, newLines)) 59 | lastLines = newLines 60 | 61 | objective.displayName = ChatColor.translateAlternateColorCodes('&', resolveTitle().take(32)) 62 | 63 | scoreboard.entries.forEach { scoreboard.resetScores(it) } 64 | entries.forEach { it.remove() } 65 | entries.clear() 66 | 67 | newLines.forEachIndexed { index, line -> 68 | val score = resolvePosition(newLines.size - 1 - index) // solo el número se invierte 69 | val identifier = getSafeIdentifier(index) 70 | 71 | val entry = BoardEntry(this, line, identifier) 72 | entry.send(score) 73 | entries.add(entry) 74 | } 75 | } 76 | 77 | fun destroy() { 78 | if (wai.debugMode) { 79 | wai.plugin.logger.info("[Wai] Destroying board for ${player.name}") 80 | } 81 | Bukkit.getPluginManager().callEvent(BoardDestroyEvent(player, this)) 82 | entries.forEach { it.remove() } 83 | entries.clear() 84 | player.scoreboard.clearSlot(DisplaySlot.SIDEBAR) 85 | wai.removeBoard(player) 86 | } 87 | 88 | private fun resolvePosition(index: Int): Int { 89 | val style = provider.getAnnotation()?.type ?: BoardStyleType.ASCENDING 90 | 91 | val descending = if (style == BoardStyleType.CUSTOM) { 92 | provider.getAnnotation()?.value ?: false 93 | } else { 94 | style.descending 95 | } 96 | 97 | val start = if (style == BoardStyleType.CUSTOM) { 98 | provider.getAnnotation()?.value ?: 0 99 | } else { 100 | style.startNumber 101 | } 102 | 103 | return if (descending) start - index else start + index 104 | } 105 | 106 | private fun resolveTitle(): String { 107 | val titleAnnotation = provider.getAnnotation() 108 | val title = titleAnnotation?.title ?: "EMPTY" 109 | return if (title != "EMPTY") title else provider.getTitle(player) 110 | } 111 | 112 | private fun getSafeIdentifier(index: Int): String { 113 | val colors = ChatColor.values().filter { it != ChatColor.RESET } 114 | val color1 = colors[index % colors.size].toString() 115 | val color2 = colors[(index / colors.size) % colors.size].toString() 116 | return color1 + color2 117 | } 118 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/board/BoardEntry.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.board 2 | 3 | import org.bukkit.ChatColor 4 | import org.bukkit.scoreboard.Scoreboard 5 | import org.bukkit.scoreboard.Team 6 | 7 | /* 8 | * This project can't be redistributed without 9 | * authorization of the developer 10 | * 11 | * Project @ CoirLibrary 12 | * @author Yair Soto @ 2025 13 | * Date: month:05 - day:02 14 | */ 15 | 16 | class BoardEntry( 17 | private val board: Board, 18 | private var text: String, 19 | private val identifier: String 20 | ) { 21 | 22 | private val team: Team 23 | 24 | init { 25 | val scoreboard: Scoreboard = board.scoreboard 26 | 27 | val teamName = identifier.take(16) 28 | val existing = scoreboard.getTeam(teamName) 29 | team = existing ?: scoreboard.registerNewTeam(teamName) 30 | 31 | if (!team.entries.contains(identifier)) { 32 | team.addEntry(identifier) 33 | } 34 | } 35 | 36 | fun send(score: Int) { 37 | val (prefix, suffix) = splitText(text) 38 | team.prefix = prefix 39 | team.suffix = suffix 40 | board.objective.getScore(identifier).score = score 41 | } 42 | 43 | fun remove() { 44 | board.scoreboard.resetScores(identifier) 45 | team.unregister() 46 | } 47 | 48 | private fun splitText(input: String): Pair<String, String> { 49 | val translated = ChatColor.translateAlternateColorCodes('&', input) 50 | if (translated.length <= 16) return translated to "" 51 | 52 | var prefix = translated.substring(0, 16) 53 | var suffix: String 54 | 55 | val lastColorIndex = prefix.lastIndexOf(ChatColor.COLOR_CHAR) 56 | if (lastColorIndex >= 14) { 57 | prefix = prefix.substring(0, lastColorIndex) 58 | suffix = ChatColor.getLastColors(translated.substring(0, 17)) + translated.substring(lastColorIndex + 2) 59 | } else { 60 | suffix = ChatColor.getLastColors(prefix) + translated.substring(16) 61 | } 62 | 63 | if (suffix.length > 16) suffix = suffix.substring(0, 16) 64 | return prefix to suffix 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/board/BoardUpdateThread.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.board 2 | 3 | import dev.y4irr.coir.scoreboard.Wai 4 | import org.bukkit.Bukkit 5 | 6 | /* 7 | * This project can't be redistributed without 8 | * authorization of the developer 9 | * 10 | * Project @ CoirLibrary 11 | * @author Yair Soto @ 2025 12 | * Date: month:05 - day:02 13 | */ 14 | 15 | class BoardUpdateThread(private val wai: Wai) : Thread("Wai-Thread") { 16 | 17 | private var running = true 18 | private var debugTickCounter = 0L 19 | 20 | override fun run() { 21 | while (running) { 22 | val now = System.currentTimeMillis() 23 | for ((player, board) in wai.getBoards()) { 24 | if (!player.isOnline) continue 25 | 26 | val interval = board.tick 27 | val last = wai.lastUpdateMap[player] ?: 0 28 | if (now - last >= interval) { 29 | wai.lastUpdateMap[player] = now 30 | if (board.isAsync) { 31 | Bukkit.getScheduler().runTaskAsynchronously(wai.plugin) { 32 | board.update() 33 | if (wai.debugMode && debugTickCounter % 20 == 0L) { 34 | wai.plugin.logger.info("[Wai] Async update: ${player.name}") 35 | } 36 | } 37 | } else { 38 | Bukkit.getScheduler().runTask(wai.plugin) { 39 | board.update() 40 | if (wai.debugMode && debugTickCounter % 20 == 0L) { 41 | wai.plugin.logger.info("[Wai] Sync update: ${player.name}") 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | if (wai.debugMode && debugTickCounter % 20 == 0L) { 49 | wai.plugin.logger.info("[Wai] Update thread running... (${wai.getBoards().size} boards tracked)") 50 | } 51 | 52 | debugTickCounter++ 53 | 54 | try { 55 | sleep(50L) 56 | } catch (e: InterruptedException) { 57 | break 58 | } 59 | } 60 | } 61 | 62 | fun stopRunning() { 63 | running = false 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/enums/BoardStyleType.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.enums 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | enum class BoardStyleType(val descending: Boolean, val startNumber: Int) { 13 | CLASSIC(true, 15), 14 | REVERSE(true, -1), 15 | ASCENDING(false, 1), 16 | CUSTOM(false, 0); 17 | 18 | fun withDescending(desc: Boolean): BoardStyleType { 19 | return entries.first { it.name == this.name }.apply { 20 | descending == desc 21 | } 22 | } 23 | 24 | fun withStartNumber(start: Int): BoardStyleType { 25 | return entries.first { it.name == this.name }.apply { 26 | startNumber == start 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/events/BoardChangeEvent.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.events 2 | 3 | import dev.y4irr.coir.scoreboard.board.Board 4 | import org.bukkit.entity.Player 5 | import org.bukkit.event.Event 6 | import org.bukkit.event.HandlerList 7 | 8 | /* 9 | * This project can't be redistributed without 10 | * authorization of the developer 11 | * 12 | * Project @ CoirLibrary 13 | * @author Yair Soto @ 2025 14 | * Date: month:05 - day:02 15 | */ 16 | 17 | class BoardChangeEvent( 18 | val player: Player, 19 | val board: Board, 20 | val oldLines: List<String>, 21 | val newLines: List<String> 22 | ) : Event() { 23 | override fun getHandlers(): HandlerList = handlerList 24 | 25 | companion object { 26 | @JvmStatic val handlerList = HandlerList() 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/events/BoardCreateEvent.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.events 2 | 3 | import dev.y4irr.coir.scoreboard.board.Board 4 | import org.bukkit.entity.Player 5 | import org.bukkit.event.Event 6 | import org.bukkit.event.HandlerList 7 | 8 | /* 9 | * This project can't be redistributed without 10 | * authorization of the developer 11 | * 12 | * Project @ CoirLibrary 13 | * @author Yair Soto @ 2025 14 | * Date: month:05 - day:02 15 | */ 16 | 17 | class BoardCreateEvent(val board: Board, val player: Player) : Event() { 18 | override fun getHandlers(): HandlerList = handlerList 19 | 20 | companion object { 21 | @JvmStatic val handlerList = HandlerList() 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/events/BoardDestroyEvent.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.events 2 | 3 | import dev.y4irr.coir.scoreboard.board.Board 4 | import org.bukkit.entity.Player 5 | import org.bukkit.event.Event 6 | import org.bukkit.event.HandlerList 7 | 8 | /* 9 | * This project can't be redistributed without 10 | * authorization of the developer 11 | * 12 | * Project @ CoirLibrary 13 | * @author Yair Soto @ 2025 14 | * Date: month:05 - day:02 15 | */ 16 | 17 | class BoardDestroyEvent(val player: Player, val board: Board) : Event() { 18 | override fun getHandlers(): HandlerList = handlerList 19 | 20 | companion object { 21 | @JvmStatic val handlerList = HandlerList() 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/listener/BoardEventLogger.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.listener 2 | 3 | import dev.y4irr.coir.scoreboard.events.BoardChangeEvent 4 | import dev.y4irr.coir.scoreboard.events.BoardCreateEvent 5 | import dev.y4irr.coir.scoreboard.events.BoardDestroyEvent 6 | import org.bukkit.event.EventHandler 7 | import org.bukkit.event.Listener 8 | 9 | /* 10 | * This project can't be redistributed without 11 | * authorization of the developer 12 | * 13 | * Project @ CoirLibrary 14 | * @author Yair Soto @ 2025 15 | * Date: month:05 - day:02 16 | */ 17 | 18 | class BoardEventLogger : Listener { 19 | 20 | @EventHandler 21 | fun onCreate(event: BoardCreateEvent) { 22 | val board = event.board 23 | val player = event.player 24 | 25 | board.wai.plugin.logger.info( 26 | "[Wai] BoardCreateEvent → ${player.name} (tick=${board.tick}, async=${board.isAsync})" 27 | ) 28 | } 29 | 30 | @EventHandler 31 | fun onChange(event: BoardChangeEvent) { 32 | val player = event.player 33 | val oldLines = event.oldLines 34 | val newLines = event.newLines 35 | 36 | event.board.wai.plugin.logger.info( 37 | "[Wai] BoardChangeEvent → ${player.name} changed lines\n" + 38 | " - Old: $oldLines\n" + 39 | " - New: $newLines" 40 | ) 41 | } 42 | 43 | @EventHandler 44 | fun onDestroy(event: BoardDestroyEvent) { 45 | val player = event.player 46 | 47 | event.board.wai.plugin.logger.info("[Wai] BoardDestroyEvent → ${player.name}") 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/listener/BoardListener.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.listener 2 | 3 | import dev.y4irr.coir.scoreboard.Wai 4 | import org.bukkit.Bukkit 5 | import org.bukkit.event.EventHandler 6 | import org.bukkit.event.Listener 7 | import org.bukkit.event.player.PlayerJoinEvent 8 | import org.bukkit.event.player.PlayerQuitEvent 9 | import org.bukkit.event.server.PluginDisableEvent 10 | import java.util.logging.Level 11 | 12 | /* 13 | * This project can't be redistributed without 14 | * authorization of the developer 15 | * 16 | * Project @ CoirLibrary 17 | * @author Yair Soto @ 2025 18 | * Date: month:05 - day:02 19 | */ 20 | 21 | class BoardListener(private val wai: Wai) : Listener { 22 | 23 | @EventHandler 24 | fun onJoin(event: PlayerJoinEvent) { 25 | val player = event.player 26 | 27 | Bukkit.getScheduler().runTaskLater(wai.plugin, { 28 | if (player.isOnline) { 29 | try { 30 | wai.createBoard(player) 31 | } catch (ex: Exception) { 32 | wai.plugin.logger.log(Level.SEVERE, "Failed to create scoreboard for ${player.name}", ex) 33 | } 34 | } 35 | }, 10L) 36 | } 37 | 38 | @EventHandler 39 | fun onQuit(event: PlayerQuitEvent) { 40 | wai.getBoards()[event.player]?.destroy() 41 | } 42 | 43 | @EventHandler 44 | fun onPluginDisable(event: PluginDisableEvent) { 45 | if (event.plugin == wai.plugin) { 46 | wai.stop() 47 | wai.getBoards().values.forEach { it.destroy() } 48 | wai.clearBoards() 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/y4irr/coir/scoreboard/utils/AnnotationUtils.kt: -------------------------------------------------------------------------------- 1 | package dev.y4irr.coir.scoreboard.utils 2 | 3 | /* 4 | * This project can't be redistributed without 5 | * authorization of the developer 6 | * 7 | * Project @ CoirLibrary 8 | * @author Yair Soto @ 2025 9 | * Date: month:05 - day:02 10 | */ 11 | 12 | inline fun <reified T : Annotation> Any.getAnnotation(): T? { 13 | return this.javaClass.getAnnotation(T::class.java) 14 | } --------------------------------------------------------------------------------