├── .gitignore ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── libs ├── GermPlugin-Snapshot-4.2.1.jar └── [插件]DragonCore-2.4.9.6.jar ├── readme.md ├── settings.gradle └── src └── main ├── java └── com │ └── github │ └── playerslotapi │ ├── PlayerSlotAPI.java │ ├── PlayerSlotPlugin.java │ ├── commands │ └── CommandHub.java │ ├── event │ ├── AsyncSlotUpdateEvent.java │ ├── SlotUpdateEvent.java │ └── UpdateTrigger.java │ ├── hook │ ├── DragonCoreHook.java │ ├── GermPluginHook.java │ └── VanillaHook.java │ ├── slot │ ├── PlayerSlot.java │ ├── PlayerSlotCache.java │ └── impl │ │ ├── DragonCoreSlot.java │ │ ├── GermPluginSlot.java │ │ └── VanillaEquipSlot.java │ └── util │ ├── Events.java │ └── Util.java └── resources └── plugin.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea/ 3 | build/ -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.github.johnrengelman.shadow' version '7.1.2' 3 | id 'java' 4 | } 5 | 6 | apply plugin: 'com.github.johnrengelman.shadow' 7 | 8 | group 'com.github.playerslotapi' 9 | version '1.0-Release' 10 | 11 | repositories { 12 | mavenCentral() 13 | maven { 14 | name = 'minecraft-repo' 15 | url = 'https://libraries.minecraft.net/' 16 | } 17 | maven { 18 | url = "https://jitpack.io" 19 | } 20 | maven { 21 | url = "https://repo.extendedclip.com/content/repositories/placeholderapi/" 22 | } 23 | maven { 24 | url = "https://papermc.io/repo/repository/maven-public/" 25 | } 26 | maven { 27 | url = "https://repo.codemc.org/repository/maven-public/" 28 | } 29 | } 30 | 31 | 32 | dependencies { 33 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' 34 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' 35 | // testImplementation("com.github.seeseemelk:MockBukkit:v1.17-SNAPSHOT") 36 | // compileOnly("me.clip:placeholderapi:2.11.1") 37 | // compileOnly("com.github.MilkBowl:VaultAPI:1.7.1") 38 | // compileOnly("com.mojang:authlib:1.5.21") 39 | compileOnly("com.destroystokyo.paper:paper-api:1.16.5-R0.1-SNAPSHOT") 40 | compileOnly(fileTree("libs")) 41 | } 42 | 43 | test { 44 | useJUnitPlatform() 45 | } 46 | 47 | 48 | tasks.withType(JavaCompile) { 49 | options.encoding = "UTF-8" 50 | } 51 | 52 | processResources { 53 | filesMatching("plugin.yml") { 54 | expand("version": version) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why2332742172/PlayerSlotAPI/a2ef84cf7ef77bbe3671978163e0427f97e6930c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /libs/GermPlugin-Snapshot-4.2.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why2332742172/PlayerSlotAPI/a2ef84cf7ef77bbe3671978163e0427f97e6930c/libs/GermPlugin-Snapshot-4.2.1.jar -------------------------------------------------------------------------------- /libs/[插件]DragonCore-2.4.9.6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why2332742172/PlayerSlotAPI/a2ef84cf7ef77bbe3671978163e0427f97e6930c/libs/[插件]DragonCore-2.4.9.6.jar -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # PlayerSlotAPI 2 | 3 | Bukkit 1.9 - 1.18.1 异步装备缓存、信息读取、装备修改、技能触发API 4 | 5 | ## Introduction 6 | 7 | 本API功能 8 | 9 | - 提供信息修改事件,实时捕获玩家装备槽位里物品的变化情况 10 | - 例如穿脱盔甲、切换主手物品、改变副手物品 11 | - 兼容原版六槽位萌芽/龙核和其它自定义槽位 12 | - 提供装备异步修改和应用 13 | 14 | ## API Usage 15 | 16 | - 将本API打包到你的插件内并relocate 17 | - 获取API 18 | 19 | ```java 20 | PlayerSlotAPI slotApi = PlayerSlotAPI.getAPI(); 21 | ``` 22 | 23 | - 注册原版槽位 24 | 25 | ``` 26 | slotApi.registerVanilla(); 27 | ``` 28 | 29 | - 注册龙核/萌芽槽位 30 | 31 | ```java 32 | PlayerSlot petSlot = new DragonCoreSlot("宠物槽位"); 33 | manager.registerSlot(petSlot); 34 | ``` 35 | - 注册完毕以后,如果需要监听装备变动,只需监听两个事件 36 | 37 | - AsyncSlotUpdateEvent:**推荐**,异步装备更新事件,能确保装备准确更新 38 | - SlotUpdateEvent:同步装备更新事件,可以取消(取消对龙核萌芽无效) 39 | - 如果新装备为null,代表插件无法判断新装备会是什么 40 | 41 | - 如果需要获取缓存的槽位装备,可以直接 42 | 43 | ```java 44 | PlayerSlotCache cache = slotApi.getSlotCache(player); 45 | ItemStack item = cache.getItem(petSlot); 46 | ``` 47 | 48 | - 如果要异步修改装备: 49 | 50 | ```java 51 | // 想要修改装备,必须先调用getModifiedItem 52 | ItemStack toModify = cache.getModifiedItem(slot); 53 | // 修改装备,比如把它变得无法破坏 54 | toModify.setUnbreakable(true); 55 | cache.setModifiedItem(toModify); 56 | // 上述流程可以反复进行, 最后应用槽位更改。 如果想要验证时忽略耐久就传入true 57 | cache.applyModification(false); 58 | // 修改不一定能成功,如果用户在异步操作期间改变了他的装备,修改就不会生效(防止刷物品) 59 | ``` 60 | 61 | - 另外,插件支持直接存取萌芽、龙核装备 62 | 63 | ```java 64 | petSlot.get(player, item->{ 65 | if(item==null){ 66 | // 获取失败 67 | return; 68 | } 69 | Bukkit.runTask()->{ 70 | player.getInventory().addItem(item); 71 | } 72 | }) 73 | ``` 74 | 75 | - 高级用法:扩展本框架,读取其它储存的槽位 76 | 77 | ```java 78 | public class MySQLSlot extends PlayerSlot{ 79 | private String 槽位名; 80 | private static SQLDatasource sql = 你的数据源; 81 | // 实现 get set 方法 82 | public void get(Player player, Consumer callback){ 83 | // 获取物品 84 | // 我建议如果是MySQL或者Yml槽位的话这里用异步 85 | if(Bukkit.isPrimaryThread()){ 86 | Bukkit.getScheduler().runTaskAsynchroniously(MyPlugin.getInstance(),()->{ 87 | ItemStack item = sql.get(player, 槽位名); 88 | callback.accept(item); 89 | }); 90 | }else{ 91 | ItemStack item = sql.get(player, 槽位名); 92 | callback.accept(item); 93 | } 94 | } 95 | public void set(Player player, ItemStack item, Consumer callback){ 96 | // 设置物品 97 | if(Bukkit.isPrimaryThread()){ 98 | Bukkit.getScheduler().runTaskAsynchroniously(MyPlugin.getInstance(),()->{ 99 | bollean success = sql.set(player, 槽位名, item); 100 | callback.accept(success); 101 | }); 102 | }else{ 103 | bollean success = sql.set(player, 槽位名, item); 104 | callback.accept(success); 105 | } 106 | } 107 | 108 | // 实现AsyncSafe方法, 告知框架本slot可以异步设置和读取 109 | public boolean isAsyncSafe(){ 110 | return true; 111 | } 112 | } 113 | // 由于是你自己的槽位,你应该在自己设置的时候callEvent 114 | // 在你的设置API处callEvent 115 | public class SQLDatasourcre{ 116 | public void set(Player player, String 槽位名, ItemStack item){ 117 | // 加入下列代码,让框架帮你缓存物品 118 | SlotUpdateEvent event = new SlotUpdateEvent(UpdateTrigger.CUSTOM, player, new MySQLSlot(槽位名), null, item); 119 | event.setUpdateImmediately(); 120 | Bukkit.getPluginManager().callEvent(event); 121 | if(event.isCancelled()){ 122 | return; 123 | } 124 | // ... 125 | sql.setItem(player,槽位名,item); 126 | } 127 | } 128 | ``` 129 | 130 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'PlayerSlotAPI' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/PlayerSlotAPI.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi; 2 | 3 | import com.github.playerslotapi.event.SlotUpdateEvent; 4 | import com.github.playerslotapi.hook.DragonCoreHook; 5 | import com.github.playerslotapi.hook.GermPluginHook; 6 | import com.github.playerslotapi.hook.VanillaHook; 7 | import com.github.playerslotapi.slot.PlayerSlot; 8 | import com.github.playerslotapi.slot.PlayerSlotCache; 9 | import com.github.playerslotapi.slot.impl.DragonCoreSlot; 10 | import com.github.playerslotapi.slot.impl.GermPluginSlot; 11 | import com.github.playerslotapi.slot.impl.VanillaEquipSlot; 12 | import com.github.playerslotapi.util.Events; 13 | import org.bukkit.Bukkit; 14 | import org.bukkit.entity.Player; 15 | import org.bukkit.event.player.*; 16 | import org.bukkit.plugin.Plugin; 17 | 18 | import java.lang.reflect.Field; 19 | import java.util.Map; 20 | import java.util.UUID; 21 | import java.util.concurrent.ConcurrentHashMap; 22 | 23 | /** 24 | * 玩家槽位API工具 25 | * 静态单例, 不可实例化 26 | * 建议自行relocate 27 | */ 28 | public class PlayerSlotAPI { 29 | 30 | private static final GermPluginHook GERM_PLUGIN_HOOK; 31 | private static final DragonCoreHook DRAGON_CORE_HOOK; 32 | private static String PREFIX = "§9[§ePlayerSlotAPI§9]§f"; 33 | 34 | /** 35 | * 核心管理器实例 36 | */ 37 | private static final PlayerSlotAPI API; 38 | /** 39 | * 加载本API的插件 40 | */ 41 | private static final Plugin PLUGIN; 42 | 43 | static { 44 | if (Bukkit.getPluginManager().getPlugin("GermPlugin") != null) { 45 | //萌芽 46 | GERM_PLUGIN_HOOK = new GermPluginHook(); 47 | DragonCoreHook.register(); 48 | Bukkit.getConsoleSender().sendMessage(PREFIX+"已加载GermPlugin作为前置!"); 49 | } else { 50 | GERM_PLUGIN_HOOK = null; 51 | } 52 | if (Bukkit.getPluginManager().getPlugin("DragonCore") != null) { 53 | //龙核 54 | DRAGON_CORE_HOOK = new DragonCoreHook(); 55 | GermPluginHook.register(); 56 | Bukkit.getConsoleSender().sendMessage(PREFIX+"已加载DragonCore作为前置!"); 57 | } else { 58 | DRAGON_CORE_HOOK = null; 59 | } 60 | ClassLoader loader = PlayerSlotAPI.class.getClassLoader(); 61 | try { 62 | Class pluginClassLoader = Class.forName("org.bukkit.plugin.java.PluginClassLoader"); 63 | while (!(pluginClassLoader.isInstance(loader))) { 64 | loader = loader.getParent(); 65 | if (loader == null) { 66 | throw new RuntimeException(PREFIX + "错误:未找到Bukkit插件主类"); 67 | } 68 | } 69 | Field field = pluginClassLoader.getDeclaredField("plugin"); 70 | field.setAccessible(true); 71 | PLUGIN = (Plugin) field.get(loader); 72 | API = new PlayerSlotAPI(); 73 | }catch (Exception e){ 74 | throw new RuntimeException(PREFIX + "错误:未找到Bukkit插件主类"); 75 | } 76 | } 77 | 78 | private final Map SLOT_MAP = new ConcurrentHashMap<>(); 79 | private final Map PLAYER_MAP = new ConcurrentHashMap<>(); 80 | 81 | private PlayerSlotAPI() { 82 | Events.subscribe(PlayerJoinEvent.class, event -> { 83 | PLAYER_MAP.put(event.getPlayer().getUniqueId(), new PlayerSlotCache(event.getPlayer())); 84 | }); 85 | Events.subscribe(PlayerQuitEvent.class, event -> { 86 | PLAYER_MAP.remove(event.getPlayer().getUniqueId()); 87 | }); 88 | Events.subscribe(PlayerKickEvent.class, event -> { 89 | PLAYER_MAP.remove(event.getPlayer().getUniqueId()); 90 | }); 91 | Events.subscribe(SlotUpdateEvent.class, this::onSlotUpdate); 92 | Events.subscribe(PlayerTeleportEvent.class, this::onWorldChange); 93 | Events.subscribe(PlayerRespawnEvent.class, this::onPlayerRespawn); 94 | reload(); 95 | } 96 | 97 | public static GermPluginHook getGermPluginHook() { 98 | return GERM_PLUGIN_HOOK; 99 | } 100 | 101 | public static DragonCoreHook getDragonCoreHook() { 102 | return DRAGON_CORE_HOOK; 103 | } 104 | 105 | public static Plugin getPlugin() { 106 | return PLUGIN; 107 | } 108 | 109 | public static PlayerSlotAPI getAPI() { 110 | return API; 111 | } 112 | 113 | 114 | /** 115 | * 获取所有已经注册的槽位 116 | * 117 | * @return 已经注册的槽位 118 | */ 119 | 120 | public Map getSlotMap() { 121 | return SLOT_MAP; 122 | } 123 | 124 | /** 125 | * 槽位注册 126 | * 127 | * @param slot 要注册的槽位类型 128 | */ 129 | public void registerSlot(PlayerSlot slot) { 130 | if (SLOT_MAP.containsKey(slot.toString())) { 131 | return; 132 | } 133 | if (slot instanceof DragonCoreSlot) { 134 | if (PlayerSlotAPI.DRAGON_CORE_HOOK == null) { 135 | return; 136 | } 137 | SLOT_MAP.put(((DragonCoreSlot) slot).getIdentifier(), slot); 138 | } else if (slot instanceof GermPluginSlot) { 139 | if (PlayerSlotAPI.GERM_PLUGIN_HOOK == null) { 140 | return; 141 | } 142 | SLOT_MAP.put(((GermPluginSlot) slot).getIdentifier(), slot); 143 | } else { 144 | SLOT_MAP.put(slot.toString(), slot); 145 | } 146 | for (PlayerSlotCache cache : PLAYER_MAP.values()) { 147 | cache.initSlot(slot); 148 | } 149 | } 150 | 151 | /** 152 | * 注册原版槽位 153 | */ 154 | public void registerVanilla() { 155 | registerSlot(VanillaEquipSlot.MAINHAND); 156 | registerSlot(VanillaEquipSlot.OFFHAND); 157 | registerSlot(VanillaEquipSlot.HELMET); 158 | registerSlot(VanillaEquipSlot.CHESTPLATE); 159 | registerSlot(VanillaEquipSlot.LEGGINGS); 160 | registerSlot(VanillaEquipSlot.BOOTS); 161 | VanillaHook.registerEvents(); 162 | } 163 | 164 | /** 165 | * 重载槽位缓存 166 | */ 167 | public void reload() { 168 | PLAYER_MAP.clear(); 169 | for (Player player : Bukkit.getOnlinePlayers()) { 170 | PLAYER_MAP.put(player.getUniqueId(), new PlayerSlotCache(player)); 171 | } 172 | } 173 | 174 | public PlayerSlotCache getSlotCache(Player player) { 175 | PlayerSlotCache result = PLAYER_MAP.get(player.getUniqueId()); 176 | if (result == null) { 177 | result = new PlayerSlotCache(player); 178 | PLAYER_MAP.put(player.getUniqueId(), result); 179 | } 180 | return result; 181 | } 182 | 183 | 184 | private void onSlotUpdate(SlotUpdateEvent event) { 185 | PlayerSlotCache cache = getSlotCache(event.getPlayer()); 186 | if (event.isUpdateImmediately()) { 187 | // 如果是forceUpdate, 立即更新无视准确性 188 | cache.updateItem(event.getTrigger(), event.getSlot(), event.getNewItem()); 189 | } else { 190 | // 否则延迟1 tick 检查, 准确更新装备缓存 191 | Bukkit.getScheduler().runTask(PlayerSlotAPI.getPlugin(), () -> { 192 | event.getSlot().get(event.getPlayer(), 193 | item -> cache.updateItem(event.getTrigger(), event.getSlot(), item)); 194 | }); 195 | } 196 | } 197 | 198 | private void onPlayerRespawn(PlayerRespawnEvent event) { 199 | Bukkit.getScheduler().runTaskLater(PlayerSlotAPI.getPlugin(), () -> getSlotCache(event.getPlayer()).updateAll(), 1L); 200 | } 201 | 202 | private void onWorldChange(PlayerTeleportEvent event) { 203 | Bukkit.getScheduler().runTaskLater(PlayerSlotAPI.getPlugin(), () -> getSlotCache(event.getPlayer()).updateAll(), 1L); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/PlayerSlotPlugin.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi; 2 | 3 | import com.github.playerslotapi.commands.CommandHub; 4 | import com.github.playerslotapi.event.AsyncSlotUpdateEvent; 5 | import com.github.playerslotapi.event.SlotUpdateEvent; 6 | import com.github.playerslotapi.util.Events; 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.command.PluginCommand; 9 | import org.bukkit.plugin.java.JavaPlugin; 10 | 11 | public class PlayerSlotPlugin extends JavaPlugin { 12 | 13 | private static PlayerSlotPlugin instance; 14 | private static PlayerSlotAPI slotApi; 15 | 16 | public static PlayerSlotPlugin getInstance() { 17 | return instance; 18 | } 19 | 20 | 21 | @Override 22 | public void onLoad() { 23 | instance = this; 24 | } 25 | 26 | @Override 27 | public void onEnable() { 28 | printInfo(); 29 | slotApi = PlayerSlotAPI.getAPI(); 30 | slotApi.registerVanilla(); 31 | slotApi.reload(); 32 | //测试命令 33 | PluginCommand command = getCommand("psapi"); 34 | if (command != null) { 35 | command.setExecutor(new CommandHub()); 36 | } 37 | Events.subscribe(AsyncSlotUpdateEvent.class, event -> { 38 | event.getPlayer().sendMessage("异步装备更新 - " 39 | + event.getSlot().toString() + ": " 40 | + event.getOldItem().getType().toString() + "*" + event.getOldItem().getAmount() 41 | + " -> " 42 | + event.getNewItem().getType().toString() + "*" + event.getNewItem().getAmount()); 43 | }); 44 | Events.subscribe(SlotUpdateEvent.class, event -> { 45 | event.getPlayer().sendMessage("同步装备更新 - " + event.getSlot().toString() + ": " 46 | + event.getOldItem().getType().toString() + "*" + event.getOldItem().getAmount() + 47 | " -> " + (event.getNewItem() == null ? "无法判断" : 48 | event.getNewItem().getType().toString() + "*" + event.getNewItem().getAmount())); 49 | }); 50 | } 51 | 52 | @Override 53 | public void onDisable() { 54 | Bukkit.getConsoleSender().sendMessage("§9=============================================================="); 55 | Bukkit.getConsoleSender().sendMessage("§9Disabling PlayerSlotApi"); 56 | Bukkit.getConsoleSender().sendMessage("§9=============================================================="); 57 | } 58 | 59 | private void printInfo() { 60 | Bukkit.getConsoleSender().sendMessage("§9=============================================================="); 61 | Bukkit.getConsoleSender().sendMessage("§9Enabling PlayerSlotApi"); 62 | Bukkit.getConsoleSender().sendMessage("§9=============================================================="); 63 | Bukkit.getConsoleSender().sendMessage("§9[§ePlayerSlotAPI§9]§fLoading..."); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/commands/CommandHub.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.commands; 2 | 3 | import com.github.playerslotapi.PlayerSlotPlugin; 4 | import com.github.playerslotapi.slot.impl.DragonCoreSlot; 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.Material; 7 | import org.bukkit.command.Command; 8 | import org.bukkit.command.CommandExecutor; 9 | import org.bukkit.command.CommandSender; 10 | import org.bukkit.entity.Player; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | 14 | public class CommandHub implements CommandExecutor { 15 | 16 | //测试命令 17 | @Override 18 | public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { 19 | Player senderPlayer = (Player) sender; 20 | if (args.length < 3) { 21 | senderPlayer.sendMessage("参数数量不正确"); 22 | senderPlayer.sendMessage("psapi set/get player identity"); 23 | return true; 24 | } 25 | Player targetPlayer = Bukkit.getPlayer(args[1]); 26 | if (targetPlayer == null) { 27 | return true; 28 | } 29 | String identifier = args[2]; 30 | DragonCoreSlot slot = new DragonCoreSlot(identifier); 31 | if (args[0].equalsIgnoreCase("set")) { 32 | slot.set(targetPlayer, senderPlayer.getInventory().getItemInMainHand(), result -> { 33 | senderPlayer.sendMessage(result ? "设置成功" : "设置失败"); 34 | }); 35 | } else if (args[0].equalsIgnoreCase("get")) { 36 | slot.get(targetPlayer, a -> { 37 | if (a == null) { 38 | senderPlayer.sendMessage("获取失败"); 39 | } else if (a.getType() == Material.AIR) { 40 | senderPlayer.sendMessage("该槽位物品为空气"); 41 | } else { 42 | senderPlayer.sendMessage(a.toString()); 43 | if (Bukkit.isPrimaryThread()) { 44 | senderPlayer.getInventory().addItem(a); 45 | } else { 46 | Bukkit.getScheduler().runTask(PlayerSlotPlugin.getInstance(), () -> { 47 | senderPlayer.getInventory().addItem(a); 48 | }); 49 | } 50 | } 51 | }); 52 | } 53 | return true; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/event/AsyncSlotUpdateEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.event; 2 | 3 | import com.github.playerslotapi.slot.PlayerSlot; 4 | import org.bukkit.entity.Player; 5 | import org.bukkit.event.Event; 6 | import org.bukkit.event.HandlerList; 7 | import org.bukkit.inventory.ItemStack; 8 | 9 | /** 10 | * 异步槽位更新事件. 缓存更新时触发 11 | * 1 tick后触发, 使得槽位更新捕捉准确 12 | * 这个事件不能取消, 仅作为通知使用 13 | */ 14 | public class AsyncSlotUpdateEvent extends Event { 15 | private static final HandlerList HANDLERS = new HandlerList(); 16 | /** 17 | * 触发原因 18 | */ 19 | private final UpdateTrigger trigger; 20 | /** 21 | * 玩家 22 | */ 23 | private final Player player; 24 | /** 25 | * 更新的槽位 26 | */ 27 | private final PlayerSlot slot; 28 | /** 29 | * 旧装备的副本 30 | */ 31 | private final ItemStack oldItem; 32 | 33 | /** 34 | * 新装备的副本 35 | */ 36 | private final ItemStack newItem; 37 | 38 | 39 | public AsyncSlotUpdateEvent(UpdateTrigger trigger, Player player, PlayerSlot slot, ItemStack oldItem, ItemStack newItem) { 40 | super(true); 41 | this.trigger = trigger; 42 | this.player = player; 43 | this.slot = slot; 44 | this.oldItem = oldItem; 45 | this.newItem = newItem; 46 | } 47 | 48 | public static HandlerList getHandlerList() { 49 | return HANDLERS; 50 | } 51 | 52 | public UpdateTrigger getTrigger() { 53 | return trigger; 54 | } 55 | 56 | public Player getPlayer() { 57 | return player; 58 | } 59 | 60 | public PlayerSlot getSlot() { 61 | return slot; 62 | } 63 | 64 | public ItemStack getOldItem() { 65 | return oldItem; 66 | } 67 | 68 | public ItemStack getNewItem() { 69 | return newItem; 70 | } 71 | 72 | @Override 73 | public HandlerList getHandlers() { 74 | return HANDLERS; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/event/SlotUpdateEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.event; 2 | 3 | import com.github.playerslotapi.slot.PlayerSlot; 4 | import org.bukkit.Material; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.HandlerList; 8 | import org.bukkit.event.player.PlayerEvent; 9 | import org.bukkit.inventory.ItemStack; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | /** 13 | * 装备更新事件 14 | * 当实际槽位可能发生装备更新时触发 15 | */ 16 | public class SlotUpdateEvent extends PlayerEvent implements Cancellable { 17 | private static final HandlerList HANDLERS = new HandlerList(); 18 | /** 19 | * 触发原因 20 | */ 21 | private final UpdateTrigger trigger; 22 | /** 23 | * 触发槽位 24 | */ 25 | private final PlayerSlot slot; 26 | /** 27 | * 槽位中原先的装备 28 | */ 29 | private final ItemStack oldItem; 30 | /** 31 | * 槽位中的新装备. 当且仅当API不能事先得知槽位中是什么装备时, 这个物品为null 32 | */ 33 | private final ItemStack newItem; 34 | /** 35 | * 取消状态. 取消时槽位装备将不会发生更新 36 | */ 37 | private boolean isCancelled; 38 | 39 | /** 40 | * 是否立即更新. 对于原版槽位, 延时更新能使得更新更准确 41 | * 但对于自定义槽位而言就没有必要了 42 | */ 43 | private boolean immediate = false; 44 | 45 | public SlotUpdateEvent(UpdateTrigger trigger, Player player, PlayerSlot slot, ItemStack oldItem, ItemStack newItem) { 46 | super(player); 47 | this.trigger = trigger; 48 | this.slot = slot; 49 | this.oldItem = oldItem; 50 | this.newItem = newItem; 51 | } 52 | 53 | public static HandlerList getHandlerList() { 54 | return HANDLERS; 55 | } 56 | 57 | public ItemStack getOldItem() { 58 | return oldItem == null ? new ItemStack(Material.AIR) : oldItem; 59 | } 60 | 61 | @Nullable 62 | public ItemStack getNewItem() { 63 | return newItem; 64 | } 65 | 66 | public UpdateTrigger getTrigger() { 67 | return trigger; 68 | } 69 | 70 | public PlayerSlot getSlot() { 71 | return slot; 72 | } 73 | 74 | public boolean isUpdateImmediately() { 75 | return immediate; 76 | } 77 | 78 | public void setUpdateImmediately() { 79 | immediate = true; 80 | } 81 | 82 | @Override 83 | public boolean isCancelled() { 84 | return isCancelled; 85 | } 86 | 87 | @Override 88 | public void setCancelled(boolean b) { 89 | isCancelled = b; 90 | } 91 | 92 | @Override 93 | public HandlerList getHandlers() { 94 | return HANDLERS; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/event/UpdateTrigger.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.event; 2 | 3 | public enum UpdateTrigger { 4 | /** 5 | * Update by Initialize 6 | */ 7 | INIT, 8 | 9 | /** 10 | * Update by PlayerSlotAPI set 11 | */ 12 | SET, 13 | 14 | /** 15 | * When admin use command to edit equip 16 | */ 17 | COMMAND, 18 | 19 | /** 20 | * When you use /hat to put on a hat 21 | */ 22 | COMMAND_HAT, 23 | /** 24 | * When you pickup an item 25 | */ 26 | PICKUP, 27 | /** 28 | * When you shift click an armor piece to equip or unequip 29 | */ 30 | SHIFT_CLICK, 31 | /** 32 | * When you drag and drop the item to equip or unequip 33 | */ 34 | DRAG, 35 | /** 36 | * When you are damaged 37 | */ 38 | DAMAGED, 39 | /** 40 | * When you manually equip or unequip the item. Use to be DRAG 41 | */ 42 | PICK_DROP, 43 | /** 44 | * When you press the hotbar slot number to change your held slot 45 | */ 46 | HELD, 47 | /** 48 | * When you press F to swap your main/off hand item 49 | */ 50 | SWAP, 51 | /** 52 | * When you right click an armor piece in the hotbar without the inventory open to equip. 53 | */ 54 | HOTBAR, 55 | /** 56 | * When you press the hotbar slot number while hovering over the armor slot to equip or unequip 57 | */ 58 | HOTBAR_SWAP, 59 | /** 60 | * When you use item in hand or offhand 61 | */ 62 | USE, 63 | /** 64 | * When in range of a dispenser that shoots an armor piece to equip.
65 | * Requires the spigot version to have {@link org.bukkit.event.block.BlockDispenseArmorEvent} implemented. 66 | */ 67 | DISPENSER, 68 | /** 69 | * When an armor piece is removed due to it losing all durability. 70 | */ 71 | BROKE, 72 | /** 73 | * When you die causing all armor to unequip 74 | */ 75 | DEATH, 76 | /** 77 | * When you drop your item in main hand 78 | */ 79 | DROP, 80 | /** 81 | * Check for special purpose 82 | */ 83 | CUSTOM, 84 | /** 85 | * DragonCore slot 86 | */ 87 | DRAGON_CORE, 88 | /** 89 | * GermPlugin slot 90 | */ 91 | GERM_PLUGIN 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/hook/DragonCoreHook.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.hook; 2 | 3 | 4 | import com.github.playerslotapi.PlayerSlotAPI; 5 | import com.github.playerslotapi.event.SlotUpdateEvent; 6 | import com.github.playerslotapi.event.UpdateTrigger; 7 | import com.github.playerslotapi.slot.PlayerSlot; 8 | import com.github.playerslotapi.util.Events; 9 | import com.github.playerslotapi.util.Util; 10 | import eos.moe.dragoncore.DragonCore; 11 | import eos.moe.dragoncore.api.event.PlayerSlotUpdateEvent; 12 | import eos.moe.dragoncore.database.IDataBase; 13 | import eos.moe.dragoncore.network.PacketSender; 14 | import org.bukkit.Bukkit; 15 | import org.bukkit.Material; 16 | import org.bukkit.entity.Player; 17 | import org.bukkit.inventory.ItemStack; 18 | 19 | import java.util.function.Consumer; 20 | 21 | public class DragonCoreHook { 22 | 23 | private final DragonCore instance; 24 | 25 | public DragonCoreHook() { 26 | this.instance = DragonCore.getInstance(); 27 | } 28 | 29 | public static void register() { 30 | Events.subscribe(PlayerSlotUpdateEvent.class, event -> { 31 | ItemStack newItem = event.getItemStack(); 32 | String identifier = event.getIdentifier(); 33 | //龙核特有傻逼 进服时identifier是null 34 | if (identifier != null) { 35 | if (PlayerSlotAPI.getAPI().getSlotMap().containsKey(identifier)) { 36 | PlayerSlot slot = PlayerSlotAPI.getAPI().getSlotMap().get(identifier); 37 | SlotUpdateEvent update = new SlotUpdateEvent(UpdateTrigger.DRAGON_CORE, event.getPlayer(), slot, null, newItem); 38 | update.setUpdateImmediately(); 39 | Bukkit.getPluginManager().callEvent(update); 40 | } 41 | } 42 | }); 43 | } 44 | 45 | public void getItemFromSlot(Player player, String identifier, Consumer callback) { 46 | instance.getDB().getData(player, identifier, new IDataBase.Callback() { 47 | @Override 48 | public void onResult(ItemStack itemStack) { 49 | if (!Util.isAir(itemStack)) { 50 | //有物品则返回该物品 51 | callback.accept(itemStack); 52 | } else { 53 | callback.accept(new ItemStack(Material.AIR)); 54 | } 55 | } 56 | 57 | @Override 58 | public void onFail() { 59 | callback.accept(null); 60 | } 61 | }); 62 | } 63 | 64 | public void setItemToSlot(Player player, String identifier, ItemStack toBePuttedItem, Consumer callback) { 65 | instance.getDB().setData(player, identifier, toBePuttedItem, new IDataBase.Callback() { 66 | @Override 67 | public void onResult(ItemStack itemStack) { 68 | if (toBePuttedItem != null) { 69 | PacketSender.putClientSlotItem(player, identifier, toBePuttedItem); 70 | } else { 71 | //为null代表删除该槽位物品 72 | PacketSender.putClientSlotItem(player, identifier, new ItemStack(Material.AIR)); 73 | } 74 | callback.accept(true); 75 | } 76 | 77 | @Override 78 | public void onFail() { 79 | Bukkit.getConsoleSender().sendMessage("§9[§ePlayerSlotAPI§9]§f 为玩家:" + player + "的" + identifier + "槽位设置物品时出错"); 80 | callback.accept(false); 81 | } 82 | }); 83 | } 84 | 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/hook/GermPluginHook.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.hook; 2 | 3 | import com.germ.germplugin.GermPlugin; 4 | import com.germ.germplugin.api.GermPacketAPI; 5 | import com.germ.germplugin.api.GermSlotAPI; 6 | import com.germ.germplugin.api.event.gui.GermGuiSlotPreClickEvent; 7 | import com.germ.germplugin.api.event.gui.GermGuiSlotSavedEvent; 8 | import com.github.playerslotapi.PlayerSlotAPI; 9 | import com.github.playerslotapi.event.SlotUpdateEvent; 10 | import com.github.playerslotapi.event.UpdateTrigger; 11 | import com.github.playerslotapi.slot.PlayerSlot; 12 | import com.github.playerslotapi.slot.impl.GermPluginSlot; 13 | import com.github.playerslotapi.util.Events; 14 | import org.bukkit.Bukkit; 15 | import org.bukkit.entity.Player; 16 | import org.bukkit.inventory.ItemStack; 17 | 18 | import java.util.Map; 19 | 20 | public class GermPluginHook { 21 | 22 | public GermPlugin instance; 23 | 24 | public GermPluginHook() { 25 | this.instance = GermPlugin.getPlugin(); 26 | } 27 | 28 | public static void register() { 29 | Map slotMap = PlayerSlotAPI.getAPI().getSlotMap(); 30 | try { 31 | Events.subscribe(GermGuiSlotSavedEvent.class, event -> { 32 | String identifier = event.getIdentity(); 33 | if (slotMap.containsKey(identifier)) { 34 | PlayerSlot slot = slotMap.get(identifier); 35 | SlotUpdateEvent update = new SlotUpdateEvent(UpdateTrigger.GERM_PLUGIN, event.getPlayer(), slot, event.getOldItemStack(), event.getNewItemStack()); 36 | update.setUpdateImmediately(); 37 | Bukkit.getPluginManager().callEvent(update); 38 | } 39 | }); 40 | GermPluginSlot.disableCacheUpdate(); 41 | } catch (Throwable e) { 42 | Events.subscribe(GermGuiSlotPreClickEvent.class, event -> { 43 | // 旧版萌芽获取不到新旧物品, 因此需要延时检测 44 | String identifier = event.getSlotIdentity(); 45 | if (slotMap.containsKey(identifier)) { 46 | PlayerSlot slot = slotMap.get(identifier); 47 | ItemStack oldItem = event.getSlot(); 48 | ItemStack newItem = event.getCursor(); 49 | if (oldItem.equals(newItem)) { 50 | return; 51 | } 52 | SlotUpdateEvent update = new SlotUpdateEvent(UpdateTrigger.GERM_PLUGIN, event.getPlayer(), slot, oldItem, newItem); 53 | Bukkit.getPluginManager().callEvent(update); 54 | if (update.isCancelled()) { 55 | event.setCancelled(true); 56 | } 57 | } 58 | }); 59 | } 60 | } 61 | 62 | public ItemStack getItemFromSlot(Player player, String identity) { 63 | return GermSlotAPI.getItemStackFromIdentity(player, identity); 64 | } 65 | 66 | public void setItemToSlot(Player player, String identifier, ItemStack toBePuttedItem) { 67 | GermSlotAPI.saveItemStackToIdentity(player, identifier, toBePuttedItem); 68 | GermPacketAPI.sendSlotItemStack(player, identifier, toBePuttedItem); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/hook/VanillaHook.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.hook; 2 | 3 | 4 | import com.github.playerslotapi.event.SlotUpdateEvent; 5 | import com.github.playerslotapi.event.UpdateTrigger; 6 | import com.github.playerslotapi.slot.PlayerSlot; 7 | import com.github.playerslotapi.slot.impl.VanillaEquipSlot; 8 | import com.github.playerslotapi.util.Events; 9 | import org.bukkit.Bukkit; 10 | import org.bukkit.Material; 11 | import org.bukkit.entity.Player; 12 | import org.bukkit.event.Event.Result; 13 | import org.bukkit.event.EventPriority; 14 | import org.bukkit.event.block.Action; 15 | import org.bukkit.event.block.BlockDispenseArmorEvent; 16 | import org.bukkit.event.inventory.*; 17 | import org.bukkit.event.inventory.InventoryType.SlotType; 18 | import org.bukkit.event.player.*; 19 | import org.bukkit.inventory.EquipmentSlot; 20 | import org.bukkit.inventory.ItemStack; 21 | import org.bukkit.inventory.PlayerInventory; 22 | 23 | import java.util.Map; 24 | import java.util.Set; 25 | 26 | import static com.github.playerslotapi.util.Util.isAir; 27 | import static com.github.playerslotapi.util.Util.newHashSet; 28 | 29 | 30 | public class VanillaHook { 31 | 32 | private static final Set BLOCKED_MATERIALS = newHashSet( 33 | "FURNACE", "CHEST", "TRAPPED_CHEST", "BEACON", "DISPENSER", "DROPPER", "HOPPER", 34 | "WORKBENCH", "ENCHANTMENT_TABLE", "ENDER_CHEST", "ANVIL", "BED_BLOCK", "FENCE_GATE", 35 | "SPRUCE_FENCE_GATE", "BIRCH_FENCE_GATE", "ACACIA_FENCE_GATE", "JUNGLE_FENCE_GATE", 36 | "DARK_OAK_FENCE_GATE", "IRON_DOOR_BLOCK", "WOODEN_DOOR", "SPRUCE_DOOR", "BIRCH_DOOR", 37 | "JUNGLE_DOOR", "ACACIA_DOOR", "DARK_OAK_DOOR", "WOOD_BUTTON", "STONE_BUTTON", "TRAP_DOOR", 38 | "IRON_TRAPDOOR", "DIODE_BLOCK_OFF", "DIODE_BLOCK_ON", "REDSTONE_COMPARATOR_OFF", 39 | "REDSTONE_COMPARATOR_ON", "FENCE", "SPRUCE_FENCE", "BIRCH_FENCE", "JUNGLE_FENCE", 40 | "DARK_OAK_FENCE", "ACACIA_FENCE", "NETHER_FENCE", "BREWING_STAND", "CAULDRON", 41 | "LEGACY_SIGN_POST", "LEGACY_WALL_SIGN", "LEGACY_SIGN", "ACACIA_SIGN", "ACACIA_WALL_SIGN", 42 | "BIRCH_SIGN", "BIRCH_WALL_SIGN", "DARK_OAK_SIGN", "DARK_OAK_WALL_SIGN", "JUNGLE_SIGN", 43 | "JUNGLE_WALL_SIGN", "OAK_SIGN", "OAK_WALL_SIGN", "SPRUCE_SIGN", "SPRUCE_WALL_SIGN", "LEVER", 44 | "BLACK_SHULKER_BOX", "BLUE_SHULKER_BOX", "BROWN_SHULKER_BOX", "CYAN_SHULKER_BOX", 45 | "GRAY_SHULKER_BOX", "GREEN_SHULKER_BOX", "LIGHT_BLUE_SHULKER_BOX", "LIME_SHULKER_BOX", 46 | "MAGENTA_SHULKER_BOX", "ORANGE_SHULKER_BOX", "PINK_SHULKER_BOX", "PURPLE_SHULKER_BOX", 47 | "RED_SHULKER_BOX", "SILVER_SHULKER_BOX", "WHITE_SHULKER_BOX", "YELLOW_SHULKER_BOX", 48 | "DAYLIGHT_DETECTOR_INVERTED", "DAYLIGHT_DETECTOR", "BARREL", "BLAST_FURNACE", "SMOKER", 49 | "CARTOGRAPHY_TABLE", "COMPOSTER", "GRINDSTONE", "LECTERN", "LOOM", "STONECUTTER", "BELL", 50 | "BEEHIVE" 51 | ); 52 | private static boolean disableDropDetect = false; 53 | 54 | /** 55 | * 工具函数,用于同时触发槽位预更新和后更新事件 56 | * 57 | * @param trigger 触发原因 58 | * @param player 玩家 59 | * @param slot 槽位 60 | * @param oldItem 老物品 61 | * @param newItem 新物品 62 | * @return 返回槽位更新事件 63 | */ 64 | private static boolean cancelSlotUpdate(UpdateTrigger trigger, Player player, PlayerSlot slot, ItemStack oldItem, ItemStack newItem) { 65 | if (oldItem == null) { 66 | oldItem = new ItemStack(Material.AIR); 67 | } 68 | if (oldItem.equals(newItem)) { 69 | return false; 70 | } 71 | SlotUpdateEvent update = new SlotUpdateEvent(trigger, player, slot, oldItem, newItem); 72 | Bukkit.getPluginManager().callEvent(update); 73 | return update.isCancelled(); 74 | } 75 | 76 | /** 77 | * 订阅所有事件 78 | */ 79 | @SuppressWarnings("deprecation") 80 | public static void registerEvents() { 81 | Events.subscribe(InventoryClickEvent.class, VanillaHook::onInventoryClick); 82 | Events.subscribe(InventoryDragEvent.class, VanillaHook::onInventoryDrag); 83 | Events.subscribe(InventoryCloseEvent.class, VanillaHook::onInventoryClose); 84 | Events.subscribe(PlayerCommandPreprocessEvent.class, VanillaHook::onCommand); 85 | Events.subscribe(PlayerDropItemEvent.class, VanillaHook::onPlayerDropItem); 86 | Events.subscribe(PlayerItemHeldEvent.class, VanillaHook::onPlayerItemHeld); 87 | Events.subscribe(PlayerSwapHandItemsEvent.class, VanillaHook::onPlayerSwapItem); 88 | Events.subscribe(PlayerPickupItemEvent.class, VanillaHook::onPlayerPickupItem); 89 | Events.subscribe(PlayerItemDamageEvent.class, VanillaHook::onPlayerItemDamage); 90 | Events.subscribe(PlayerInteractEvent.class, EventPriority.HIGHEST, false, VanillaHook::onPlayerInteract); 91 | Events.subscribe(PlayerItemBreakEvent.class, EventPriority.MONITOR, false, VanillaHook::onPlayerItemBreak); 92 | try { 93 | Events.subscribe(BlockDispenseArmorEvent.class, event -> { 94 | VanillaEquipSlot slot = VanillaEquipSlot.matchType(event.getItem()); 95 | if (slot != null && event.getTargetEntity() instanceof Player) { 96 | Player player = (Player) event.getTargetEntity(); 97 | if (cancelSlotUpdate(UpdateTrigger.DISPENSER, player, slot, slot.get(player), event.getItem())) { 98 | event.setCancelled(true); 99 | } 100 | } 101 | }); 102 | } catch (Throwable ignored) { 103 | 104 | } 105 | } 106 | 107 | /** 108 | * 检查玩家点击造成的装备和主手副手更新 109 | * 110 | * @param event GUI点击事件 111 | */ 112 | private static void onInventoryClick(final InventoryClickEvent event) { 113 | if (event.getAction() == InventoryAction.NOTHING) { 114 | // 点击无事发生 直接返回 115 | return; 116 | } 117 | if (!(event.getWhoClicked() instanceof Player)) { 118 | return; 119 | } 120 | if (event.getClickedInventory() == null) { 121 | // 点击栏外 直接返回 122 | if (event.getAction() == InventoryAction.DROP_ALL_CURSOR || event.getAction() == InventoryAction.DROP_ONE_CURSOR) { 123 | disableDropDetect = true; 124 | } 125 | return; 126 | } 127 | if (event.getAction() == InventoryAction.DROP_ALL_SLOT || event.getAction() == InventoryAction.DROP_ONE_SLOT) { 128 | // 鼠标有物品的时候按Q丢不了东西, InventoryAction却又不是NOTHING 129 | // 此乃Bukkit缺陷, 需自己解决 130 | if (!isAir(event.getCursor())) { 131 | return; 132 | } 133 | disableDropDetect = true; 134 | } 135 | boolean shift = false; 136 | boolean numberkey = false; 137 | if (event.getClick().equals(ClickType.SHIFT_LEFT) || event.getClick().equals(ClickType.SHIFT_RIGHT)) { 138 | shift = true; 139 | } 140 | if (event.getClick().equals(ClickType.NUMBER_KEY)) { 141 | numberkey = true; 142 | } 143 | Player player = (Player) event.getWhoClicked(); 144 | // 处理主手变化情况 145 | if (numberkey) { 146 | // 数字键切换, 来源去向都检查 147 | // 数字键可能同时产生主手变化和装备变化 此处只检查主手 148 | int hotbarId = event.getHotbarButton(); 149 | int targetId = event.getSlot(); 150 | if (hotbarId == player.getInventory().getHeldItemSlot()) { 151 | if (cancelSlotUpdate(UpdateTrigger.HOTBAR_SWAP, player, VanillaEquipSlot.MAINHAND, player.getInventory().getItem(hotbarId), event.getCurrentItem())) { 152 | event.setCancelled(true); 153 | return; 154 | } 155 | } else if (targetId == player.getInventory().getHeldItemSlot()) { 156 | if (cancelSlotUpdate(UpdateTrigger.HOTBAR_SWAP, player, VanillaEquipSlot.MAINHAND, event.getCurrentItem(), player.getInventory().getItem(hotbarId))) { 157 | event.setCancelled(true); 158 | return; 159 | } 160 | } 161 | } else { 162 | // 直接点击 163 | // shift点击储存区和副手时,有可能直接把装备送进格子里,从而无需检查主手 164 | // 直接在这里处理shift左键快捷装备, 如果装备了通知主副手然后短路处理 165 | boolean equipped = false; 166 | if (shift && event.getClickedInventory().getType() == InventoryType.PLAYER) { 167 | VanillaEquipSlot quickEquipSlot = VanillaEquipSlot.matchType(event.getCurrentItem()); 168 | // 成功装备上的情况 169 | if (quickEquipSlot != null && isAir(quickEquipSlot.get(player)) && (event.getSlotType() == SlotType.CONTAINER || event.getSlotType() == SlotType.QUICKBAR)) { 170 | if (cancelSlotUpdate(UpdateTrigger.SHIFT_CLICK, player, quickEquipSlot, null, event.getCurrentItem())) { 171 | event.setCancelled(true); 172 | return; 173 | } 174 | // 通知副手 准备短路处理 175 | if (event.getRawSlot() == 45 && cancelSlotUpdate(UpdateTrigger.SHIFT_CLICK, player, VanillaEquipSlot.OFFHAND, event.getCurrentItem(), new ItemStack(Material.AIR))) { 176 | event.setCancelled(true); 177 | return; 178 | } 179 | equipped = true; 180 | } 181 | } 182 | if (event.getSlotType() == SlotType.QUICKBAR && (event.getRawSlot() != 45 || event.getClickedInventory().getType() != InventoryType.PLAYER)) { 183 | // 点击热键栏且不是副手位置 检查 184 | if (event.getSlot() == player.getInventory().getHeldItemSlot()) { 185 | // 如果点击的是主手位置 186 | // 如果按了shift, 物品栏里面又没有相似物品或者空位, 返回 187 | if (shift) { 188 | if (!equipped) { 189 | ItemStack mainHandItem = player.getInventory().getItemInMainHand(); 190 | int left = mainHandItem.getAmount(); 191 | for (int i = 9; i < 36; i++) { 192 | ItemStack possible = player.getInventory().getItem(i); 193 | if (isAir(possible)) { 194 | left = 0; 195 | break; 196 | } 197 | if (possible.isSimilar(mainHandItem) && possible.getAmount() < possible.getMaxStackSize()) { 198 | left -= (possible.getMaxStackSize() - possible.getAmount()); 199 | if (left <= 0) { 200 | left = 0; 201 | break; 202 | } 203 | } 204 | } 205 | if (left == mainHandItem.getAmount()) { 206 | return; 207 | } 208 | ItemStack newItem; 209 | if (left == 0) { 210 | newItem = new ItemStack(Material.AIR); 211 | } else { 212 | newItem = mainHandItem.clone(); 213 | newItem.setAmount(left); 214 | } 215 | if (cancelSlotUpdate(UpdateTrigger.SHIFT_CLICK, player, VanillaEquipSlot.MAINHAND, event.getCurrentItem(), newItem)) { 216 | event.setCancelled(true); 217 | } 218 | } else if (cancelSlotUpdate(UpdateTrigger.SHIFT_CLICK, player, VanillaEquipSlot.MAINHAND, event.getCurrentItem(), new ItemStack(Material.AIR))) { 219 | event.setCancelled(true); 220 | } 221 | } else { 222 | ItemStack newItem; 223 | if (event.getAction() == InventoryAction.DROP_ONE_SLOT) { 224 | newItem = event.getCurrentItem().clone(); 225 | newItem.setAmount(newItem.getAmount() - 1); 226 | } else if (event.getAction() == InventoryAction.DROP_ALL_SLOT) { 227 | newItem = new ItemStack(Material.AIR); 228 | } else { 229 | newItem = event.getCursor(); 230 | } 231 | if (cancelSlotUpdate(UpdateTrigger.PICK_DROP, player, VanillaEquipSlot.MAINHAND, event.getCurrentItem(), 232 | newItem)) { 233 | event.setCancelled(true); 234 | } 235 | } 236 | return; 237 | } 238 | } else if (shift && !equipped) { 239 | // 检查点击热键栏之外的位置是否把物品送到了主手 240 | // 之前检查过快捷装备, 如果快捷装备成功此处就不检查了 241 | ItemStack mainHandItem = player.getInventory().getItemInMainHand(); 242 | ItemStack item = event.getCurrentItem(); 243 | // 如果主手物品为空或和点击物品相似 则进行检查 244 | if (isAir(mainHandItem) || mainHandItem.isSimilar(item) && mainHandItem.getMaxStackSize() > mainHandItem.getAmount()) { 245 | PlayerInventory inventory = player.getInventory(); 246 | int mainHandIndex = inventory.getHeldItemSlot(); 247 | int amount = item.getAmount(); 248 | // 如果是副手点击则先填充Container的东西 249 | boolean abort = false; 250 | if (event.getClickedInventory().getType() == InventoryType.PLAYER && event.getRawSlot() == 45) { 251 | for (int i = 9; i < 36; i++) { 252 | ItemStack possible = inventory.getItem(i); 253 | if (item.isSimilar(possible) && possible.getMaxStackSize() > possible.getAmount()) { 254 | amount -= (possible.getMaxStackSize() - possible.getAmount()); 255 | if (amount <= 0) { 256 | abort = true; 257 | break; 258 | } 259 | } 260 | } 261 | } 262 | // 填充主手前面的快捷栏 263 | if (!abort) { 264 | for (int i = 0; i < mainHandIndex; i++) { 265 | ItemStack possible = inventory.getItem(i); 266 | if (item.isSimilar(possible) && possible.getMaxStackSize() > possible.getAmount()) { 267 | amount -= (possible.getMaxStackSize() - possible.getAmount()); 268 | if (amount <= 0) { 269 | abort = true; 270 | break; 271 | } 272 | } 273 | } 274 | } 275 | // 如果主手是空气 276 | if (!abort && isAir(mainHandItem)) { 277 | // 接着填充快捷栏 278 | for (int i = mainHandIndex + 1; i < 9; i++) { 279 | ItemStack possible = inventory.getItem(i); 280 | if (item.isSimilar(possible) && possible.getMaxStackSize() > possible.getAmount()) { 281 | amount -= (possible.getMaxStackSize() - possible.getAmount()); 282 | if (amount <= 0) { 283 | abort = true; 284 | break; 285 | } 286 | } 287 | } 288 | // 如果点击了副手槽位且储存区有空气 放弃 289 | if (!abort && event.getClickedInventory().getType() == InventoryType.PLAYER && event.getRawSlot() == 45) { 290 | for (int i = 9; i < 36; i++) { 291 | if (isAir(inventory.getItem(i))) { 292 | abort = true; 293 | break; 294 | } 295 | } 296 | } 297 | // 如果主手前面有空气 放弃 298 | if (!abort) { 299 | for (int i = 0; i < mainHandIndex; i++) { 300 | if (isAir(inventory.getItem(i))) { 301 | abort = true; 302 | break; 303 | } 304 | } 305 | } 306 | } 307 | // 如果没有放弃则触发事件 308 | if (!abort) { 309 | ItemStack newItem = item.clone(); 310 | newItem.setAmount(Math.min((isAir(mainHandItem) ? 0 : mainHandItem.getAmount()) + amount, mainHandItem.getMaxStackSize())); 311 | if (cancelSlotUpdate(UpdateTrigger.PICKUP, player, VanillaEquipSlot.MAINHAND, mainHandItem, newItem)) { 312 | event.setCancelled(true); 313 | return; 314 | } 315 | } 316 | } 317 | } 318 | // 如果是shift左键快捷装备, 此时已经检查完毕 319 | // 短路处理 320 | if (equipped) { 321 | // 延迟后更新事件在此处提交Scheduler 322 | return; 323 | } 324 | } 325 | // 主手处理完毕 现在处理副手和四个装备格子 326 | // 这五个格子只会在PlayerInventory中发生改变 327 | if (event.getClickedInventory().getType() != InventoryType.PLAYER) { 328 | return; 329 | } 330 | // 快速穿装备已经被检查了。所以只剩下直接穿脱和shift脱, 不用检查其它格子 331 | VanillaEquipSlot slot = VanillaEquipSlot.getById(event.getRawSlot()); 332 | if (slot == null) { 333 | return; 334 | } 335 | boolean result; 336 | if (shift) { 337 | // shift点击快速脱 338 | // 如果物品栏没空位就不用检查了 339 | if (player.getInventory().firstEmpty() == -1) { 340 | return; 341 | } 342 | result = !cancelSlotUpdate(UpdateTrigger.SHIFT_CLICK, 343 | player, slot, event.getCurrentItem(), new ItemStack(Material.AIR)); 344 | // 点击其它位置的shift已经被检查过了 直接省略 345 | } else { 346 | // 取得新物品 347 | ItemStack newItem; 348 | if (numberkey) { 349 | newItem = event.getClickedInventory().getItem(event.getHotbarButton()); 350 | newItem = newItem == null ? new ItemStack(Material.AIR) : newItem; 351 | } else { 352 | if (event.getAction() == InventoryAction.DROP_ONE_SLOT) { 353 | newItem = event.getCurrentItem().clone(); 354 | newItem.setAmount(newItem.getAmount() - 1); 355 | } else if (event.getAction() == InventoryAction.DROP_ALL_SLOT) { 356 | newItem = new ItemStack(Material.AIR); 357 | } else { 358 | newItem = event.getCursor(); 359 | } 360 | } 361 | // 如果槽位不是副手, 那么交换需要检查是否放的进去 362 | if (slot != VanillaEquipSlot.OFFHAND) { 363 | // 空气肯定能放进去 364 | if (newItem == null) { 365 | newItem = new ItemStack(Material.AIR); 366 | } else if (newItem.getType() != Material.AIR && !slot.equals(VanillaEquipSlot.matchType(newItem))) { 367 | return; 368 | } 369 | } 370 | result = !cancelSlotUpdate(event.getAction().equals(InventoryAction.HOTBAR_SWAP) || numberkey ? UpdateTrigger.HOTBAR_SWAP : UpdateTrigger.PICK_DROP, 371 | player, slot, event.getCurrentItem(), newItem); 372 | } 373 | if (!result) { 374 | event.setCancelled(true); 375 | } 376 | } 377 | 378 | /** 379 | * 检查玩家右键导致的快速装备穿戴和主手副手更新 380 | * 381 | * @param event 玩家交互事件 382 | */ 383 | private static void onPlayerInteract(PlayerInteractEvent event) { 384 | // 如果手中物品为空或者无法使用, 无需继续检查 385 | ItemStack current = event.getItem(); 386 | if (isAir(current) || event.useItemInHand().equals(Result.DENY)) { 387 | return; 388 | } 389 | if (event.getAction() == Action.PHYSICAL) { 390 | return; 391 | } 392 | if (event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK) { 393 | Player player = event.getPlayer(); 394 | // 如果可以与方块交互 395 | if (!event.useInteractedBlock().equals(Result.DENY) && event.getClickedBlock() != null && event.getAction() == Action.RIGHT_CLICK_BLOCK && !player.isSneaking()) {// Having both of these checks is useless, might as well do it though. 396 | // 有些方块右键时候打开GUI 397 | // 这时候啥也不会触发 398 | String name = event.getClickedBlock().getType().toString().toUpperCase(); 399 | if (BLOCKED_MATERIALS.contains(name)) { 400 | return; 401 | } 402 | } 403 | final VanillaEquipSlot handSlot = event.getHand() == EquipmentSlot.HAND ? VanillaEquipSlot.MAINHAND : VanillaEquipSlot.OFFHAND; 404 | VanillaEquipSlot quickEquipSlot = VanillaEquipSlot.matchType(event.getItem()); 405 | if (quickEquipSlot != null && quickEquipSlot != VanillaEquipSlot.OFFHAND && isAir(quickEquipSlot.get(player))) { 406 | // 如果成功快速装备, 则顺便把主副手也给通知了, 然后短路处理 407 | boolean equipResult = !cancelSlotUpdate(UpdateTrigger.HOTBAR, player, quickEquipSlot, null, current); 408 | boolean handResult = !cancelSlotUpdate(UpdateTrigger.USE, player, handSlot, current, new ItemStack(Material.AIR)); 409 | if (!equipResult || !handResult) { 410 | event.setCancelled(true); 411 | } 412 | return; 413 | } 414 | // 如果触发了使用 415 | // 检查触发事件的手 416 | if (cancelSlotUpdate(UpdateTrigger.USE, player, handSlot, current, null)) { 417 | event.setCancelled(true); 418 | } 419 | } 420 | } 421 | 422 | /** 423 | * 检查拖曳事件造成的原版装备格子更新 424 | * 425 | * @param event 装备格子更新事件 426 | */ 427 | private static void onInventoryDrag(InventoryDragEvent event) { 428 | if (event.getRawSlots().isEmpty()) { 429 | return; 430 | } 431 | Player player = (Player) event.getWhoClicked(); 432 | Map items = event.getNewItems(); 433 | int mainHandIndex = player.getInventory().getHeldItemSlot(); 434 | int topSize = event.getView().getTopInventory().getSize(); 435 | for (Map.Entry entry : items.entrySet()) { 436 | if (entry.getKey() < topSize) { 437 | continue; 438 | } 439 | if (mainHandIndex == event.getView().convertSlot(entry.getKey())) { 440 | if (cancelSlotUpdate(UpdateTrigger.DRAG, player, VanillaEquipSlot.MAINHAND, event.getView().getItem(entry.getKey()), entry.getValue())) { 441 | event.setCancelled(true); 442 | return; 443 | } 444 | } 445 | } 446 | final int[] slots = {5, 6, 7, 8, 45}; 447 | for (int i : slots) { 448 | if (items.containsKey(i)) { 449 | VanillaEquipSlot slot = VanillaEquipSlot.getById(i); 450 | if (cancelSlotUpdate(UpdateTrigger.DRAG, player, slot, slot.get(player), items.get(i))) { 451 | event.setCancelled(true); 452 | return; 453 | } 454 | } 455 | } 456 | } 457 | 458 | /** 459 | * 检查装备耐久损失导致的装备更新 460 | * 依次匹配四个装备格子和主副手 461 | * 462 | * @param event 装备耐久变化事件 463 | */ 464 | @SuppressWarnings("deprecation") 465 | private static void onPlayerItemDamage(PlayerItemDamageEvent event) { 466 | if (event.getDamage() == 0) { 467 | return; 468 | } 469 | Player player = event.getPlayer(); 470 | ItemStack item = event.getItem(); 471 | VanillaEquipSlot slot = VanillaEquipSlot.matchType(item); 472 | // 如果装备成功匹配到类型, 则肯定是装备或者盾牌, 直接callEvent 473 | if (slot != null) { 474 | ItemStack newItem = item.clone(); 475 | newItem.setDurability((short) (item.getDurability() + event.getDamage())); 476 | if (cancelSlotUpdate(UpdateTrigger.DAMAGED, event.getPlayer(), slot, slot.get(player), newItem)) { 477 | event.setCancelled(true); 478 | } 479 | return; 480 | } 481 | // 否则检查主手和副手的物品 哪个和当前物品完全一致 482 | // 先检查主手 一样就不检查副手了 483 | ItemStack main = player.getInventory().getItemInMainHand(); 484 | if (item.equals(main)) { 485 | ItemStack newItem = item.clone(); 486 | newItem.setDurability((short) (item.getDurability() + event.getDamage())); 487 | if (cancelSlotUpdate(UpdateTrigger.DAMAGED, event.getPlayer(), VanillaEquipSlot.MAINHAND, main, newItem)) { 488 | event.setCancelled(true); 489 | } 490 | return; 491 | } 492 | ItemStack off = player.getInventory().getItemInOffHand(); 493 | if (item.equals(off)) { 494 | ItemStack newItem = item.clone(); 495 | newItem.setDurability((short) (item.getDurability() + event.getDamage())); 496 | if (cancelSlotUpdate(UpdateTrigger.DAMAGED, event.getPlayer(), VanillaEquipSlot.OFFHAND, off, newItem)) { 497 | event.setCancelled(true); 498 | } 499 | } 500 | } 501 | 502 | /** 503 | * 检查装备损坏 504 | * 505 | * @param event 装备损坏事件 506 | */ 507 | private static void onPlayerItemBreak(PlayerItemBreakEvent event) { 508 | Player player = event.getPlayer(); 509 | ItemStack item = event.getBrokenItem(); 510 | ItemStack newItem = new ItemStack(Material.AIR); 511 | VanillaEquipSlot slot = VanillaEquipSlot.matchType(item); 512 | // 检查是否是已知槽位 513 | if (slot != null) { 514 | if (cancelSlotUpdate(UpdateTrigger.BROKE, player, slot, item, newItem)) { 515 | slot.setSilently(player, item); 516 | } 517 | return; 518 | } 519 | // 否则检查主手和副手的物品 哪个和当前物品完全一致 520 | // 先检查主手 一样就不检查副手了 521 | ItemStack main = player.getInventory().getItemInMainHand(); 522 | if (item.equals(main)) { 523 | if (cancelSlotUpdate(UpdateTrigger.BROKE, 524 | player, VanillaEquipSlot.MAINHAND, item, newItem)) { 525 | player.getInventory().setItemInMainHand(item); 526 | } 527 | return; 528 | } 529 | ItemStack off = player.getInventory().getItemInOffHand(); 530 | if (item.equals(off)) { 531 | if (cancelSlotUpdate(UpdateTrigger.BROKE, 532 | player, VanillaEquipSlot.OFFHAND, item, newItem)) { 533 | player.getInventory().setItemInOffHand(item); 534 | } 535 | } 536 | } 537 | 538 | /** 539 | * 检查主副手交换事件 540 | * 因为可以短路的地方较多, 所以手工构造事件并触发 541 | * 542 | * @param event 玩家主副手交换事件 543 | */ 544 | private static void onPlayerSwapItem(PlayerSwapHandItemsEvent event) { 545 | Player player = event.getPlayer(); 546 | ItemStack mainHandItem = event.getMainHandItem(); 547 | ItemStack offHandItem = event.getOffHandItem(); 548 | if (mainHandItem == null) { 549 | mainHandItem = new ItemStack(Material.AIR); 550 | } 551 | if (offHandItem == null) { 552 | offHandItem = new ItemStack(Material.AIR); 553 | } 554 | if (cancelSlotUpdate(UpdateTrigger.SWAP, player, VanillaEquipSlot.MAINHAND, offHandItem, mainHandItem) 555 | || cancelSlotUpdate(UpdateTrigger.SWAP, player, VanillaEquipSlot.OFFHAND, mainHandItem, offHandItem)) { 556 | event.setCancelled(true); 557 | } 558 | } 559 | 560 | /** 561 | * 检查玩家主手更替 562 | * 563 | * @param event 玩家主手更替事件 564 | */ 565 | private static void onPlayerItemHeld(PlayerItemHeldEvent event) { 566 | Player player = event.getPlayer(); 567 | PlayerInventory inventory = player.getInventory(); 568 | ItemStack oldItem = inventory.getItem(event.getPreviousSlot()); 569 | ItemStack newItem = inventory.getItem(event.getNewSlot()); 570 | if (newItem == null) { 571 | newItem = new ItemStack(Material.AIR); 572 | } 573 | if (cancelSlotUpdate(UpdateTrigger.HELD, player, VanillaEquipSlot.MAINHAND, oldItem, newItem)) { 574 | event.setCancelled(true); 575 | } 576 | } 577 | 578 | 579 | /** 580 | * 关闭GUI时如果鼠标上还有东西, 暂时禁止掉落检测 581 | * 582 | * @param event GUI关闭事件 583 | */ 584 | private static void onInventoryClose(InventoryCloseEvent event) { 585 | if (!isAir(event.getPlayer().getItemOnCursor())) { 586 | disableDropDetect = true; 587 | } 588 | } 589 | 590 | /** 591 | * 检查玩家丢弃物品事件 592 | * 593 | * @param event 玩家丢弃物品 594 | */ 595 | private static void onPlayerDropItem(PlayerDropItemEvent event) { 596 | // InventoryClick 和 InventoryClose 引发的Drop事件, 忽略之, 不处理 597 | if (disableDropDetect) { 598 | disableDropDetect = false; 599 | return; 600 | } 601 | Player player = event.getPlayer(); 602 | // 鼠标有物品时是丢弃鼠标物品, 不触发检查 603 | ItemStack newItem = player.getInventory().getItemInMainHand(); 604 | ItemStack droppedItem = event.getItemDrop().getItemStack(); 605 | ItemStack oldItem; 606 | if (isAir(newItem)) { 607 | newItem = new ItemStack(Material.AIR); 608 | oldItem = droppedItem; 609 | } else { 610 | oldItem = newItem.clone(); 611 | oldItem.setAmount(newItem.getAmount() + droppedItem.getAmount()); 612 | } 613 | if (cancelSlotUpdate(UpdateTrigger.DROP, player, VanillaEquipSlot.MAINHAND, oldItem, newItem)) { 614 | event.setCancelled(true); 615 | } 616 | } 617 | 618 | /** 619 | * 检查玩家捡起装备产生的主手更新 620 | * 用PlayerPickup是为了1.9x兼容 621 | * 622 | * @param event 玩家拾取装备事件 623 | */ 624 | @SuppressWarnings("deprecation") 625 | private static void onPlayerPickupItem(PlayerPickupItemEvent event) { 626 | Player player = event.getPlayer(); 627 | PlayerInventory inventory = player.getInventory(); 628 | ItemStack item = event.getItem().getItemStack(); 629 | ItemStack mainHand = inventory.getItemInMainHand(); 630 | // 如果主手有物品 631 | if (!isAir(mainHand)) { 632 | // 当且仅当相似且可堆叠时触发检查 633 | if (mainHand.isSimilar(item) && mainHand.getAmount() < mainHand.getMaxStackSize()) { 634 | ItemStack newItem = item.clone(); 635 | newItem.setAmount(Math.min(mainHand.getAmount() + newItem.getAmount(), newItem.getMaxStackSize())); 636 | if (cancelSlotUpdate(UpdateTrigger.PICKUP, player, VanillaEquipSlot.MAINHAND, mainHand, newItem)) { 637 | event.setCancelled(true); 638 | } 639 | } 640 | return; 641 | } 642 | // 如果主手没有物品 643 | // 如果主手之前有空位, 返回 644 | for (int i = 0; i < inventory.getHeldItemSlot(); i++) { 645 | ItemStack current = inventory.getItem(i); 646 | if (isAir(current)) { 647 | return; 648 | } 649 | } 650 | // 否则优先给相似槽位放置 651 | int amount = item.getAmount(); 652 | ItemStack[] contents = player.getInventory().getContents(); 653 | for (ItemStack possible : contents) { 654 | if (item.isSimilar(possible)) { 655 | amount -= (item.getMaxStackSize() - possible.getAmount()); 656 | if (amount <= 0) { 657 | return; 658 | } 659 | } 660 | } 661 | // 此时主手更新了 662 | ItemStack newItem = item.clone(); 663 | newItem.setAmount(amount); 664 | if (cancelSlotUpdate(UpdateTrigger.PICKUP, player, VanillaEquipSlot.MAINHAND, null, newItem)) { 665 | event.setCancelled(true); 666 | } 667 | } 668 | 669 | private static void onCommand(PlayerCommandPreprocessEvent event) { 670 | // 出于兼容性考虑 671 | // hat指令将触发头盔的更新 672 | // 其它指令则仅仅异步检查主手 673 | Player player = event.getPlayer(); 674 | ItemStack mainhand = player.getInventory().getItemInMainHand(); 675 | if (isAir(mainhand)) { 676 | return; 677 | } 678 | if (event.getMessage().toLowerCase().startsWith("/hat")) { 679 | if (mainhand.getType().isBlock() && cancelSlotUpdate(UpdateTrigger.COMMAND_HAT, player, VanillaEquipSlot.HELMET, player.getInventory().getHelmet(), mainhand)) { 680 | event.setCancelled(true); 681 | } 682 | return; 683 | } 684 | if (cancelSlotUpdate(UpdateTrigger.COMMAND, player, VanillaEquipSlot.MAINHAND, mainhand, null)) { 685 | event.setCancelled(true); 686 | } 687 | } 688 | } 689 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/slot/PlayerSlot.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.slot; 2 | 3 | import org.bukkit.entity.Player; 4 | import org.bukkit.inventory.ItemStack; 5 | 6 | import java.util.Arrays; 7 | import java.util.Set; 8 | import java.util.TreeSet; 9 | import java.util.function.Consumer; 10 | 11 | public abstract class PlayerSlot { 12 | 13 | private final String name; 14 | 15 | private final Set aliases = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); 16 | 17 | public PlayerSlot(String name, String... aliases) { 18 | this.name = name; 19 | this.aliases.addAll(Arrays.asList(aliases)); 20 | } 21 | 22 | public abstract boolean isAsyncSafe(); 23 | 24 | public void set(Player player, ItemStack item) { 25 | set(player, item, result -> { 26 | 27 | }); 28 | } 29 | 30 | public abstract void get(Player player, Consumer callback); 31 | 32 | public abstract void set(Player player, ItemStack item, Consumer callback); 33 | 34 | public final Set getAliases() { 35 | return aliases; 36 | } 37 | 38 | @Override 39 | public int hashCode() { 40 | return name.hashCode(); 41 | } 42 | 43 | @Override 44 | public boolean equals(Object o) { 45 | return o instanceof PlayerSlot && name.equals(((PlayerSlot) o).name); 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return name; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/slot/PlayerSlotCache.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.slot; 2 | 3 | import com.github.playerslotapi.PlayerSlotAPI; 4 | import com.github.playerslotapi.event.AsyncSlotUpdateEvent; 5 | import com.github.playerslotapi.event.UpdateTrigger; 6 | import com.github.playerslotapi.util.Util; 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.Material; 9 | import org.bukkit.entity.Player; 10 | import org.bukkit.inventory.ItemStack; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | 18 | public class PlayerSlotCache { 19 | 20 | 21 | private static final ItemStack AIR = new ItemStack(Material.AIR); 22 | 23 | /** 24 | * 缓存的槽位物品和数据. 默认是 25 | */ 26 | private final Map itemCache = new ConcurrentHashMap<>(6); 27 | 28 | /** 29 | * 异步修改专用槽. 一般不会有太多数据所以初始大小为1 30 | */ 31 | private final Map verifyCache = new ConcurrentHashMap<>(1); 32 | private final Map modifiedCache = new ConcurrentHashMap<>(1); 33 | 34 | private final Player player; 35 | 36 | public PlayerSlotCache(Player player) { 37 | this.player = player; 38 | for (PlayerSlot slot : PlayerSlotAPI.getAPI().getSlotMap().values()) { 39 | initSlot(slot); 40 | } 41 | } 42 | 43 | /** 44 | * 加载槽位到缓存中. 45 | * 初始化为全同步过程, 无视缓存 46 | * 47 | * @param slot 要加载的槽位 48 | */ 49 | public void initSlot(PlayerSlot slot) { 50 | slot.get(player, item -> { 51 | item = item == null ? new ItemStack(Material.AIR) : item.clone(); 52 | itemCache.put(slot, item); 53 | }); 54 | } 55 | 56 | 57 | /** 58 | * 获取槽位中缓存的物品 59 | * 60 | * @param slot 要获取的槽位 61 | * @return 缓存物品 62 | */ 63 | @NotNull 64 | public ItemStack getItem(PlayerSlot slot) { 65 | return itemCache.getOrDefault(slot, AIR); 66 | } 67 | 68 | /** 69 | * 获取异步修改槽位中的物品 70 | * 如果是第一次获取, 顺便储存修改前的槽位状态 71 | * 72 | * @param slot 槽位 73 | * @return 异步修改后的物品 74 | */ 75 | @NotNull 76 | public ItemStack getModifiedItem(PlayerSlot slot) { 77 | ItemStack modifiedItem = modifiedCache.get(slot); 78 | if (modifiedItem == null) { 79 | modifiedItem = getItem(slot); 80 | verifyCache.put(slot, modifiedItem); 81 | } 82 | return modifiedItem; 83 | } 84 | 85 | /** 86 | * 对槽位进行异步修改,将结果缓存 87 | * 你必须要在这个方法调用前调用getModifiedItem 88 | * 89 | * @param slot 槽位 90 | * @param item 新的异步修改后的物品 91 | */ 92 | public void setModifiedItem(PlayerSlot slot, ItemStack item) { 93 | modifiedCache.put(slot, item); 94 | } 95 | 96 | /** 97 | * 将异步修改应用到玩家槽位中 98 | * 如果玩家槽位在异步计算期间已经改变 99 | * 那么修改不起作用 防止刷物品 100 | * 此方法必须同步调用 101 | * 102 | * @param ignoreDamage 是否忽略耐久变化 103 | */ 104 | public void applyModification(boolean ignoreDamage) { 105 | final List> slots = new ArrayList<>(modifiedCache.entrySet()); 106 | final Map tempVerifyCache = new ConcurrentHashMap<>(1); 107 | tempVerifyCache.putAll(verifyCache); 108 | modifiedCache.clear(); 109 | verifyCache.clear(); 110 | for (Map.Entry entry : slots) { 111 | PlayerSlot slot = entry.getKey(); 112 | slot.get(player, truth -> { 113 | if (Util.isAir(truth)) { 114 | // 禁止修改空气装备 115 | return; 116 | } 117 | ItemStack verify = tempVerifyCache.get(slot); 118 | if (ignoreDamage) { 119 | if (verify.getType() != truth.getType() || verify.getAmount() != truth.getAmount() || !Bukkit.getItemFactory().equals(verify.getItemMeta(), truth.getItemMeta())) { 120 | return; 121 | } 122 | } else if (verify.equals(truth)) { 123 | return; 124 | } 125 | if (Bukkit.isPrimaryThread() || slot.isAsyncSafe()) { 126 | slot.set(player, entry.getValue()); 127 | } else { 128 | Bukkit.getScheduler().runTask(PlayerSlotAPI.getPlugin(), () -> { 129 | slot.set(player, entry.getValue()); 130 | }); 131 | } 132 | }); 133 | } 134 | } 135 | 136 | /** 137 | * 尝试更新槽位物品 138 | * 注意这个方法会被频繁调用 139 | * 140 | * @param slot 槽位类型 141 | * @param item 槽位物品 142 | */ 143 | public void updateItem(UpdateTrigger trigger, PlayerSlot slot, ItemStack item) { 144 | final ItemStack oldItem = itemCache.getOrDefault(slot, AIR); 145 | final ItemStack newItem = item == null ? new ItemStack(Material.AIR) : item.clone(); 146 | Bukkit.getScheduler().runTaskAsynchronously(PlayerSlotAPI.getPlugin(), () -> { 147 | if (oldItem.equals(newItem)) { 148 | return; 149 | } 150 | AsyncSlotUpdateEvent asyncEvent = new AsyncSlotUpdateEvent(trigger, player, slot, oldItem, newItem); 151 | Bukkit.getPluginManager().callEvent(asyncEvent); 152 | itemCache.put(slot, newItem); 153 | }); 154 | } 155 | 156 | /** 157 | * 更新所有槽位 158 | */ 159 | public void updateAll() { 160 | for (PlayerSlot slot : PlayerSlotAPI.getAPI().getSlotMap().values()) { 161 | slot.get(player, item -> updateItem(UpdateTrigger.CUSTOM, slot, item)); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/slot/impl/DragonCoreSlot.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.slot.impl; 2 | 3 | import com.github.playerslotapi.PlayerSlotAPI; 4 | import com.github.playerslotapi.event.SlotUpdateEvent; 5 | import com.github.playerslotapi.event.UpdateTrigger; 6 | import com.github.playerslotapi.slot.PlayerSlot; 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.entity.Player; 9 | import org.bukkit.inventory.ItemStack; 10 | 11 | import java.util.function.Consumer; 12 | 13 | public class DragonCoreSlot extends PlayerSlot { 14 | 15 | private final String identifier; 16 | 17 | public DragonCoreSlot(String identifier) { 18 | super("DRAGON_CORE_" + identifier, "DRAGON_CORE_SLOT"); 19 | this.identifier = identifier; 20 | } 21 | 22 | public String getIdentifier() { 23 | return identifier; 24 | } 25 | 26 | @Override 27 | public boolean isAsyncSafe() { 28 | return true; 29 | } 30 | 31 | @Override 32 | public void get(Player player, Consumer callback) { 33 | if (Bukkit.isPrimaryThread()) { 34 | Bukkit.getScheduler().runTaskAsynchronously(PlayerSlotAPI.getPlugin(), () -> { 35 | PlayerSlotAPI.getDragonCoreHook().getItemFromSlot(player, identifier, callback); 36 | }); 37 | } else { 38 | PlayerSlotAPI.getDragonCoreHook().getItemFromSlot(player, identifier, callback); 39 | } 40 | } 41 | 42 | @Override 43 | public void set(Player player, ItemStack item, Consumer callback) { 44 | PlayerSlotAPI.getDragonCoreHook().setItemToSlot(player, identifier, item, 45 | result -> { 46 | if (result) { 47 | SlotUpdateEvent updateEvent = new SlotUpdateEvent(UpdateTrigger.SET, player, this, null, item); 48 | updateEvent.setUpdateImmediately(); 49 | Bukkit.getPluginManager().callEvent(updateEvent); 50 | } 51 | callback.accept(result); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/slot/impl/GermPluginSlot.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.slot.impl; 2 | 3 | import com.github.playerslotapi.PlayerSlotAPI; 4 | import com.github.playerslotapi.event.SlotUpdateEvent; 5 | import com.github.playerslotapi.event.UpdateTrigger; 6 | import com.github.playerslotapi.slot.PlayerSlot; 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.entity.Player; 9 | import org.bukkit.inventory.ItemStack; 10 | 11 | import java.util.function.Consumer; 12 | 13 | public class GermPluginSlot extends PlayerSlot { 14 | 15 | private static boolean disableCacheUpdate = false; 16 | private final String identifier; 17 | 18 | public GermPluginSlot(String identifier) { 19 | super("GERM_PLUGIN_" + identifier, "GERM_PLUGIN_SLOT"); 20 | this.identifier = identifier; 21 | } 22 | 23 | public static void disableCacheUpdate() { 24 | disableCacheUpdate = true; 25 | } 26 | 27 | public String getIdentifier() { 28 | return identifier; 29 | } 30 | 31 | @Override 32 | public boolean isAsyncSafe() { 33 | return true; 34 | } 35 | 36 | @Override 37 | public void get(Player player, Consumer callback) { 38 | callback.accept(PlayerSlotAPI.getGermPluginHook().getItemFromSlot(player, identifier)); 39 | } 40 | 41 | @Override 42 | public void set(Player player, ItemStack item, Consumer callback) { 43 | PlayerSlotAPI.getGermPluginHook().setItemToSlot(player, identifier, item); 44 | if (!disableCacheUpdate) { 45 | SlotUpdateEvent updateEvent = new SlotUpdateEvent(UpdateTrigger.SET, player, this, null, item); 46 | updateEvent.setUpdateImmediately(); 47 | Bukkit.getPluginManager().callEvent(updateEvent); 48 | } 49 | callback.accept(true); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/slot/impl/VanillaEquipSlot.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.slot.impl; 2 | 3 | import com.github.playerslotapi.event.SlotUpdateEvent; 4 | import com.github.playerslotapi.event.UpdateTrigger; 5 | import com.github.playerslotapi.slot.PlayerSlot; 6 | import org.bukkit.Bukkit; 7 | import org.bukkit.Material; 8 | import org.bukkit.entity.Player; 9 | import org.bukkit.inventory.ItemStack; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.function.BiConsumer; 14 | import java.util.function.Consumer; 15 | import java.util.function.Function; 16 | 17 | public class VanillaEquipSlot extends PlayerSlot { 18 | 19 | public static final VanillaEquipSlot MAINHAND; 20 | public static final VanillaEquipSlot OFFHAND; 21 | public static final VanillaEquipSlot HELMET; 22 | public static final VanillaEquipSlot CHESTPLATE; 23 | public static final VanillaEquipSlot LEGGINGS; 24 | public static final VanillaEquipSlot BOOTS; 25 | private static final Map ID_MAP = new HashMap<>(); 26 | 27 | static { 28 | MAINHAND = new VanillaEquipSlot((player) -> player.getInventory().getItemInMainHand(), 29 | (player, item) -> player.getInventory().setItemInMainHand(item), "MAINHAND", -1, "mainhand", "hand", "main"); 30 | OFFHAND = new VanillaEquipSlot((player) -> player.getInventory().getItemInOffHand(), 31 | (player, item) -> player.getInventory().setItemInOffHand(item), "OFFHAND", 45, "offhand", "off"); 32 | HELMET = new VanillaEquipSlot((player) -> player.getInventory().getHelmet(), 33 | (player, item) -> player.getInventory().setHelmet(item), "HELMET", 5, "helmet", "head"); 34 | CHESTPLATE = new VanillaEquipSlot((player) -> player.getInventory().getChestplate(), 35 | (player, item) -> player.getInventory().setChestplate(item), "CHESTPLATE", 6, "chestplate", "chests", "chest"); 36 | LEGGINGS = new VanillaEquipSlot((player) -> player.getInventory().getLeggings(), 37 | (player, item) -> player.getInventory().setLeggings(item), "LEGGINGS", 7, "leggings", "legs", "leg"); 38 | BOOTS = new VanillaEquipSlot((player) -> player.getInventory().getBoots(), 39 | (player, item) -> player.getInventory().setBoots(item), "BOOTS", 8, "boots", "boot", "feet", "foot"); 40 | 41 | } 42 | 43 | private final Function getter; 44 | private final BiConsumer setter; 45 | private final int id; 46 | 47 | private VanillaEquipSlot(Function getter, BiConsumer setter, String name, int id, String... alias) { 48 | super(name, alias); 49 | this.getter = getter; 50 | this.setter = setter; 51 | this.id = id; 52 | ID_MAP.put(id, this); 53 | } 54 | 55 | public static VanillaEquipSlot getById(int id) { 56 | return ID_MAP.get(id); 57 | } 58 | 59 | public static VanillaEquipSlot matchType(final ItemStack itemStack) { 60 | if (itemStack == null || itemStack.getType() == Material.AIR) { 61 | return null; 62 | } 63 | String type = itemStack.getType().name(); 64 | return matchType(type); 65 | } 66 | 67 | public static VanillaEquipSlot matchType(String type) { 68 | if (type.endsWith("_HELMET") || type.endsWith("_SKULL") || type.endsWith("_HEAD")) { 69 | return HELMET; 70 | } else if (type.endsWith("_CHESTPLATE") || "ELYTRA".equals(type)) { 71 | return CHESTPLATE; 72 | } else if (type.endsWith("_LEGGINGS")) { 73 | return LEGGINGS; 74 | } else if (type.endsWith("_BOOTS")) { 75 | return BOOTS; 76 | } else if ("SHIELD".equals(type)) { 77 | return OFFHAND; 78 | } else { 79 | return null; 80 | } 81 | } 82 | 83 | @Override 84 | public boolean isAsyncSafe() { 85 | return false; 86 | } 87 | 88 | @Override 89 | public void get(Player player, Consumer callback) { 90 | callback.accept(getter.apply(player)); 91 | } 92 | 93 | 94 | public void setSilently(Player player, ItemStack item) { 95 | setter.accept(player, item); 96 | } 97 | 98 | @Override 99 | public void set(Player player, ItemStack item, Consumer callback) { 100 | setter.accept(player, item); 101 | SlotUpdateEvent updateEvent = new SlotUpdateEvent(UpdateTrigger.SET, player, this, null, item); 102 | updateEvent.setUpdateImmediately(); 103 | Bukkit.getPluginManager().callEvent(updateEvent); 104 | callback.accept(true); 105 | } 106 | 107 | public ItemStack get(Player player) { 108 | return getter.apply(player); 109 | } 110 | 111 | public int getId() { 112 | return id; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/util/Events.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.util; 2 | 3 | import com.github.playerslotapi.PlayerSlotAPI; 4 | import org.bukkit.Bukkit; 5 | import org.bukkit.event.Event; 6 | import org.bukkit.event.EventPriority; 7 | import org.bukkit.event.HandlerList; 8 | import org.bukkit.event.Listener; 9 | import org.bukkit.plugin.EventExecutor; 10 | 11 | import java.lang.reflect.Method; 12 | import java.util.concurrent.atomic.AtomicLong; 13 | import java.util.function.Consumer; 14 | import java.util.function.Function; 15 | 16 | /** 17 | * 可取消式快速事件订阅工具 18 | * 不受所在类中的软依赖影响 19 | * 20 | * @param 要订阅的事件 21 | * @author YiMiner 22 | */ 23 | public class Events implements Listener, EventExecutor { 24 | 25 | private final AtomicLong expire = new AtomicLong(-1); 26 | private final Class clazz; 27 | private final Function func; 28 | private boolean unregistered = false; 29 | 30 | public Events(Class clazz, Function func) { 31 | this.func = func; 32 | this.clazz = clazz; 33 | } 34 | 35 | /** 36 | * 订阅一个事件. 事件处理函数无法自我取消事件的注册 37 | * 最高优先级、忽略取消 38 | * 39 | * @param clazz 事件类 40 | * @param func 事件处理函数. 没有返回值 41 | * @return 已经订阅的事件对象. 可以用unregister取消它 42 | */ 43 | public static Events subscribe(Class clazz, Consumer func) { 44 | return subscribe(clazz, EventPriority.HIGHEST, true, func); 45 | } 46 | 47 | /** 48 | * 订阅一个事件. 事件处理函数可以自我取消事件的监听 49 | * 最高优先级、忽略取消 50 | * 51 | * @param clazz 事件类 52 | * @param func 事件处理函数. 返回false时取消事件注册 53 | * @return 已经订阅的事件对象. 可以用unregister取消它 54 | */ 55 | public static Events subscribe(Class clazz, Function func) { 56 | return subscribe(clazz, EventPriority.HIGHEST, true, func); 57 | } 58 | 59 | /** 60 | * 订阅一个事件. 事件处理函数不能自我取消事件的监听 61 | * 62 | * @param clazz 事件类 63 | * @param priority 事件优先级 64 | * @param ignoreCancelled 是否忽视已经取消的事件 65 | * @param func 事件处理函数. 没有返回值 66 | * @return 已经订阅的事件对象. 可以用unregister取消它 67 | */ 68 | public static Events subscribe(Class clazz, EventPriority priority, boolean ignoreCancelled, Consumer func) { 69 | return subscribe(clazz, priority, ignoreCancelled, (event) -> { 70 | func.accept(event); 71 | return true; 72 | }); 73 | } 74 | 75 | /** 76 | * 订阅一个事件. 事件处理函数可以自我取消事件的监听 77 | * 78 | * @param clazz 事件类 79 | * @param priority 事件优先级 80 | * @param ignoreCancelled 是否忽视已经取消的事件 81 | * @param func 事件处理函数. 返回false时取消事件监听 82 | * @return 已经订阅的事件对象. 可以用unregister取消它 83 | */ 84 | public static Events subscribe(Class clazz, EventPriority priority, boolean ignoreCancelled, Function func) { 85 | Events factory = new Events<>(clazz, func); 86 | if (Bukkit.isPrimaryThread()) { 87 | Bukkit.getPluginManager().registerEvent(clazz, factory, priority, factory, PlayerSlotAPI.getPlugin(), ignoreCancelled); 88 | } else { 89 | Bukkit.getScheduler().runTask(PlayerSlotAPI.getPlugin(), () -> Bukkit.getPluginManager().registerEvent(clazz, factory, priority, factory, PlayerSlotAPI.getPlugin(), ignoreCancelled)); 90 | } 91 | return factory; 92 | } 93 | 94 | /** 95 | * 为事件监听器加上过期时间. 不调用这个方法时监听器永不过期 96 | * 97 | * @param expire 过期时间 98 | * @return 已经注册的事件对象 99 | */ 100 | public Events withTime(long expire) { 101 | this.expire.set(expire); 102 | return this; 103 | } 104 | 105 | /** 106 | * 判断当前事件监听器是否过期 107 | * 108 | * @return 是否过期 109 | */ 110 | public boolean expired() { 111 | return expire.get() > 0 && System.currentTimeMillis() > expire.get(); 112 | } 113 | 114 | @SuppressWarnings("unchecked") 115 | @Override 116 | public void execute(Listener listener, Event event) { 117 | if (expired()) { 118 | unregister(); 119 | return; 120 | } 121 | if (!clazz.isInstance(event)) { 122 | return; 123 | } 124 | if (!func.apply((T) event)) { 125 | unregister(); 126 | } 127 | } 128 | 129 | /** 130 | * 取消事件监听 131 | */ 132 | public void unregister() { 133 | if (unregistered) { 134 | return; 135 | } 136 | try { 137 | Method getHandlerListMethod = clazz.getMethod("getHandlerList"); 138 | HandlerList handlerList = (HandlerList) getHandlerListMethod.invoke(null); 139 | if (Bukkit.isPrimaryThread()) { 140 | handlerList.unregister(this); 141 | } else { 142 | Bukkit.getScheduler().runTask(PlayerSlotAPI.getPlugin(), () -> handlerList.unregister(this)); 143 | } 144 | } catch (Exception ignored) { 145 | } 146 | unregistered = true; 147 | } 148 | 149 | 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/com/github/playerslotapi/util/Util.java: -------------------------------------------------------------------------------- 1 | package com.github.playerslotapi.util; 2 | 3 | import org.bukkit.Material; 4 | import org.bukkit.inventory.ItemStack; 5 | 6 | import java.util.Collections; 7 | import java.util.HashSet; 8 | 9 | public class Util { 10 | public static boolean isAir(ItemStack item) { 11 | return item == null || item.getType().equals(Material.AIR); 12 | } 13 | 14 | public static HashSet newHashSet(E... elements) { 15 | HashSet set = new HashSet(elements.length); 16 | Collections.addAll(set, elements); 17 | return set; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: PlayerSlotAPI 2 | main: com.github.playerslotapi.PlayerSlotPlugin 3 | authors: 4 | - YiMiner 5 | - StrawberryYu 6 | version: ${version} 7 | softdepend: 8 | - Vault 9 | - PlaceHolderAPI 10 | - DragonCore 11 | - GermPlugin 12 | api-version: 1.13 13 | commands: 14 | psapi: 15 | description: test command 16 | usage: /psapi --------------------------------------------------------------------------------