commit c4892410c4d27185884c896033bb89eb839e4624 Author: Fortern Date: Thu Mar 28 01:14:05 2024 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e5b89f --- /dev/null +++ b/.gitignore @@ -0,0 +1,117 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ + + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +target/ + +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next + +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.flattened-pom.xml + +# Common working directory +run/ + +# Gradle +.gradle/ +build/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..113dd98 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,27 @@ +# 使用的镜像 +image: gradle:8.5-jdk17 +# 阶段 +stages: + - build +# 缓存路径配置 +cache: + paths: + - .gradle +# 启用此流水线的条件 +workflow: + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH +before_script: + - chmod +x ./gradlew + +# 配置build阶段的工作 +package_job: + stage: build + tags: + - main + script: + - echo "Gradle building started..." + - ./gradlew shadowJar + artifacts: + paths: + - build/libs/*.jar diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..4f07b40 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("com.github.johnrengelman.shadow") version "8.1.1" + kotlin("jvm") version "1.9.22" +} + +group = "xyz.fortern" +version = "1.0" + +java { + sourceCompatibility = JavaVersion.VERSION_17 +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + +repositories { + mavenCentral() + maven("https://repo.papermc.io/repository/maven-public/") +} + +dependencies { + // Paper API https://mvnrepository.com/artifact/net.kyori/adventure-api + compileOnly("io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT") + // Adventure API + compileOnly("net.kyori:adventure-api:4.14.0") + // Kotlin Stdlib https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib + implementation(kotlin("stdlib")) + // Kotlin Reflect https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect + runtimeOnly(kotlin("reflect")) +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7f93135 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1af9e09 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..06ccffd --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "hello-paper" diff --git a/src/main/kotlin/xyz/fortern/HelloPaper.kt b/src/main/kotlin/xyz/fortern/HelloPaper.kt new file mode 100644 index 0000000..84dced8 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/HelloPaper.kt @@ -0,0 +1,185 @@ +package xyz.fortern + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.Bukkit +import org.bukkit.ChatColor +import org.bukkit.plugin.PluginLogger +import org.bukkit.plugin.java.JavaPlugin +import org.bukkit.scoreboard.Criteria +import org.bukkit.scoreboard.DisplaySlot +import org.bukkit.scoreboard.RenderType +import xyz.fortern.command.executor.AbstractCommandExecutor +import xyz.fortern.event.MyEventHandler +import xyz.fortern.event.OnTakeAwardListener +import xyz.fortern.event.PlayerMessageHandler +import xyz.fortern.recipe.MyCraftingRecipe +import xyz.fortern.recipe.MyFurnaceRecipe +import java.io.IOException +import java.net.JarURLConnection +import java.util.* +import java.util.logging.Logger + +class HelloPaper : JavaPlugin() { + + /** + * 日志记录器 + */ + private val logger: Logger = PluginLogger(this) + + override fun getLogger(): Logger = logger + + /** + * 全局计分板 + */ + val scoreboard by lazy { + Bukkit.getScoreboardManager().newScoreboard + } + + companion object { + /** + * 此plugin对象的实例,可以通过类名访问到 + */ + @JvmStatic + private lateinit var instance: HelloPaper + + /** + * 获取插件的实例 + */ + @JvmStatic + fun getInstance() = instance + + /** + * 侧边栏名称 + */ + @JvmStatic + var SIDEBAR_NAME = "side-bar" + } + + init { + instance = this + } + + // Plugin startup logic + override fun onEnable() { + logger.info("Hello, Minecraft! by logger") + + val javaVersion = System.getProperty("java.version") + val javafxVersion = System.getProperty("javafx.version") + logger.info("Hello, JavaFX ${javafxVersion}, running on Java ${javaVersion}.") + + //注册事件处理器 + //var pluginManager = getServer().getPluginManager() + val pluginManager = Bukkit.getPluginManager() + pluginManager.registerEvents(MyEventHandler(), this) + pluginManager.registerEvents(OnTakeAwardListener(), this) + pluginManager.registerEvents(PlayerMessageHandler(), this) + + //注册所有的命令执行器 + val classLoader = HelloPaper::class.java.classLoader + val packageName = "xyz/fortern/command/executor/impl" + val resource = classLoader.getResource(packageName) + if (resource != null) { + try { + val urls = getClassLoader().getResources(packageName) + while (urls.hasMoreElements()) { + val url = urls.nextElement() + val protocol = url.protocol + /* + * 为什么不要写成 "jar" == protocol + * Yoda 表示法错在哪里 + * https://www.yinwang.org/blog-cn/2013/04/16/yoda-notation + * 另一层问题,以人的阅读习惯,应当左侧为变量,右侧为常量,比如"他是小明吗","他"是一个变量,"小明"是个常量,而"小明是他吗"就会很奇怪 + */ + if (protocol == "jar") { + val jarURLConnection = url.openConnection() as JarURLConnection + val jarFile = jarURLConnection.jarFile + val jarEntries = jarFile.entries() + while (jarEntries.hasMoreElements()) { + val jarEntry = jarEntries.nextElement() + var jarEntryName = jarEntry.name + if (jarEntryName.startsWith(packageName) && jarEntryName.endsWith(".class")) { + jarEntryName = jarEntryName.substring(0, jarEntryName.length - 6).replace('/', '.') + try { + val aClass = Class.forName(jarEntryName) + if (AbstractCommandExecutor::class.java.isAssignableFrom(aClass)) { + val executor = aClass.getConstructor().newInstance() as AbstractCommandExecutor + val commandName = executor.command + //获取在plugin.yml中定义的命令 + val command = getCommand(commandName) + if (command == null) + logger.warning("命令未定义:${commandName}") + else + command.setExecutor(executor) + } + } catch (e: ReflectiveOperationException) { + logger.warning(e.message) + } + } + } + } + } + } catch (e: IOException) { + logger.warning(e.message) + } + } + + /* + 添加所有熔炉配方 + */ + MyFurnaceRecipe.createRecipes(this).forEach(Bukkit::addRecipe) + + /* + 添加工作台合成配方,有摆放形状和无摆放形状两种 + */ + MyCraftingRecipe.createShapedRecipes(this).forEach(Bukkit::addRecipe) + MyCraftingRecipe.createShapelessRecipes(this).forEach(Bukkit::addRecipe) + + //初始化计分板 + scoreBoardHandler() + + logger.info("onEnable complete") + } + + override fun onDisable() { + // Plugin shutdown logic + logger.info("onDisabled complete") + } + + /** + * 计分板初始化,仅在插件加载时调用一次 + */ + private fun scoreBoardHandler() { + //玩家名字标签下方 + val nameBelowObjective = scoreboard.registerNewObjective( + "name-below", + Criteria.FOOD,//此处指定了数值的来源 + Component.text("饥饿值").color(NamedTextColor.GREEN) + ) + nameBelowObjective.displaySlot = DisplaySlot.BELOW_NAME + + //设置 玩家列表计分项 + val tabListObjective = scoreboard.registerNewObjective( + "tab-list", + Criteria.HEALTH,//此处指定了数值的来源 + null//此处不会展示名称,因此传null即可 + ) + tabListObjective.displaySlot = DisplaySlot.PLAYER_LIST//设置显示位置为 + tabListObjective.renderType = RenderType.HEARTS//设置显示形式为心形,心形只在玩家列表处有效 + + //侧栏计分板,通过修改队伍后缀实现信息变化,而不是注销重新注册 + val sidebarObjective = scoreboard.registerNewObjective( + SIDEBAR_NAME, + Criteria.DUMMY, + Component.text("规则列表") + ) + sidebarObjective.getScore("${ChatColor.GOLD}系统时间:").score = 0 + sidebarObjective.displaySlot = DisplaySlot.SIDEBAR + + //设置队伍 + val team = scoreboard.registerNewTeam("rule-info") + team.addEntry("${ChatColor.GOLD}系统时间:") + team.suffix(Component.text(Date().toString())) + } + +} diff --git a/src/main/kotlin/xyz/fortern/command/executor/AbstractCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/AbstractCommandExecutor.kt new file mode 100644 index 0000000..9fe1513 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/AbstractCommandExecutor.kt @@ -0,0 +1,17 @@ +package xyz.fortern.command.executor + +import org.bukkit.command.CommandExecutor +import xyz.fortern.HelloPaper +import java.util.logging.Logger + +/** + * 抽象的指令执行器,封装共有的属性 + */ +abstract class AbstractCommandExecutor( + /** + * 命令的主名称 + */ + val command: String, +) : CommandExecutor { + val logger: Logger = HelloPaper.getInstance().logger +} diff --git a/src/main/kotlin/xyz/fortern/command/executor/impl/AwardCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/impl/AwardCommandExecutor.kt new file mode 100644 index 0000000..dd516e6 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/impl/AwardCommandExecutor.kt @@ -0,0 +1,76 @@ +package xyz.fortern.command.executor.impl + +import net.kyori.adventure.text.Component +import org.bukkit.Bukkit +import org.bukkit.ChatColor +import org.bukkit.Material +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.Player +import org.bukkit.inventory.Inventory +import org.bukkit.inventory.ItemStack +import xyz.fortern.command.executor.AbstractCommandExecutor +import xyz.fortern.holder.GlobalChestHolder + +class AwardCommandExecutor : AbstractCommandExecutor("award") { + + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + //0个参数的情况,仅玩家有效 + if (args.isEmpty()) { + return if (sender is Player) { + doAware(sender, sender) + true + } else { + false + } + } + //1个参数的情况,看这个参数是不是玩家 + if (args.size == 1) { + val player = Bukkit.getPlayerExact(args[0]) + return if (player == null) { + sender.sendMessage("玩家${ChatColor.BLUE}${args[0]}${ChatColor.WHITE}不存在") + false + } else { + doAware(sender, player) + true + } + } + return false + } + + /** + * 向某个物品栏添加奖励物品 + * + * @param inventory 要添加物品的物品栏 + * @return true - 添加成功, false - 添加失败 + */ + private fun award(inventory: Inventory): Boolean { + val index = inventory.firstEmpty()//获取第一个空槽位 + return if (index != -1) { + //生成一个物品槽 + val itemStack = ItemStack(Material.FEATHER) + val itemMeta = itemStack.itemMeta + itemMeta.displayName(Component.text("${ChatColor.RED}小鸡毛")) + val loreList = listOf(Component.text("风属性"), Component.text("火属性")) + itemMeta.lore(loreList) + itemMeta.addEnchant(Enchantment.LUCK, 3, true) + itemStack.itemMeta = itemMeta + inventory.setItem(index, itemStack) + true + } else false + } + + private fun doAware(sender: CommandSender, player: Player) { + val uuid = player.uniqueId + var inventory = GlobalChestHolder.getInventory(uuid) + if (inventory == null) { + inventory = Bukkit.createInventory(player, 9, Component.text("${player.name}的私人背包")) + GlobalChestHolder.addInventory(uuid, inventory) + } + if (award(inventory)) + sender.sendMessage("${ChatColor.GREEN}添加成功") + else + sender.sendMessage("${ChatColor.RED}添加失败") + } +} diff --git a/src/main/kotlin/xyz/fortern/command/executor/impl/BossCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/impl/BossCommandExecutor.kt new file mode 100644 index 0000000..d0629ed --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/impl/BossCommandExecutor.kt @@ -0,0 +1,36 @@ +package xyz.fortern.command.executor.impl + +import org.bukkit.Bukkit +import org.bukkit.boss.BarColor +import org.bukkit.boss.BarFlag +import org.bukkit.boss.BarStyle +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import xyz.fortern.HelloPaper +import xyz.fortern.command.executor.AbstractCommandExecutor +import java.util.concurrent.atomic.AtomicInteger + +class BossCommandExecutor : AbstractCommandExecutor("boss") { + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + val plugin = HelloPaper.getInstance() + //创建一个BOSS条 + val bossBar = Bukkit.createBossBar("究极BOSS", BarColor.GREEN, BarStyle.SEGMENTED_6, BarFlag.CREATE_FOG) + //每一个在线玩家都能看到BOSS条 + Bukkit.getOnlinePlayers().forEach { player -> bossBar.addPlayer(player) } + val scheduler = Bukkit.getScheduler() + val atomicInteger = AtomicInteger(6) + //Kotlin搞笑呢,0参和1参的lambda都可以省略参数列表,结果自己搞不清楚对应的究竟是0参的Runnable还是1参的Consumer,最终还是要我手动指定形参列表 + val taskId = scheduler.runTaskTimerAsynchronously( + plugin, + { -> bossBar.progress = atomicInteger.decrementAndGet() * (1.0 / 6) }, + //Runnable { bossBar.progress = atomicInteger.decrementAndGet() * (1.0 / 6) }, + 0, 2 * 20 + ).taskId + scheduler.runTaskLater(plugin, { -> + bossBar.removeAll() + bossBar.isVisible = false + scheduler.cancelTask(taskId) + }, (6 * 2 * 20).toLong()) + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/fortern/command/executor/impl/GodCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/impl/GodCommandExecutor.kt new file mode 100644 index 0000000..1bb5227 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/impl/GodCommandExecutor.kt @@ -0,0 +1,20 @@ +package xyz.fortern.command.executor.impl + +import org.bukkit.ChatColor +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import xyz.fortern.command.executor.AbstractCommandExecutor + +class GodCommandExecutor : AbstractCommandExecutor("god") { + + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + logger.info("进入god的指令执行器,所传指令为$label") + return if (sender is Player) { + val invulnerable = sender.isInvulnerable + sender.isInvulnerable = !invulnerable + sender.sendMessage("你已进入${if (invulnerable) "${ChatColor.GREEN}普通" else "${ChatColor.RED}上帝"}${ChatColor.WHITE}模式") + true + } else false + } +} diff --git a/src/main/kotlin/xyz/fortern/command/executor/impl/MyNbtCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/impl/MyNbtCommandExecutor.kt new file mode 100644 index 0000000..f57dc5c --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/impl/MyNbtCommandExecutor.kt @@ -0,0 +1,132 @@ +package xyz.fortern.command.executor.impl + +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.block.TileState +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.command.TabExecutor +import org.bukkit.entity.Player +import org.bukkit.persistence.PersistentDataType +import xyz.fortern.HelloPaper +import xyz.fortern.command.executor.AbstractCommandExecutor +import xyz.fortern.persistent.DatetimeTagType +import xyz.fortern.persistent.UUIDTagType +import java.time.ZonedDateTime +import java.util.* + +class MyNbtCommandExecutor : AbstractCommandExecutor("my-nbt"), TabExecutor { + private val subCommands = listOf("seti", "geti", "setb", "getb") + + private val uuidNamespace = NamespacedKey(HelloPaper.getInstance(), "playerUUID") + private val timeNamespace = NamespacedKey(HelloPaper.getInstance(), "updateTime") + private val msgNamespace = NamespacedKey(HelloPaper.getInstance(), "msg") + + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + if (sender !is Player) return false + when (args[0]) { + subCommands[0] -> { + setNbtToItem(sender) + } + + subCommands[1] -> { + getNbtFromItem(sender) + } + + subCommands[2] -> { + setNbtToBlock(sender) + } + + subCommands[3] -> { + getNbtFromBlock(sender) + } + + else -> return false + } + return true + } + + private fun setNbtToItem(player: Player) { + val itemStack = player.inventory.itemInMainHand + if (itemStack.type != Material.AIR) { + val itemMeta = itemStack.itemMeta + val dataContainer = itemMeta.persistentDataContainer + dataContainer.set(uuidNamespace, UUIDTagType, player.uniqueId) + dataContainer.set(timeNamespace, DatetimeTagType, ZonedDateTime.now()) + dataContainer.set(msgNamespace, PersistentDataType.STRING, "默认的自定义物品信息") + itemStack.itemMeta = itemMeta + player.sendMessage("添加自定义信息成功") + } else { + player.sendMessage("手中没有物品") + } + } + + private fun getNbtFromItem(player: Player) { + val itemStack = player.inventory.itemInMainHand + if (itemStack.type != Material.AIR) { + val itemMeta = itemStack.itemMeta + //存储自定义信息的容器 + //ItemMate实现了PersistentDataHolder接口,意味着任何ItemMate都可以添加信息 + val dataContainer = itemMeta.persistentDataContainer + //存入自定义信息 + val uuid = dataContainer.get(uuidNamespace, UUIDTagType) + val zonedDateTime = dataContainer.get(timeNamespace, DatetimeTagType) + val s = dataContainer.get(msgNamespace, PersistentDataType.STRING) + player.sendMessage("物品名称:${itemStack.type.translationKey()}\nUUID:$uuid\n更新时间:$zonedDateTime\n信息:$s") + } else { + player.sendMessage("手中没有物品") + } + } + + private fun setNbtToBlock(player: Player) { + val block = player.getTargetBlockExact(5) + if (block != null) { + val state = block.state + if (state is TileState) { + //TileState继承了PersistentDataHolder接口,这意味着只有只有继承了TileState的方块才能存储自定义信息 + val dataContainer = state.persistentDataContainer + dataContainer.set(uuidNamespace, UUIDTagType, player.uniqueId) + dataContainer.set(timeNamespace, DatetimeTagType, ZonedDateTime.now()) + dataContainer.set(msgNamespace, PersistentDataType.STRING, "默认的自定义方块信息") + state.update(false, false) + player.sendMessage("添加自定义信息成功") + } else { + player.sendMessage("该方块无法添加自定义信息") + } + } else { + player.sendMessage("没有目标方块") + } + } + + private fun getNbtFromBlock(player: Player) { + val block = player.getTargetBlockExact(5) + if (block != null) { + val state = block.state + if (state is TileState) { + //TileState继承了PersistentDataHolder接口,这意味着只有只有继承了TileState的方块才能存储自定义信息 + val dataContainer = state.persistentDataContainer + val uuid = dataContainer.get(uuidNamespace, UUIDTagType) + val zonedDateTime = dataContainer.get(timeNamespace, DatetimeTagType) + val s = dataContainer.get(msgNamespace, PersistentDataType.STRING) + player.sendMessage("方块名称:${state.type.translationKey()}\nUUID:$uuid\n更新时间:$zonedDateTime\n信息:$s") + } + } else { + player.sendMessage("没有目标方块") + } + } + + override fun onTabComplete( + sender: CommandSender, + command: Command, + label: String, + args: Array, + ): List? { + if (args.size == 1) { + return if (args[0].isEmpty()) + subCommands + else + subCommands.filter { it.startsWith(args[0].lowercase(Locale.getDefault())) } + } + return null + } +} diff --git a/src/main/kotlin/xyz/fortern/command/executor/impl/OpenCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/impl/OpenCommandExecutor.kt new file mode 100644 index 0000000..9ca4b3b --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/impl/OpenCommandExecutor.kt @@ -0,0 +1,28 @@ +package xyz.fortern.command.executor.impl + +import net.kyori.adventure.text.Component +import org.bukkit.Bukkit +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import xyz.fortern.command.executor.AbstractCommandExecutor +import xyz.fortern.holder.GlobalChestHolder + +class OpenCommandExecutor : AbstractCommandExecutor("open") { + + /** + * 打开一个玩家特有的物品栏 + */ + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + return if (sender is Player) { + val playerUuid = sender.uniqueId + var privateInventory = GlobalChestHolder.getInventory(playerUuid) + if (privateInventory === null) { + privateInventory = Bukkit.createInventory(sender, 9, Component.text(sender.name + "的私人背包")) + GlobalChestHolder.addInventory(playerUuid, privateInventory) + } + sender.openInventory(privateInventory) + true + } else false + } +} diff --git a/src/main/kotlin/xyz/fortern/command/executor/impl/PrintCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/impl/PrintCommandExecutor.kt new file mode 100644 index 0000000..5b6ed54 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/impl/PrintCommandExecutor.kt @@ -0,0 +1,107 @@ +package xyz.fortern.command.executor.impl + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.event.ClickEvent +import net.kyori.adventure.text.event.HoverEvent +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextDecoration +import net.md_5.bungee.api.chat.TextComponent +import net.md_5.bungee.api.chat.TranslatableComponent +import org.bukkit.ChatColor +import org.bukkit.Material +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.command.TabExecutor +import org.bukkit.entity.EntityType +import org.bukkit.entity.Player +import xyz.fortern.command.executor.AbstractCommandExecutor +import java.util.* + +class PrintCommandExecutor : AbstractCommandExecutor("print"), TabExecutor { + + private val subcommands = listOf("common", "color", "comp") + + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + if (args.size == 1 && args[0].isEmpty()) + return false + if (sender is Player) { + when (args[0]) { + "common" -> f0(sender) + "color" -> f1(sender) + "comp" -> f2(sender) + } + } + return false + } + + override fun onTabComplete( + sender: CommandSender, + command: Command, + label: String, + args: Array, + ): List? { + if (args.size == 1) { + return if (args[0].isEmpty()) + subcommands + else + subcommands.filter { it.startsWith(args[0].lowercase(Locale.getDefault())) } + } + return null + } + + /** + * 发送普通的颜色消息,使用枚举中的颜色 + */ + private fun f0(player: Player) { + //用法1,ChatColor拼接 + val message = "${ChatColor.GOLD}你好${ChatColor.BOLD}${ChatColor.RED}世界" + player.sendMessage(message) + //用法2,使用ChatColor提供的静态方法 + val message1 = ChatColor.translateAlternateColorCodes('&', "&6你好&l&c世界") + player.sendMessage(message1) + } + + /** + * 使用自定义颜色发送消息 + */ + private fun f1(player: Player) { + val color1 = net.md_5.bungee.api.ChatColor.of("#FCF3CF") + val color2 = net.md_5.bungee.api.ChatColor.of("#F9E79F") + val color3 = net.md_5.bungee.api.ChatColor.of("#F7DC6F") + val color4 = net.md_5.bungee.api.ChatColor.of("#F4D03F") + val color5 = net.md_5.bungee.api.ChatColor.of("#F1C40F") + val color6 = net.md_5.bungee.api.ChatColor.of("#D4AC0D") + val message = "${color1}大${color2}家${color3}好${color4}啊${color5}欢${color6}迎" + player.sendMessage(message) + } + + /** + * 发送带有组件的消息 + */ + private fun f2(player: Player) { + val url = "https://space.bilibili.com/34710876" + val textComponent1 = Component.text("点击").color(NamedTextColor.GREEN) + .append( + Component.text("此处") + .color(NamedTextColor.RED) + .hoverEvent(HoverEvent.showText(Component.text(url))) + .clickEvent(ClickEvent.openUrl(url)) + ) + .append(Component.text("跳转到Fortern的B站主页").color(NamedTextColor.GREEN)) + player.sendMessage(textComponent1) + val textComponent2 = Component.text("游戏中点击") + .append(Component.keybind("key.use").color(NamedTextColor.RED).decoration(TextDecoration.BOLD, true)) + .append(Component.text("用于")) + .append(Component.text("提交物品").decorate(TextDecoration.OBFUSCATED)) + player.sendMessage(textComponent2) + val textComponent3 = Component.text("item") + .hoverEvent(HoverEvent.showEntity(EntityType.PLAYER, player.uniqueId, player.displayName())) + player.sendMessage(textComponent3) + val textComponent4 = Component.text("物品:") + .append(Component.translatable(Material.DIRT).color(NamedTextColor.GOLD)) + player.sendMessage(textComponent4) + //使用spigot的API + player.spigot().sendMessage(TextComponent("物品:"), TranslatableComponent(Material.DIRT.translationKey())) + player.sendActionBar(Component.text("物品栏上方的文字")) + } +} diff --git a/src/main/kotlin/xyz/fortern/command/executor/impl/ScoreCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/impl/ScoreCommandExecutor.kt new file mode 100644 index 0000000..11760e7 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/impl/ScoreCommandExecutor.kt @@ -0,0 +1,18 @@ +package xyz.fortern.command.executor.impl + +import net.kyori.adventure.text.Component +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import xyz.fortern.HelloPaper +import xyz.fortern.command.executor.AbstractCommandExecutor +import java.util.* + +class ScoreCommandExecutor : AbstractCommandExecutor("score") { + + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + val scoreboard = HelloPaper.getInstance().scoreboard + scoreboard.getTeam("rule-info")?.suffix(Component.text(Date().toString())) + return true + } + +} diff --git a/src/main/kotlin/xyz/fortern/command/executor/impl/SpawnCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/impl/SpawnCommandExecutor.kt new file mode 100644 index 0000000..ff7d1c3 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/impl/SpawnCommandExecutor.kt @@ -0,0 +1,20 @@ +package xyz.fortern.command.executor.impl + +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.entity.FallingBlock +import org.bukkit.entity.Player +import xyz.fortern.command.executor.AbstractCommandExecutor + +class SpawnCommandExecutor: AbstractCommandExecutor("spawn") { + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + if(sender !is Player) return false + //生成下落方块 + sender.world.spawn(sender.location.toCenterLocation(), FallingBlock::class.java) { entity -> + entity.blockData = Bukkit.createBlockData(Material.TNT) + } + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/fortern/command/executor/impl/StuckCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/impl/StuckCommandExecutor.kt new file mode 100644 index 0000000..fe6186a --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/impl/StuckCommandExecutor.kt @@ -0,0 +1,78 @@ +package xyz.fortern.command.executor.impl + +import org.bukkit.Bukkit +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.scheduler.BukkitRunnable +import xyz.fortern.HelloPaper.Companion.getInstance +import xyz.fortern.command.executor.AbstractCommandExecutor + +class StuckCommandExecutor : AbstractCommandExecutor("stuck") { + private val plugin = getInstance() + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + if (args.isEmpty()) return false + when (args[0]) { + "1" -> //BukkitRunnable runTask 同步阻塞线程调用 + object : BukkitRunnable() { + override fun run() { + sender.sendMessage("主线程停止3秒") + try { + Thread.sleep(3000) + } catch (e: InterruptedException) { + e.printStackTrace() + } finally { + sender.sendMessage("主线程恢复运行") + } + } + }.runTask(plugin) + + "2" -> //BukkitRunnable runTaskAsynchronously 异步阻塞线程调用 + object : BukkitRunnable() { + override fun run() { + sender.sendMessage("异步线程停止3秒,主线程不受影响") + try { + Thread.sleep(3000) + } catch (e: InterruptedException) { + e.printStackTrace() + } finally { + sender.sendMessage("异步线程恢复运行") + } + } + }.runTaskAsynchronously(plugin) + + "3" -> { + sender.sendMessage("异步线程3*20tick后开始执行") + object : BukkitRunnable() { + override fun run() { + sender.sendMessage("异步线程停止3秒,主线程不受影响") + try { + Thread.sleep(3000) + } catch (e: InterruptedException) { + e.printStackTrace() + } finally { + sender.sendMessage("异步线程恢复运行") + } + } + }.runTaskLaterAsynchronously(plugin, (3 * 20).toLong()) + } + + "4" -> { //周期性执行任务 + val task = object : BukkitRunnable() { + override fun run() { + sender.sendMessage("周期性执行的线程任务,每2秒执行1次") + } + } + val bukkitTask = task.runTaskTimerAsynchronously(plugin, 20, (2 * 20).toLong()) + + //让任务调度器在几秒钟后停止task的执行 + Bukkit.getScheduler().runTaskLater(plugin, Runnable { bukkitTask.cancel() }, (20 * 20).toLong()) + } + + "5" -> { + val scheduler = Bukkit.getScheduler() //获取线程调度器 + scheduler.runTaskLater(plugin, Runnable { sender.sendMessage("使用线程调度器执行原生Runnable") }, 0) + } + } + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/fortern/command/executor/impl/SuicideCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/impl/SuicideCommandExecutor.kt new file mode 100644 index 0000000..974f4d6 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/impl/SuicideCommandExecutor.kt @@ -0,0 +1,34 @@ +package xyz.fortern.command.executor.impl + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import xyz.fortern.command.executor.AbstractCommandExecutor + +class SuicideCommandExecutor : AbstractCommandExecutor("suicide") { + + /** + * 自杀指令处理 + * + * @param sender Source of the command + * @param command Command which was executed + * @param label Alias of the command which was used + * @param args Passed command arguments + * @return 指令执行是否成功 + */ + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + logger.info("进入suicide的指令执行器,所传指令为$label") + return if (sender is Player) { + // 发送广播 + sender.getServer().broadcast( + Component.text("再见了,世界!\n").color(NamedTextColor.YELLOW) + .append(sender.displayName().append(Component.text("去世了:/")).color(NamedTextColor.BLUE)) + ) + // 设置血量为0 + sender.health = 0.0 + true + } else false + } +} diff --git a/src/main/kotlin/xyz/fortern/command/executor/impl/SuperItemCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/impl/SuperItemCommandExecutor.kt new file mode 100644 index 0000000..0dcacec --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/impl/SuperItemCommandExecutor.kt @@ -0,0 +1,141 @@ +package xyz.fortern.command.executor.impl + +import org.bukkit.Material +import org.bukkit.attribute.Attribute +import org.bukkit.attribute.AttributeModifier +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.command.TabExecutor +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.Player +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.ItemFlag +import xyz.fortern.command.executor.AbstractCommandExecutor +import java.util.* + +class SuperItemCommandExecutor : AbstractCommandExecutor("strengthen"), TabExecutor { + private val subCommands = listOf("add", "remove", "hide", "show") + + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + if (sender !is Player || args.isEmpty()) return false + when (args[0]) { + subCommands[0] -> { + addAttribute(sender) + } + + subCommands[1] -> { + removeAttribute(sender) + } + + subCommands[2] -> { + hideAttribute(sender) + } + + subCommands[3] -> { + showAttribute(sender) + } + + else -> return false + } + return true + } + + override fun onTabComplete( + sender: CommandSender, + command: Command, + label: String, + args: Array, + ): List? { + if (args.size == 1) { + return if (args[0].isEmpty()) + subCommands + else + subCommands.filter { it.startsWith(args[0].lowercase(Locale.getDefault())) } + } + return null + } + + private fun addAttribute(player: Player) { + val itemStack = player.inventory.itemInMainHand + if (itemStack.type != Material.AIR) { + val itemMeta = itemStack.itemMeta + itemMeta.isUnbreakable = true + itemMeta.addEnchant(Enchantment.LUCK, 100, true) + //移除所有位置的物品属性 + for (slot in EquipmentSlot.values()) { + itemMeta.removeAttributeModifier(slot) + } + itemMeta.addAttributeModifier( + Attribute.GENERIC_MOVEMENT_SPEED, + AttributeModifier( + UUID.randomUUID(), + "增幅玩家移速", + 0.5, + AttributeModifier.Operation.ADD_NUMBER, + EquipmentSlot.HAND + ) + ) + itemMeta.addAttributeModifier( + Attribute.GENERIC_ATTACK_DAMAGE, + AttributeModifier( + UUID.randomUUID(), + "增加玩家伤害", + 19.0, + AttributeModifier.Operation.ADD_NUMBER, + EquipmentSlot.HAND + ) + ) + itemMeta.addAttributeModifier( + Attribute.GENERIC_MAX_HEALTH, + AttributeModifier( + UUID.randomUUID(), + "增加最大血量", + 40.0, + AttributeModifier.Operation.ADD_NUMBER, + EquipmentSlot.HEAD + ) + ) + itemStack.itemMeta = itemMeta + } else { + player.sendMessage("主手没有物品") + } + } + + private fun removeAttribute(player: Player) { + val itemStack = player.inventory.itemInMainHand + if (itemStack.type != Material.AIR) { + val itemMeta = itemStack.itemMeta + itemMeta.isUnbreakable = false + itemMeta.removeEnchant(Enchantment.LUCK) + //移除所有位置的物品属性 + for (slot in EquipmentSlot.values()) { + itemMeta.removeAttributeModifier(slot) + } + itemStack.itemMeta = itemMeta + } else { + player.sendMessage("主手没有物品") + } + } + + private fun hideAttribute(player: Player) { + val itemStack = player.inventory.itemInMainHand + if (itemStack.type != Material.AIR) { + val itemMeta = itemStack.itemMeta + itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS, ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_UNBREAKABLE) + itemStack.itemMeta = itemMeta + } else { + player.sendMessage("主手没有物品") + } + } + + private fun showAttribute(player: Player) { + val itemStack = player.inventory.itemInMainHand + if (itemStack.type != Material.AIR) { + val itemMeta = itemStack.itemMeta + itemMeta.removeItemFlags(ItemFlag.HIDE_ENCHANTS, ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_UNBREAKABLE) + itemStack.itemMeta = itemMeta + } else { + player.sendMessage("主手没有物品") + } + } +} diff --git a/src/main/kotlin/xyz/fortern/command/executor/impl/TitleCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/impl/TitleCommandExecutor.kt new file mode 100644 index 0000000..cae5bd9 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/impl/TitleCommandExecutor.kt @@ -0,0 +1,26 @@ +package xyz.fortern.command.executor.impl + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.title.Title +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import xyz.fortern.command.executor.AbstractCommandExecutor +import java.time.Duration + +class TitleCommandExecutor : AbstractCommandExecutor("title") { + + private val title = Title.title( + Component.text("欢迎来到Fortern的世界", NamedTextColor.WHITE), + Component.text("喵呜", NamedTextColor.GRAY), + Title.Times.times(Duration.ofMillis(500), Duration.ofMillis(3000), Duration.ofMillis(1000)) + ) + + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + return if (sender is Player) { + sender.showTitle(title) + true + } else false + } +} diff --git a/src/main/kotlin/xyz/fortern/command/executor/impl/VillagerCommandExecutor.kt b/src/main/kotlin/xyz/fortern/command/executor/impl/VillagerCommandExecutor.kt new file mode 100644 index 0000000..1ecbaae --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/executor/impl/VillagerCommandExecutor.kt @@ -0,0 +1,32 @@ +package xyz.fortern.command.executor.impl + +import net.kyori.adventure.text.Component +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.entity.EntityType +import org.bukkit.entity.Player +import org.bukkit.entity.Villager +import xyz.fortern.command.executor.AbstractCommandExecutor +import xyz.fortern.recipe.MyMerchantRecipes + +class VillagerCommandExecutor : AbstractCommandExecutor("villager") { + + private val myRecipes = MyMerchantRecipes.createMerchantRecipes() + + /** + * village命令执行器 + */ + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + return if (sender is Player) { + //生成村民对象 + val world = sender.world + val location = sender.location + val villager = world.spawnEntity(location, EntityType.VILLAGER) as Villager + //修改村民信息 + villager.setAI(false) + villager.customName(Component.text("奸商")) + villager.recipes = myRecipes + true + } else false + } +} diff --git a/src/main/kotlin/xyz/fortern/command/renderer/ForternChatRenderer.kt b/src/main/kotlin/xyz/fortern/command/renderer/ForternChatRenderer.kt new file mode 100644 index 0000000..03958a0 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/command/renderer/ForternChatRenderer.kt @@ -0,0 +1,21 @@ +package xyz.fortern.command.renderer + +import io.papermc.paper.chat.ChatRenderer +import net.kyori.adventure.audience.Audience +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.TextColor +import org.bukkit.entity.Player + +/** + * 自定义聊天渲染器 + */ +class ForternChatRenderer : ChatRenderer { + override fun render(source: Player, sourceDisplayName: Component, message: Component, viewer: Audience): Component { + return Component.text("[") + .append(Component.text("mojo").color(TextColor.color(0x66CCFF))) + .append(Component.text("]<")) + .append(sourceDisplayName) + .append(Component.text("> ")) + .append(message) + } +} diff --git a/src/main/kotlin/xyz/fortern/event/MyEventHandler.kt b/src/main/kotlin/xyz/fortern/event/MyEventHandler.kt new file mode 100644 index 0000000..7e35066 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/event/MyEventHandler.kt @@ -0,0 +1,89 @@ +package xyz.fortern.event + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.DyeColor +import org.bukkit.Material +import org.bukkit.Sound +import org.bukkit.block.data.type.Beehive +import org.bukkit.entity.Player +import org.bukkit.entity.Sheep +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.inventory.EquipmentSlot +import xyz.fortern.HelloPaper + +class MyEventHandler : Listener { + + val logger = HelloPaper.getInstance().logger + + /** + * 玩家进入服务器时的处理事件 + * + * @param event 当前事件 + */ + @EventHandler + fun onPlayerJoin(event: PlayerJoinEvent) { + //玩家进入服务器后,获取玩家位置,在附近播放升级音效 + val player = event.player + val location = player.location + location.world?.playSound(location, Sound.ENTITY_PLAYER_LEVELUP, .5f, 1f) + //并发布全局广播消息 + event.joinMessage( + Component.text("欢迎玩家 ").color(NamedTextColor.GREEN) + .append(Component.text(player.name).color(NamedTextColor.RED)) + ) + + + player.scoreboard = HelloPaper.getInstance().scoreboard + + } + + /** + * 玩家攻击羊,使羊变色,且没有伤害 + * + * @param event 当前事件 + */ + @EventHandler + fun onPlayerHitSheep(event: EntityDamageByEntityEvent) { + val damager = event.damager + if (damager is Player) { + val entity = event.entity + if (entity is Sheep) { + val colors = DyeColor.entries + val i = (0..colors.size).random() + entity.color = colors[i] + event.isCancelled = true//取消事件的后续处理? + } + } + } + + @EventHandler + fun getHoneycombInfo(event: PlayerInteractEvent) { + val clickedBlock = event.clickedBlock ?: return + val blockData = clickedBlock.blockData + if (blockData !is Beehive) + return + + if (event.hand == EquipmentSlot.OFF_HAND) + return + + val player = event.player + if (player.inventory.itemInMainHand.type != Material.AIR) + return + + val honeyLevel = blockData.honeyLevel + val blockState = clickedBlock.state as org.bukkit.block.Beehive + val count = blockState.entityCount + player.sendActionBar( + Component.text("蜂蜜等级:").color(NamedTextColor.GREEN) + .append(Component.text(honeyLevel).color(NamedTextColor.RED)) + .append(Component.text(" ")) + .append(Component.text("蜜蜂数量:").color(NamedTextColor.GREEN)) + .append(Component.text(count).color(NamedTextColor.RED)) + ) + } +} diff --git a/src/main/kotlin/xyz/fortern/event/OnTakeAwardListener.kt b/src/main/kotlin/xyz/fortern/event/OnTakeAwardListener.kt new file mode 100644 index 0000000..669fd9e --- /dev/null +++ b/src/main/kotlin/xyz/fortern/event/OnTakeAwardListener.kt @@ -0,0 +1,30 @@ +package xyz.fortern.event + +import org.bukkit.ChatColor +import org.bukkit.Material +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.inventory.InventoryClickEvent +import xyz.fortern.holder.GlobalChestHolder + +class OnTakeAwardListener: Listener { + + /** + * 当玩家试图在自定义物品栏拿走特殊物品时会失败 + * + * @param event 物品栏点击事件 + */ + @EventHandler + fun onTakeAward(event: InventoryClickEvent) { + val inventory = event.inventory + val player = event.whoClicked + val privateInventory = GlobalChestHolder.getInventory(player.uniqueId) + if (inventory === privateInventory) { + val clickedStack = event.currentItem + if (clickedStack != null && clickedStack.type === Material.FEATHER) { + event.isCancelled = true + player.sendMessage(ChatColor.GREEN.toString() + "哈哈哈,放弃吧,你拿不到的") + } + } + } +} diff --git a/src/main/kotlin/xyz/fortern/event/PlayerMessageHandler.kt b/src/main/kotlin/xyz/fortern/event/PlayerMessageHandler.kt new file mode 100644 index 0000000..1180d0c --- /dev/null +++ b/src/main/kotlin/xyz/fortern/event/PlayerMessageHandler.kt @@ -0,0 +1,14 @@ +package xyz.fortern.event + +import io.papermc.paper.event.player.AsyncChatEvent +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import xyz.fortern.command.renderer.ForternChatRenderer + +class PlayerMessageHandler: Listener { + private val renderer = ForternChatRenderer() + @EventHandler + fun onChat(event: AsyncChatEvent) { + event.renderer(renderer) + } +} diff --git a/src/main/kotlin/xyz/fortern/holder/GlobalChestHolder.kt b/src/main/kotlin/xyz/fortern/holder/GlobalChestHolder.kt new file mode 100644 index 0000000..eabd306 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/holder/GlobalChestHolder.kt @@ -0,0 +1,22 @@ +package xyz.fortern.holder + +import org.bukkit.inventory.Inventory +import java.util.* + +/** + * 全部的箱子集合 + */ +object GlobalChestHolder { + private val inventoryMap: MutableMap = HashMap() + operator fun contains(uuid: UUID): Boolean { + return inventoryMap.containsKey(uuid) + } + + fun getInventory(uuid: UUID): Inventory? { + return inventoryMap[uuid] + } + + fun addInventory(uuid: UUID, inventory: Inventory) { + inventoryMap[uuid] = inventory//inventoryMap.put(uuid, inventory) + } +} diff --git a/src/main/kotlin/xyz/fortern/persistent/DatetimeTagType.kt b/src/main/kotlin/xyz/fortern/persistent/DatetimeTagType.kt new file mode 100644 index 0000000..bb9306c --- /dev/null +++ b/src/main/kotlin/xyz/fortern/persistent/DatetimeTagType.kt @@ -0,0 +1,32 @@ +package xyz.fortern.persistent + +import org.bukkit.persistence.PersistentDataAdapterContext +import org.bukkit.persistence.PersistentDataType +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime + +/** + * 自定义容器存储类型,将时间类以Long时间戳存储 + */ +object DatetimeTagType : PersistentDataType { + + @Suppress("UNCHECKED_CAST") + override fun getPrimitiveType(): Class { + //此处如果返回Long::class.java,编译器会进行“优化”将其变为原始类型long的Class,从而导致Bukkit报错 + //我们所期望的是java.lang.Long的Class + return java.lang.Long::class.java as Class + } + + override fun getComplexType(): Class { + return ZonedDateTime::class.java + } + + override fun toPrimitive(complex: ZonedDateTime, context: PersistentDataAdapterContext): Long { + return complex.toInstant().toEpochMilli() + } + + override fun fromPrimitive(primitive: Long, context: PersistentDataAdapterContext): ZonedDateTime { + return ZonedDateTime.ofInstant(Instant.ofEpochMilli(primitive), ZoneId.systemDefault()) + } +} diff --git a/src/main/kotlin/xyz/fortern/persistent/UUIDTagType.kt b/src/main/kotlin/xyz/fortern/persistent/UUIDTagType.kt new file mode 100644 index 0000000..8e2e48e --- /dev/null +++ b/src/main/kotlin/xyz/fortern/persistent/UUIDTagType.kt @@ -0,0 +1,35 @@ +package xyz.fortern.persistent + +import org.bukkit.persistence.PersistentDataAdapterContext +import org.bukkit.persistence.PersistentDataType +import java.nio.ByteBuffer +import java.util.* + +/** + * 自定义容器存储类型,将UUID以byte数组存储 + * + * @author Fortern + */ +object UUIDTagType : PersistentDataType { + override fun getPrimitiveType(): Class { + return ByteArray::class.java + } + + override fun getComplexType(): Class { + return UUID::class.java + } + + override fun toPrimitive(complex: UUID, context: PersistentDataAdapterContext): ByteArray { + val byteBuffer = ByteBuffer.wrap(ByteArray(16)) + byteBuffer.putLong(complex.mostSignificantBits) + byteBuffer.putLong(complex.leastSignificantBits) + return byteBuffer.array() + } + + override fun fromPrimitive(primitive: ByteArray, context: PersistentDataAdapterContext): UUID { + val byteBuffer = ByteBuffer.wrap(primitive) + val mostSignificantBits = byteBuffer.long + val leastSignificantBits = byteBuffer.long + return UUID(mostSignificantBits, leastSignificantBits) + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/fortern/recipe/MyCraftingRecipe.kt b/src/main/kotlin/xyz/fortern/recipe/MyCraftingRecipe.kt new file mode 100644 index 0000000..0a04358 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/recipe/MyCraftingRecipe.kt @@ -0,0 +1,57 @@ +package xyz.fortern.recipe + +import net.kyori.adventure.text.Component +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.enchantments.Enchantment +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.ShapedRecipe +import org.bukkit.inventory.ShapelessRecipe +import org.bukkit.plugin.Plugin + +/** + * 合成配方类 + */ +object MyCraftingRecipe { + fun createShapedRecipes(plugin: Plugin): Array { + val itemStack = ItemStack(Material.BOW) + val itemMeta = itemStack.itemMeta + itemMeta.addEnchant(Enchantment.MENDING, 1, false) + itemMeta.addEnchant(Enchantment.ARROW_INFINITE, 1, false) + val shapedRecipe1 = ShapedRecipe(NamespacedKey(plugin, "craft_god_bow"), itemStack) + /* + * [0 1 2| + * |3 4 5| + * |6 7 8] + * 合成栏为3*3的格子,每个格子用一个字符表示,每一行3个格子在一起组成一个字符串shape + * shape的长度为1,2或3,而shape参数的数量也是1,2或3 + * 多个shape字符串从上到下排列,形成配方表 + * 每种符号表示一种物品,空格表示此处没有物品 + */ + /** + * 下面这行代码表示的配方是这样: + * [0 | + * | 1| + * 两个物品以左上右下的对角方式放置 + */ + shapedRecipe1.shape("0 ", " 1")//原材料的摆放 + //摆放方式中字符代表的物品 + shapedRecipe1.setIngredient('0', Material.BLAZE_POWDER) + shapedRecipe1.setIngredient('1', Material.SLIME_BALL) + return arrayOf(shapedRecipe1) + } + + fun createShapelessRecipes(plugin: Plugin): Array { + val itemStack = ItemStack(Material.BOW) + val itemMeta = itemStack.itemMeta + itemMeta.addEnchant(Enchantment.MENDING, 1, false) + itemMeta.addEnchant(Enchantment.ARROW_INFINITE, 1, false) + itemMeta.displayName(Component.text("神弓")) + val shapelessRecipe = ShapelessRecipe(NamespacedKey(plugin, "craft_gooood_bow"), itemStack) + shapelessRecipe.addIngredient(3, Material.FEATHER) + shapelessRecipe.addIngredient(1, Material.GUNPOWDER) + return arrayOf( + shapelessRecipe + ) + } +} diff --git a/src/main/kotlin/xyz/fortern/recipe/MyFurnaceRecipe.kt b/src/main/kotlin/xyz/fortern/recipe/MyFurnaceRecipe.kt new file mode 100644 index 0000000..868124d --- /dev/null +++ b/src/main/kotlin/xyz/fortern/recipe/MyFurnaceRecipe.kt @@ -0,0 +1,31 @@ +package xyz.fortern.recipe + +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.inventory.FurnaceRecipe +import org.bukkit.inventory.ItemStack +import org.bukkit.plugin.Plugin + +/** + * 我的熔炉配方 + */ +object MyFurnaceRecipe { + fun createRecipes(plugin: Plugin): Array { + return arrayOf( + FurnaceRecipe( + NamespacedKey(plugin, "dirt_to_coarse_dirt"), + ItemStack(Material.COARSE_DIRT), + Material.DIRT, + 10f, + 5 * 20 + ), + FurnaceRecipe( + NamespacedKey(plugin, "warped_stem_to_charcoal"), + ItemStack(Material.CHARCOAL), + Material.WARPED_STEM, + 10f, + 5 * 20 + ) + ) + } +} diff --git a/src/main/kotlin/xyz/fortern/recipe/MyMerchantRecipes.kt b/src/main/kotlin/xyz/fortern/recipe/MyMerchantRecipes.kt new file mode 100644 index 0000000..935459e --- /dev/null +++ b/src/main/kotlin/xyz/fortern/recipe/MyMerchantRecipes.kt @@ -0,0 +1,21 @@ +package xyz.fortern.recipe + +import org.bukkit.Material +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.MerchantRecipe + +object MyMerchantRecipes { + fun createMerchantRecipes(): List { + //创建一个配方,指定交易得到的ItemStack和最大交易次数 + val merchantRecipe1 = MerchantRecipe(ItemStack(Material.DIAMOND_BLOCK, 2), 12) + //给配方指定交易所需放物品 + merchantRecipe1.ingredients = listOf(ItemStack(Material.COARSE_DIRT, 3)) + val merchantRecipe2 = MerchantRecipe(ItemStack(Material.FEATHER, 2), 12) + merchantRecipe2.ingredients = + listOf(ItemStack(Material.COARSE_DIRT, 3), ItemStack(Material.DIRT, 1)) + return listOf( + merchantRecipe1, + merchantRecipe2 + ) + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..cf77bde --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,72 @@ +name: Hello-Paper +version: '${project.version}' +main: xyz.fortern.HelloPaper +api-version: 1.20 +authors: [ Fortern ] +description: Fortern第一次尝试开发插件 + +#定义详细的指令信息 +commands: + #指令名 + suicide: + #指令描述 + description: 自杀 + #错误使用提示 + usage: 直接输入/用于自杀 + #指令别名 + aliases: [ killme, killmyself ] + #指令权限,不写默认为OP + permission: fortern.suicide + #无权限提示 + permission-message: 你没有权限~ + god: + description: 在上帝模式与普通模式间切换 + usage: 输入/用于在上帝模式与普通模式间切换 + aliases: + - g + - g1 + - g2 + permission: fortern.op + permission-message: 你没有权限! + open: + description: 打开玩家的私人背包 + usage: /用于打开玩家的私人背包 + permission: fortern.common + award: + description: 在某玩家背包中生成特殊物品 + usage: / [player_name] + villager: + description: 生成一个可以交易特殊物品的村民 + usage: /用于生成一只可以交易特殊物品的村民,仅管理员可用 + permission: fortern.op + stuck: + description: 执行同步或异步线程 + usage: / [1|2|3...] + print: + description: 打印一条消息的命令 + title: + description: 显示标题的命令 + boss: + description: 产生一个boss条 + score: + description: 创建自定义的计分板 + strengthen: + description: 获取或移除一个超级物品。只有玩家可以执行该指令。 + usage: / [add|remove] + my-nbt: + description: 自定义NBT标签,只有玩家可以执行该指令 + usage: / [get|set] + spawn: + description: 生成一个下落的方块 + +#permissions权限声明 +permissions: + fortern.suicide: + description: 可以自杀的权限 + default: true #所有玩家 + fortern.op: + description: op的权限 + default: op + fortern.common: + description: 普通玩家权限 + default: true