From 113f0a1ede1ec6aca7b5bc241ccbb6d283441416 Mon Sep 17 00:00:00 2001 From: Timi Date: Wed, 16 Jul 2025 14:17:18 +0800 Subject: [PATCH] Initial project for 1.9.4+ --- .gitignore | 128 ++--- .idea/.gitignore | 3 + .idea/encodings.xml | 7 + .idea/misc.xml | 17 + .idea/uiDesigner.xml | 124 +++++ .idea/vcs.xml | 6 + README.md | 4 +- pom.xml | 82 ++++ .../cn/forevermc/spigot/core/FMCCore.java | 44 ++ .../cn/forevermc/spigot/core/FMCPlugin.java | 121 +++++ .../spigot/core/bean/BoundingBox.java | 437 ++++++++++++++++++ .../spigot/core/bean/ConfigPath.java | 17 + .../spigot/core/bean/NameEntity.java | 130 ++++++ .../cn/forevermc/spigot/core/bean/Region.java | 179 +++++++ .../spigot/core/command/AbstractCommand.java | 138 ++++++ .../spigot/core/command/ConsoleCommand.java | 8 + .../core/command/ConsolePlayerCommand.java | 8 + .../spigot/core/command/FMCCommand.java | 17 + .../core/command/FMCCommandExecutor.java | 83 ++++ .../spigot/core/command/OPCommand.java | 8 + .../spigot/core/command/PlayerCommand.java | 8 + .../core/exception/ArgsValueException.java | 15 + .../core/serializable/LocationSerializer.java | 48 ++ .../core/serializable/RegionSerializer.java | 41 ++ .../cn/forevermc/spigot/core/util/Ref.java | 255 ++++++++++ .../cn/forevermc/spigot/core/util/Util.java | 163 +++++++ src/main/resources/plugin.yml | 9 + 27 files changed, 2004 insertions(+), 96 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/encodings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/uiDesigner.xml create mode 100644 .idea/vcs.xml create mode 100644 pom.xml create mode 100644 src/main/java/cn/forevermc/spigot/core/FMCCore.java create mode 100644 src/main/java/cn/forevermc/spigot/core/FMCPlugin.java create mode 100644 src/main/java/cn/forevermc/spigot/core/bean/BoundingBox.java create mode 100644 src/main/java/cn/forevermc/spigot/core/bean/ConfigPath.java create mode 100644 src/main/java/cn/forevermc/spigot/core/bean/NameEntity.java create mode 100644 src/main/java/cn/forevermc/spigot/core/bean/Region.java create mode 100644 src/main/java/cn/forevermc/spigot/core/command/AbstractCommand.java create mode 100644 src/main/java/cn/forevermc/spigot/core/command/ConsoleCommand.java create mode 100644 src/main/java/cn/forevermc/spigot/core/command/ConsolePlayerCommand.java create mode 100644 src/main/java/cn/forevermc/spigot/core/command/FMCCommand.java create mode 100644 src/main/java/cn/forevermc/spigot/core/command/FMCCommandExecutor.java create mode 100644 src/main/java/cn/forevermc/spigot/core/command/OPCommand.java create mode 100644 src/main/java/cn/forevermc/spigot/core/command/PlayerCommand.java create mode 100644 src/main/java/cn/forevermc/spigot/core/exception/ArgsValueException.java create mode 100644 src/main/java/cn/forevermc/spigot/core/serializable/LocationSerializer.java create mode 100644 src/main/java/cn/forevermc/spigot/core/serializable/RegionSerializer.java create mode 100644 src/main/java/cn/forevermc/spigot/core/util/Ref.java create mode 100644 src/main/java/cn/forevermc/spigot/core/util/Util.java create mode 100644 src/main/resources/plugin.yml diff --git a/.gitignore b/.gitignore index c6d98d1..5ff6309 100644 --- a/.gitignore +++ b/.gitignore @@ -1,98 +1,38 @@ -# ---> JetBrains -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -# ---> Maven target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar -.mvn/wrapper/maven-wrapper.jar +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ -# Eclipse m2e generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated .classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..e7c58c2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 1fe1e4c..ebc3725 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# FMCCore +# FMCCore - ForeverMC 服务器插件核心依赖 -Minecraft Spigot 插件依赖 \ No newline at end of file +主分支仅作说明和向导,适用于不同 Minecraft 版本的插件源码在以该版本命名的分支中维护。 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a5c0d8e --- /dev/null +++ b/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + cn.forevermc.spigot + fmc-core + 0.0.1+${mc.version} + jar + + + 1.9.4+ + 8 + 8 + 8 + UTF-8 + + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + E:\SpigotMC\test194\plugins + FMCCore-${version}.jar + + jar-with-dependencies + + + + + package + + single + + + + + + + + + + org.spigotmc + spigot-api + 1.9.4-R0.1-SNAPSHOT + provided + + + org.projectlombok + lombok + 1.18.34 + provided + + + net.objecthunter + exp4j + 0.4.8 + + + org.dom4j + dom4j + 2.1.4 + + + xml-apis + xml-apis + + + + + diff --git a/src/main/java/cn/forevermc/spigot/core/FMCCore.java b/src/main/java/cn/forevermc/spigot/core/FMCCore.java new file mode 100644 index 0000000..04f40c1 --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/FMCCore.java @@ -0,0 +1,44 @@ +package cn.forevermc.spigot.core; + +import cn.forevermc.spigot.core.command.FMCCommand; +import cn.forevermc.spigot.core.command.FMCCommandExecutor; +import lombok.Getter; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * + * + * @author 夜雨 + * @since 2024-10-10 15:05 + */ +public final class FMCCore extends JavaPlugin { + + private static final List FMC_PLUGIN_LIST = new ArrayList<>(); + + @Getter + private static FMCCore instance; + + @Getter + private FMCCommand fmcCommand; + + public static void register(T fmcPlugin) { + FMC_PLUGIN_LIST.add(fmcPlugin); + } + + @Override + public void onEnable() { + instance = this; + + fmcCommand = new FMCCommand(); + + FMCCommandExecutor commandExecutor = new FMCCommandExecutor(); + for (int i = 0; i < FMC_PLUGIN_LIST.size(); i++) { + FMC_PLUGIN_LIST.get(i).fmcCommand = fmcCommand; + } + Objects.requireNonNull(getCommand("fmc")).setExecutor(commandExecutor); + } +} diff --git a/src/main/java/cn/forevermc/spigot/core/FMCPlugin.java b/src/main/java/cn/forevermc/spigot/core/FMCPlugin.java new file mode 100644 index 0000000..65d7eda --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/FMCPlugin.java @@ -0,0 +1,121 @@ +package cn.forevermc.spigot.core; + +import cn.forevermc.spigot.core.bean.ConfigPath; +import cn.forevermc.spigot.core.command.FMCCommand; +import cn.forevermc.spigot.core.util.Ref; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author 夜雨 + * @since 2024-10-19 16:07 + */ +public abstract class FMCPlugin extends JavaPlugin { + + protected FMCCommand fmcCommand; + + public FMCPlugin() { + } + + protected final void defaultConfig(String path) { + try { + InputStream is = getClass().getClassLoader().getResourceAsStream(path); + StringBuilder sb = new StringBuilder(); + assert is != null; + BufferedInputStream bis = new BufferedInputStream(is); + InputStreamReader isr = new InputStreamReader(bis, StandardCharsets.UTF_8); + char[] buffer = new char[4096]; + int l; + while ((l = isr.read(buffer)) != -1) { + sb.append(buffer, 0, l); + } + isr.close(); + bis.close(); + is.close(); + + FileConfiguration config = getConfig(); + YamlConfiguration defConfig = new YamlConfiguration(); + defConfig.options().indent(4); + defConfig.loadFromString(sb.toString()); + config.addDefaults(defConfig); + config.options().copyDefaults(true); + super.saveConfig(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected final void saveConfigAs(Object obj, FileConfiguration config) { + try { + serializeConfig(obj, config); + super.saveConfig(); + } catch (Exception e) { + throw new RuntimeException("serialize config error", e); + } + } + + protected final T loadConfigAs(Class clazz, FileConfiguration config) { + try { + T t = Ref.newInstance(clazz); + deserializeConfig(t, config); + return t; + } catch (Exception e) { + throw new RuntimeException("deserialize config error", e); + } + } + + private void serializeConfig(Object obj, FileConfiguration config) throws Exception { + List fieldList = Ref.listFields(obj.getClass()); + for (int i = 0; i < fieldList.size(); i++) { + Field field = fieldList.get(i); + ConfigPath configPath = field.getAnnotation(ConfigPath.class); + if (configPath == null) { + serializeConfig(Ref.getFieldValue(obj, field, Object.class), config); + } else { + if (Map.class.isAssignableFrom(field.getType())) { + Map map = Ref.getFieldValue(obj, field, Map.class); + map.forEach((key, value) -> config.set(configPath.value() + "." + key.toString(), value)); + } else { + config.set(configPath.value(), Ref.getFieldValue(obj, field, Object.class)); + } + } + } + } + + private void deserializeConfig(Object obj, FileConfiguration config) throws Exception { + List fieldList = Ref.listFields(obj.getClass()); + for (int i = 0; i < fieldList.size(); i++) { + Field field = fieldList.get(i); + ConfigPath configPath = field.getAnnotation(ConfigPath.class); + if (configPath == null) { + Ref.setFieldValue(obj, field, Ref.newInstance(field.getType())); + deserializeConfig(field.get(obj), config); + } else { + String configPathVal = configPath.value(); + if (Map.class.isAssignableFrom(field.getType())) { + Map map = new HashMap<>(); + Set keys = config.getKeys(true); + for (String key : keys) { + if (key.length() != configPathVal.length() && key.startsWith(configPathVal)) { + map.put(key.substring(configPathVal.length() + 1), config.get(key)); + } + } + Ref.setFieldValue(obj, field, map); + } else { + Ref.setFieldValue(obj, field, config.get(configPathVal)); + } + } + } + } +} diff --git a/src/main/java/cn/forevermc/spigot/core/bean/BoundingBox.java b/src/main/java/cn/forevermc/spigot/core/bean/BoundingBox.java new file mode 100644 index 0000000..05345d0 --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/bean/BoundingBox.java @@ -0,0 +1,437 @@ +package cn.forevermc.spigot.core.bean; + +import lombok.Getter; +import org.apache.commons.lang.Validate; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.configuration.serialization.SerializableAs; +import org.bukkit.util.NumberConversions; +import org.bukkit.util.Vector; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** Copy for 1.13.2 org.bukkit.util.BoundingBox */ +@Getter +@SerializableAs("BoundingBox") +public class BoundingBox implements Cloneable, ConfigurationSerializable { + + private double minX; + private double minY; + private double minZ; + private double maxX; + private double maxY; + private double maxZ; + + public static BoundingBox of(Vector corner1, Vector corner2) { + Validate.notNull(corner1, "Corner1 is null!"); + Validate.notNull(corner2, "Corner2 is null!"); + return new BoundingBox(corner1.getX(), corner1.getY(), corner1.getZ(), corner2.getX(), corner2.getY(), corner2.getZ()); + } + + public static BoundingBox of(Location corner1, Location corner2) { + Validate.notNull(corner1, "Corner1 is null!"); + Validate.notNull(corner2, "Corner2 is null!"); + Validate.isTrue(Objects.equals(corner1.getWorld(), corner2.getWorld()), "Locations from different worlds!"); + return new BoundingBox(corner1.getX(), corner1.getY(), corner1.getZ(), corner2.getX(), corner2.getY(), corner2.getZ()); + } + + public static BoundingBox of(Block corner1, Block corner2) { + Validate.notNull(corner1, "Corner1 is null!"); + Validate.notNull(corner2, "Corner2 is null!"); + Validate.isTrue(Objects.equals(corner1.getWorld(), corner2.getWorld()), "Blocks from different worlds!"); + int x1 = corner1.getX(); + int y1 = corner1.getY(); + int z1 = corner1.getZ(); + int x2 = corner2.getX(); + int y2 = corner2.getY(); + int z2 = corner2.getZ(); + int minX = Math.min(x1, x2); + int minY = Math.min(y1, y2); + int minZ = Math.min(z1, z2); + int maxX = Math.max(x1, x2) + 1; + int maxY = Math.max(y1, y2) + 1; + int maxZ = Math.max(z1, z2) + 1; + return new BoundingBox(minX, minY, minZ, maxX, maxY, maxZ); + } + + public static BoundingBox of(Block block) { + Validate.notNull(block, "Block is null!"); + return new BoundingBox(block.getX(), block.getY(), block.getZ(), block.getX() + 1, block.getY() + 1, block.getZ() + 1); + } + + public static BoundingBox of(Vector center, double x, double y, double z) { + Validate.notNull(center, "Center is null!"); + return new BoundingBox(center.getX() - x, center.getY() - y, center.getZ() - z, center.getX() + x, center.getY() + y, center.getZ() + z); + } + + public static BoundingBox of(Location center, double x, double y, double z) { + Validate.notNull(center, "Center is null!"); + return new BoundingBox(center.getX() - x, center.getY() - y, center.getZ() - z, center.getX() + x, center.getY() + y, center.getZ() + z); + } + + public BoundingBox() { + this.resize(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + } + + public BoundingBox(double x1, double y1, double z1, double x2, double y2, double z2) { + this.resize(x1, y1, z1, x2, y2, z2); + } + + public BoundingBox resize(double x1, double y1, double z1, double x2, double y2, double z2) { + NumberConversions.checkFinite(x1, "x1 not finite"); + NumberConversions.checkFinite(y1, "y1 not finite"); + NumberConversions.checkFinite(z1, "z1 not finite"); + NumberConversions.checkFinite(x2, "x2 not finite"); + NumberConversions.checkFinite(y2, "y2 not finite"); + NumberConversions.checkFinite(z2, "z2 not finite"); + this.minX = Math.min(x1, x2); + this.minY = Math.min(y1, y2); + this.minZ = Math.min(z1, z2); + this.maxX = Math.max(x1, x2); + this.maxY = Math.max(y1, y2); + this.maxZ = Math.max(z1, z2); + return this; + } + + public Vector getMin() { + return new Vector(this.minX, this.minY, this.minZ); + } + + public Vector getMax() { + return new Vector(this.maxX, this.maxY, this.maxZ); + } + + public double getWidthX() { + return this.maxX - this.minX; + } + + public double getWidthZ() { + return this.maxZ - this.minZ; + } + + public double getHeight() { + return this.maxY - this.minY; + } + + public double getVolume() { + return this.getHeight() * this.getWidthX() * this.getWidthZ(); + } + + public double getCenterX() { + return this.minX + this.getWidthX() * 0.5; + } + + public double getCenterY() { + return this.minY + this.getHeight() * 0.5; + } + + public double getCenterZ() { + return this.minZ + this.getWidthZ() * 0.5; + } + + public Vector getCenter() { + return new Vector(this.getCenterX(), this.getCenterY(), this.getCenterZ()); + } + + public BoundingBox copy(BoundingBox other) { + Validate.notNull(other, "Other bounding box is null!"); + return this.resize(other.getMinX(), other.getMinY(), other.getMinZ(), other.getMaxX(), other.getMaxY(), other.getMaxZ()); + } + + public BoundingBox expand(double negativeX, double negativeY, double negativeZ, double positiveX, double positiveY, double positiveZ) { + if (negativeX == 0.0 && negativeY == 0.0 && negativeZ == 0.0 && positiveX == 0.0 && positiveY == 0.0 && positiveZ == 0.0) { + return this; + } else { + double newMinX = this.minX - negativeX; + double newMinY = this.minY - negativeY; + double newMinZ = this.minZ - negativeZ; + double newMaxX = this.maxX + positiveX; + double newMaxY = this.maxY + positiveY; + double newMaxZ = this.maxZ + positiveZ; + double centerZ; + if (newMinX > newMaxX) { + centerZ = this.getCenterX(); + if (newMaxX >= centerZ) { + newMinX = newMaxX; + } else if (newMinX <= centerZ) { + newMaxX = newMinX; + } else { + newMinX = centerZ; + newMaxX = centerZ; + } + } + if (newMinY > newMaxY) { + centerZ = this.getCenterY(); + if (newMaxY >= centerZ) { + newMinY = newMaxY; + } else if (newMinY <= centerZ) { + newMaxY = newMinY; + } else { + newMinY = centerZ; + newMaxY = centerZ; + } + } + if (newMinZ > newMaxZ) { + centerZ = this.getCenterZ(); + if (newMaxZ >= centerZ) { + newMinZ = newMaxZ; + } else if (newMinZ <= centerZ) { + newMaxZ = newMinZ; + } else { + newMinZ = centerZ; + newMaxZ = centerZ; + } + } + + return this.resize(newMinX, newMinY, newMinZ, newMaxX, newMaxY, newMaxZ); + } + } + + public BoundingBox expand(double x, double y, double z) { + return this.expand(x, y, z, x, y, z); + } + + public BoundingBox expand(Vector expansion) { + Validate.notNull(expansion, "Expansion is null!"); + double x = expansion.getX(); + double y = expansion.getY(); + double z = expansion.getZ(); + return this.expand(x, y, z, x, y, z); + } + + public BoundingBox expand(double expansion) { + return this.expand(expansion, expansion, expansion, expansion, expansion, expansion); + } + + public BoundingBox expand(double dirX, double dirY, double dirZ, double expansion) { + if (expansion == 0.0) { + return this; + } else if (dirX == 0.0 && dirY == 0.0 && dirZ == 0.0) { + return this; + } else { + double negativeX = dirX < 0.0 ? -dirX * expansion : 0.0; + double negativeY = dirY < 0.0 ? -dirY * expansion : 0.0; + double negativeZ = dirZ < 0.0 ? -dirZ * expansion : 0.0; + double positiveX = dirX > 0.0 ? dirX * expansion : 0.0; + double positiveY = dirY > 0.0 ? dirY * expansion : 0.0; + double positiveZ = dirZ > 0.0 ? dirZ * expansion : 0.0; + return this.expand(negativeX, negativeY, negativeZ, positiveX, positiveY, positiveZ); + } + } + + public BoundingBox expand(Vector direction, double expansion) { + Validate.notNull(direction, "Direction is null!"); + return this.expand(direction.getX(), direction.getY(), direction.getZ(), expansion); + } + + public BoundingBox expandDirectional(double dirX, double dirY, double dirZ) { + return this.expand(dirX, dirY, dirZ, 1.0); + } + + public BoundingBox expandDirectional(Vector direction) { + Validate.notNull(direction, "Expansion is null!"); + return this.expand(direction.getX(), direction.getY(), direction.getZ(), 1.0); + } + + public BoundingBox union(double posX, double posY, double posZ) { + double newMinX = Math.min(this.minX, posX); + double newMinY = Math.min(this.minY, posY); + double newMinZ = Math.min(this.minZ, posZ); + double newMaxX = Math.max(this.maxX, posX); + double newMaxY = Math.max(this.maxY, posY); + double newMaxZ = Math.max(this.maxZ, posZ); + return newMinX == this.minX && newMinY == this.minY && newMinZ == this.minZ && newMaxX == this.maxX && newMaxY == this.maxY && newMaxZ == this.maxZ ? this : this.resize(newMinX, newMinY, newMinZ, newMaxX, newMaxY, newMaxZ); + } + + public BoundingBox union(Vector position) { + Validate.notNull(position, "Position is null!"); + return this.union(position.getX(), position.getY(), position.getZ()); + } + + public BoundingBox union(Location position) { + Validate.notNull(position, "Position is null!"); + return this.union(position.getX(), position.getY(), position.getZ()); + } + + public BoundingBox union(BoundingBox other) { + Validate.notNull(other, "Other bounding box is null!"); + if (this.contains(other)) { + return this; + } else { + double newMinX = Math.min(this.minX, other.minX); + double newMinY = Math.min(this.minY, other.minY); + double newMinZ = Math.min(this.minZ, other.minZ); + double newMaxX = Math.max(this.maxX, other.maxX); + double newMaxY = Math.max(this.maxY, other.maxY); + double newMaxZ = Math.max(this.maxZ, other.maxZ); + return this.resize(newMinX, newMinY, newMinZ, newMaxX, newMaxY, newMaxZ); + } + } + + public BoundingBox intersection(BoundingBox other) { + Validate.notNull(other, "Other bounding box is null!"); + Validate.isTrue(this.overlaps(other), "The bounding boxes do not overlap!"); + double newMinX = Math.max(this.minX, other.minX); + double newMinY = Math.max(this.minY, other.minY); + double newMinZ = Math.max(this.minZ, other.minZ); + double newMaxX = Math.min(this.maxX, other.maxX); + double newMaxY = Math.min(this.maxY, other.maxY); + double newMaxZ = Math.min(this.maxZ, other.maxZ); + return this.resize(newMinX, newMinY, newMinZ, newMaxX, newMaxY, newMaxZ); + } + + public BoundingBox shift(double shiftX, double shiftY, double shiftZ) { + return shiftX == 0.0 && shiftY == 0.0 && shiftZ == 0.0 ? this : this.resize(this.minX + shiftX, this.minY + shiftY, this.minZ + shiftZ, this.maxX + shiftX, this.maxY + shiftY, this.maxZ + shiftZ); + } + + public BoundingBox shift(Vector shift) { + Validate.notNull(shift, "Shift is null!"); + return this.shift(shift.getX(), shift.getY(), shift.getZ()); + } + + public BoundingBox shift(Location shift) { + Validate.notNull(shift, "Shift is null!"); + return this.shift(shift.getX(), shift.getY(), shift.getZ()); + } + + private boolean overlaps(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + return this.minX < maxX && this.maxX > minX && this.minY < maxY && this.maxY > minY && this.minZ < maxZ && this.maxZ > minZ; + } + + public boolean overlaps(BoundingBox other) { + Validate.notNull(other, "Other bounding box is null!"); + return this.overlaps(other.minX, other.minY, other.minZ, other.maxX, other.maxY, other.maxZ); + } + + public boolean overlaps(Vector min, Vector max) { + Validate.notNull(min, "Min is null!"); + Validate.notNull(max, "Max is null!"); + double x1 = min.getX(); + double y1 = min.getY(); + double z1 = min.getZ(); + double x2 = max.getX(); + double y2 = max.getY(); + double z2 = max.getZ(); + return this.overlaps(Math.min(x1, x2), Math.min(y1, y2), Math.min(z1, z2), Math.max(x1, x2), Math.max(y1, y2), Math.max(z1, z2)); + } + + public boolean contains(double x, double y, double z) { + return x >= this.minX && x < this.maxX && y >= this.minY && y < this.maxY && z >= this.minZ && z < this.maxZ; + } + + public boolean contains(Vector position) { + Validate.notNull(position, "Position is null!"); + return this.contains(position.getX(), position.getY(), position.getZ()); + } + + private boolean contains(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + return this.minX <= minX && this.maxX >= maxX && this.minY <= minY && this.maxY >= maxY && this.minZ <= minZ && this.maxZ >= maxZ; + } + + public boolean contains(BoundingBox other) { + Validate.notNull(other, "Other bounding box is null!"); + return this.contains(other.minX, other.minY, other.minZ, other.maxX, other.maxY, other.maxZ); + } + + public boolean contains(Vector min, Vector max) { + Validate.notNull(min, "Min is null!"); + Validate.notNull(max, "Max is null!"); + double x1 = min.getX(); + double y1 = min.getY(); + double z1 = min.getZ(); + double x2 = max.getX(); + double y2 = max.getY(); + double z2 = max.getZ(); + return this.contains(Math.min(x1, x2), Math.min(y1, y2), Math.min(z1, z2), Math.max(x1, x2), Math.max(y1, y2), Math.max(z1, z2)); + } + + public int hashCode() { + int result = 1; + long temp = Double.doubleToLongBits(this.maxX); + result = 31 * result + (int) (temp ^ temp >>> 32); + temp = Double.doubleToLongBits(this.maxY); + result = 31 * result + (int) (temp ^ temp >>> 32); + temp = Double.doubleToLongBits(this.maxZ); + result = 31 * result + (int) (temp ^ temp >>> 32); + temp = Double.doubleToLongBits(this.minX); + result = 31 * result + (int) (temp ^ temp >>> 32); + temp = Double.doubleToLongBits(this.minY); + result = 31 * result + (int) (temp ^ temp >>> 32); + temp = Double.doubleToLongBits(this.minZ); + result = 31 * result + (int) (temp ^ temp >>> 32); + return result; + } + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof BoundingBox)) { + return false; + } else { + BoundingBox other = (BoundingBox) obj; + if (Double.doubleToLongBits(this.maxX) != Double.doubleToLongBits(other.maxX)) { + return false; + } else if (Double.doubleToLongBits(this.maxY) != Double.doubleToLongBits(other.maxY)) { + return false; + } else if (Double.doubleToLongBits(this.maxZ) != Double.doubleToLongBits(other.maxZ)) { + return false; + } else if (Double.doubleToLongBits(this.minX) != Double.doubleToLongBits(other.minX)) { + return false; + } else if (Double.doubleToLongBits(this.minY) != Double.doubleToLongBits(other.minY)) { + return false; + } else { + return Double.doubleToLongBits(this.minZ) == Double.doubleToLongBits(other.minZ); + } + } + } + + public BoundingBox clone() { + try { + return (BoundingBox) super.clone(); + } catch (CloneNotSupportedException var2) { + throw new Error(var2); + } + } + + public Map serialize() { + Map result = new LinkedHashMap<>(); + result.put("minX", this.minX); + result.put("minY", this.minY); + result.put("minZ", this.minZ); + result.put("maxX", this.maxX); + result.put("maxY", this.maxY); + result.put("maxZ", this.maxZ); + return result; + } + + public static BoundingBox deserialize(Map args) { + double minX = 0.0; + double minY = 0.0; + double minZ = 0.0; + double maxX = 0.0; + double maxY = 0.0; + double maxZ = 0.0; + if (args.containsKey("minX")) { + minX = ((Number) args.get("minX")).doubleValue(); + } + if (args.containsKey("minY")) { + minY = ((Number) args.get("minY")).doubleValue(); + } + if (args.containsKey("minZ")) { + minZ = ((Number) args.get("minZ")).doubleValue(); + } + if (args.containsKey("maxX")) { + maxX = ((Number) args.get("maxX")).doubleValue(); + } + if (args.containsKey("maxY")) { + maxY = ((Number) args.get("maxY")).doubleValue(); + } + if (args.containsKey("maxZ")) { + maxZ = ((Number) args.get("maxZ")).doubleValue(); + } + return new BoundingBox(minX, minY, minZ, maxX, maxY, maxZ); + } +} diff --git a/src/main/java/cn/forevermc/spigot/core/bean/ConfigPath.java b/src/main/java/cn/forevermc/spigot/core/bean/ConfigPath.java new file mode 100644 index 0000000..207bb98 --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/bean/ConfigPath.java @@ -0,0 +1,17 @@ +package cn.forevermc.spigot.core.bean; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author 夜雨 + * @since 2025-01-19 21:57 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ConfigPath { + + String value(); +} diff --git a/src/main/java/cn/forevermc/spigot/core/bean/NameEntity.java b/src/main/java/cn/forevermc/spigot/core/bean/NameEntity.java new file mode 100644 index 0000000..13d6ead --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/bean/NameEntity.java @@ -0,0 +1,130 @@ +package cn.forevermc.spigot.core.bean; + +import cn.forevermc.spigot.core.FMCCore; +import cn.forevermc.spigot.core.util.Util; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; + +import java.util.Objects; +import java.util.UUID; + +/** + * 命名化实体,用于绑定和持久化储存 + * + * @author 夜雨 + * @since 2024-08-29 16:50 + */ +public class NameEntity { + + /** 实体 UUID */ + @Getter + protected String uuid; + + /** 名称 */ + @Setter + @Getter + protected String name; + + /** 显示名称 */ + @Setter + @Getter + protected String displayName; + + /** 类型 */ + @Setter + @Getter + protected EntityType type; + + /** 位置 */ + @Setter + @Getter + protected Location location; + + /** true 为启用 */ + @Setter + @Getter + protected boolean enable; + + /** UUID */ + protected transient UUID uuidObj; + + /** 实体(禁用或未生成时可能不存在) */ + @Getter + protected transient Entity entity; + + /** + * 发光 + * + * @param ttl 持续时间(TPS) + */ + public void glowing(int ttl) { + entity.setGlowing(true); + Bukkit.getScheduler().runTaskLater(FMCCore.getInstance(), () -> entity.setGlowing(false), ttl); + } + + /** + * 传送至新的位置 + * + * @param location 位置 + */ + public void teleportTo(Location location) { + this.location = location; + entity.teleport(location); + } + + /** @return true 为存活于世界中 */ + public boolean isAlive() { + return get() != null; + } + + /** 生成实体 */ + public Entity spawn() { + Entity entity = Objects.requireNonNull(location.getWorld()).spawnEntity(location, type); + entity.setCustomName(displayName); + entity.setCustomNameVisible(true); + entity.setInvulnerable(true); + this.uuidObj = entity.getUniqueId(); + this.uuid = uuidObj.toString(); + this.entity = entity; + return entity; + } + + /** 重新生成 */ + public Entity respawn() { + kill(); + return spawn(); + } + + /** @return 获取实体 */ + public Entity get() { + return Util.getEntityByUID(getUID()); + } + + /** @return 实体 UUID */ + public UUID getUID() { + if (uuidObj == null) { + uuidObj = UUID.fromString(uuid); + } + return uuidObj; + } + + /** 击杀 */ + public void kill() { + get().remove(); + } + + /** + * 设置实体对象 + * + * @param entity 实体 + */ + public void setEntity(Entity entity) { + this.entity = entity; + this.uuidObj = entity.getUniqueId(); + this.uuid = this.uuidObj.toString(); + } +} diff --git a/src/main/java/cn/forevermc/spigot/core/bean/Region.java b/src/main/java/cn/forevermc/spigot/core/bean/Region.java new file mode 100644 index 0000000..121e16f --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/bean/Region.java @@ -0,0 +1,179 @@ +package cn.forevermc.spigot.core.bean; + +import lombok.Getter; +import lombok.Setter; +import org.bukkit.Location; +import org.bukkit.World; + +import java.util.Objects; + +/** + * 区域(两点产生的区间,可能是线、面、长方体) + * + * @author 夜雨 + * @since 2024-08-19 00:38 + */ +public class Region implements Cloneable { + + /** + * 遍历区域回调 + * + * @author 夜雨 + * @since 2024-08-19 15:06 + */ + public interface ForEachCallback { + + /** + * 执行器 + * + * @param world 世界 + * @param x X 坐标 + * @param y Y 坐标 + * @param z Z 坐标 + */ + void handler(World world, double x, double y, double z); + } + + /** 区域顶点 */ + @Setter + @Getter + private Location left, right; + + /** 碰撞箱 */ + private transient BoundingBox boundingBox; + + /** 安全区域(必须已选两点且同一世界) */ + private void assetSafeLocation() { + if (isFull() && !Objects.equals(left.getWorld(), right.getWorld())) { + throw new IllegalArgumentException("无法执行:选区不完整或世界不一致"); + } + } + + /** + * 遍历区域 + * + * @param callback 遍历回调 + */ + public void forEach(ForEachCallback callback) { + final World world = left.getWorld(); + if (world == null) { + return; + } + assetSafeLocation(); + + final int x1 = left.getBlockX(); + final int y1 = left.getBlockY(); + final int z1 = left.getBlockZ(); + final int x2 = right.getBlockX(); + final int y2 = right.getBlockY(); + final int z2 = right.getBlockZ(); + + for (int x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) { + for (int y = Math.min(y1, y2); y <= Math.max(y1, y2); y++) { + for (int z = Math.min(z1, z2); z <= Math.max(z1, z2); z++) { + callback.handler(world, x, y, z); + } + } + } + } + + /** + * 坐标是否在区域内 + * + * @param location 位置 + * @return true 为该坐标在区域内 + */ + public boolean contain(Location location) { + return contain(location.getX(), location.getY(), location.getZ()); + } + + /** + * 坐标是否在区域内 + * + * @param x X 坐标 + * @param y Y 坐标 + * @param z Z 坐标 + * @return true 为该坐标在区域内 + */ + public boolean contain(double x, double y, double z) { + if (!isFull()) { + return false; + } + assetSafeLocation(); + return getBoundingBox().contains(x, y, z); + } + + /** @return 获取区域碰撞箱 */ + public BoundingBox getBoundingBox() { + assetSafeLocation(); + if (boundingBox == null) { + final int x1 = left.getBlockX(); + final int z1 = left.getBlockZ(); + final int y1 = left.getBlockY(); + final int x2 = right.getBlockX(); + final int y2 = right.getBlockY(); + final int z2 = right.getBlockZ(); + + final double minX = Math.min(x1, x2); + final double minY = Math.min(y1, y2); + final double minZ = Math.min(z1, z2); + final double maxX = Math.max(x1, x2) + .99; + final double maxY = Math.max(y1, y2) + .99; + final double maxZ = Math.max(z1, z2) + .99; + + boundingBox = new BoundingBox(minX, minY, minZ, maxX, maxY, maxZ); + } + return boundingBox; + } + + /** @return true 为没有选择区域 */ + public boolean isEmpty() { + return left == null && right == null; + } + + /** @return true 为仅选择一点 */ + public boolean isHalf() { + return !isEmpty() && !isFull(); + } + + /** @return true 为已选择两点区域 */ + public boolean isFull() { + return left != null && right != null; + } + + /** 清空顶点 */ + public void clear() { + left = right = null; + } + + /** @return 获取顶点 1 */ + public Location getP1() { + return left; + } + + /** @return 获取顶点 2 */ + public Location getP2() { + return right; + } + + /** @return 中间位置 */ + public Location getCenter() { + double centerX = (left.getX() + right.getX()) * .5; + double centerY = (left.getY() + right.getY()) * .5; + double centerZ = (left.getZ() + right.getZ()) * .5; + return new Location(left.getWorld(), centerX, centerY, centerZ); + } + + /** @return 克隆区域 */ + @Override + public Region clone() { + try { + Region clone = (Region) super.clone(); + clone.left = left.clone(); + clone.right = right.clone(); + return clone; + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/cn/forevermc/spigot/core/command/AbstractCommand.java b/src/main/java/cn/forevermc/spigot/core/command/AbstractCommand.java new file mode 100644 index 0000000..ee40506 --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/command/AbstractCommand.java @@ -0,0 +1,138 @@ +package cn.forevermc.spigot.core.command; + +import cn.forevermc.spigot.core.FMCCore; +import cn.forevermc.spigot.core.exception.ArgsValueException; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; + +/** + * 抽象指令 + * + * @author 夜雨 + * @since 2024-08-15 09:59 + */ +public abstract class AbstractCommand { + + /** 指令参数提示(子级) */ + protected final Map> argsTabCompleterMap = new HashMap<>(); + + /** 子级 */ + protected final Map childrenCommandMap = new HashMap<>(); + + // ---------- 实时参数(每次触发更新) ---------- + + /** 发送玩家 */ + protected Player senderPlayer; + + /** 动作 */ + protected List actionList; + + /** 发送者 */ + protected CommandSender sender; + + /** 触发指令,由 {@link FMCCommandExecutor} 调度 */ + final void run0(CommandSender sender, String[] args) { + this.sender = sender; + // 发送对象 + if (!(this instanceof ConsolePlayerCommand)) { + if (this instanceof PlayerCommand && !Player.class.isAssignableFrom(sender.getClass())) { + error("不允许的执行对象"); + } else { + senderPlayer = (Player) sender; + // 权限 + if (this instanceof OPCommand && !senderPlayer.isOp()) { + error("没有权限操作"); + return; + } + } + if (this instanceof ConsoleCommand && !ConsoleCommandSender.class.isAssignableFrom(sender.getClass())) { + error("不允许的执行对象"); + } + } + // 触发 + try { + run(args); + } catch (ArgsValueException e) { + error("参数错误:" + e.getMessage()); + } catch (Exception e) { + error("执行错误:" + e.getMessage()); + throw new RuntimeException("exec command error", e); + } + } + + /** + * 执行指令 + * + * @param args 执行参数 + */ + protected abstract void run(String[] args) throws ArgsValueException; + + protected void appendArgsTabCompleter(String... tips) { + appendArgsTabCompleter(Arrays.asList(tips)); + } + + protected void appendArgsTabCompleter(Collection list) { + argsTabCompleterMap.put(argsTabCompleterMap.size(), list); + } + + protected void putArgsTabCompleter(int index, Collection list) { + argsTabCompleterMap.put(index, list); + } + + protected void success(String msg) { + msg(Level.INFO, msg); + } + + protected void error(String msg) { + msg(Level.WARNING, msg); + } + + protected void msg(Level level, String msg) { + if (sender instanceof Player) { + Player player = (Player) sender; + ChatColor color = level == Level.INFO ? ChatColor.GREEN : ChatColor.RED; + player.spawnParticle(Particle.SMOKE_NORMAL, new Location(player.getWorld(), 0, 0, 0), 10); + player.sendMessage(color + msg); + } else { + FMCCore.getInstance().getLogger().log(level, msg); + } + } + + AbstractCommand getCommand(String command) { + return this.childrenCommandMap.get(command); + } + + public final boolean hasChildren() { + return !childrenCommandMap.isEmpty(); + } + + public List getTabCompleterList(CommandSender sender) { + List result = new ArrayList<>(); + if (sender instanceof Player && !sender.isOp()) { + for (Map.Entry item : childrenCommandMap.entrySet()) { + if (!OPCommand.class.isAssignableFrom(item.getValue().getClass())) { + result.add(item.getKey()); + } + } + } else { + result.addAll(childrenCommandMap.keySet()); + } + return result; + } + + public void appendChildren(String name, AbstractCommand command) { + childrenCommandMap.put(name, command); + } +} diff --git a/src/main/java/cn/forevermc/spigot/core/command/ConsoleCommand.java b/src/main/java/cn/forevermc/spigot/core/command/ConsoleCommand.java new file mode 100644 index 0000000..31ce8df --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/command/ConsoleCommand.java @@ -0,0 +1,8 @@ +package cn.forevermc.spigot.core.command; + +/** + * @author 夜雨 + * @since 2024-10-10 14:48 + */ +public abstract class ConsoleCommand extends AbstractCommand { +} diff --git a/src/main/java/cn/forevermc/spigot/core/command/ConsolePlayerCommand.java b/src/main/java/cn/forevermc/spigot/core/command/ConsolePlayerCommand.java new file mode 100644 index 0000000..a1a98b8 --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/command/ConsolePlayerCommand.java @@ -0,0 +1,8 @@ +package cn.forevermc.spigot.core.command; + +/** + * @author 夜雨 + * @since 2024-08-29 20:40 + */ +public abstract class ConsolePlayerCommand extends AbstractCommand { +} diff --git a/src/main/java/cn/forevermc/spigot/core/command/FMCCommand.java b/src/main/java/cn/forevermc/spigot/core/command/FMCCommand.java new file mode 100644 index 0000000..60bdc29 --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/command/FMCCommand.java @@ -0,0 +1,17 @@ +package cn.forevermc.spigot.core.command; + +/** + * ForeverMC 指令 + * + * @author 夜雨 + * @since 2024-08-15 10:21 + */ +public class FMCCommand extends AbstractCommand { + + public FMCCommand() { + } + + @Override + public void run(String[] args) { + } +} diff --git a/src/main/java/cn/forevermc/spigot/core/command/FMCCommandExecutor.java b/src/main/java/cn/forevermc/spigot/core/command/FMCCommandExecutor.java new file mode 100644 index 0000000..67b1842 --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/command/FMCCommandExecutor.java @@ -0,0 +1,83 @@ +package cn.forevermc.spigot.core.command; + +import cn.forevermc.spigot.core.FMCCore; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 指令执行器 + * + * @author 夜雨 + * @since 2024-08-15 19:39 + */ +public class FMCCommandExecutor implements CommandExecutor { + + public FMCCommandExecutor() { + FMCCore fmcCore = FMCCore.getInstance(); + // 指令提示 + Objects.requireNonNull(fmcCore.getCommand("fmc")).setTabCompleter((sender, command, label, args) -> { + AbstractCommand cmd = fmcCore.getFmcCommand(); + int i = 0; + + List result = new ArrayList<>(); + while (!cmd.childrenCommandMap.isEmpty()) { + final int j = i; + if (i == args.length - 1) { + result.addAll(cmd.getTabCompleterList(sender).stream().filter(item -> item.contains(args[j])).collect(Collectors.toList())); + break; + } + if (cmd.getCommand(args[i]) == null) { + break; + } + cmd = cmd.getCommand(args[i]); + i++; + } + // 无子级指令 + if (!cmd.argsTabCompleterMap.isEmpty()) { + Collection argsTabCompleterList = cmd.argsTabCompleterMap.get(args.length - 1 - i); + if (argsTabCompleterList != null) { + final String key = args[args.length - 1]; + result.addAll(argsTabCompleterList.stream().filter(item -> item != null && item.contains(key)).collect(Collectors.toList())); + } + } + return result; + }); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + List action = new ArrayList<>(); + AbstractCommand cmd = FMCCore.getInstance().getFmcCommand(); + if (args == null || args.length == 0) { + cmd.run0(sender, new String[0]); + } else { + for (int i = 0; i < args.length; i++) { + if (cmd.hasChildren() && cmd.getCommand(args[i]) != null) { + action.add(args[i]); + + cmd = cmd.getCommand(args[i]); + if (i == args.length - 1) { + String[] subArgs = new String[0]; + cmd.actionList = action; + cmd.run0(sender, subArgs); + } + } else { + // 全部视为参数 + String[] newArgs = new String[args.length - i]; + System.arraycopy(args, i, newArgs, 0, newArgs.length); + cmd.actionList = action; + cmd.run0(sender, newArgs); + break; + } + } + } + return true; + } +} diff --git a/src/main/java/cn/forevermc/spigot/core/command/OPCommand.java b/src/main/java/cn/forevermc/spigot/core/command/OPCommand.java new file mode 100644 index 0000000..376bfb6 --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/command/OPCommand.java @@ -0,0 +1,8 @@ +package cn.forevermc.spigot.core.command; + +/** + * @author 夜雨 + * @since 2024-10-10 14:43 + */ +public abstract class OPCommand extends PlayerCommand { +} diff --git a/src/main/java/cn/forevermc/spigot/core/command/PlayerCommand.java b/src/main/java/cn/forevermc/spigot/core/command/PlayerCommand.java new file mode 100644 index 0000000..b4cbe56 --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/command/PlayerCommand.java @@ -0,0 +1,8 @@ +package cn.forevermc.spigot.core.command; + +/** + * @author 夜雨 + * @since 2024-08-29 20:39 + */ +public abstract class PlayerCommand extends AbstractCommand { +} diff --git a/src/main/java/cn/forevermc/spigot/core/exception/ArgsValueException.java b/src/main/java/cn/forevermc/spigot/core/exception/ArgsValueException.java new file mode 100644 index 0000000..4e780d0 --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/exception/ArgsValueException.java @@ -0,0 +1,15 @@ +package cn.forevermc.spigot.core.exception; + +import lombok.NoArgsConstructor; + +/** + * @author 夜雨 + * @since 2024-08-31 00:20 + */ +@NoArgsConstructor +public class ArgsValueException extends Exception { + + public ArgsValueException(String message) { + super(message); + } +} diff --git a/src/main/java/cn/forevermc/spigot/core/serializable/LocationSerializer.java b/src/main/java/cn/forevermc/spigot/core/serializable/LocationSerializer.java new file mode 100644 index 0000000..5d70626 --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/serializable/LocationSerializer.java @@ -0,0 +1,48 @@ +package cn.forevermc.spigot.core.serializable; + +import cn.forevermc.spigot.core.FMCCore; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import org.bukkit.Location; +import org.bukkit.World; + +import java.lang.reflect.Type; +import java.util.Objects; +import java.util.UUID; + +/** + * Location 对象序列化和反序列化 + * + * @author 夜雨 + * @since 2024-08-29 10:21 + */ +public class LocationSerializer implements JsonSerializer, JsonDeserializer { + + @Override + public Location deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject obj = json.getAsJsonObject(); + + World world = FMCCore.getInstance().getServer().getWorld(UUID.fromString(obj.get("world").getAsString())); + Location location = new Location(world, obj.get("x").getAsDouble(), obj.get("y").getAsDouble(), obj.get("z").getAsDouble()); + location.setYaw(obj.get("yaw").getAsFloat()); + location.setPitch(obj.get("pitch").getAsFloat()); + return location; + } + + @Override + public JsonElement serialize(Location src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + obj.addProperty("world", Objects.requireNonNull(src.getWorld()).getUID().toString()); + obj.addProperty("x", src.getX()); + obj.addProperty("y", src.getY()); + obj.addProperty("z", src.getZ()); + obj.addProperty("yaw", src.getYaw()); + obj.addProperty("pitch", src.getPitch()); + return obj; + } +} diff --git a/src/main/java/cn/forevermc/spigot/core/serializable/RegionSerializer.java b/src/main/java/cn/forevermc/spigot/core/serializable/RegionSerializer.java new file mode 100644 index 0000000..4a95e6f --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/serializable/RegionSerializer.java @@ -0,0 +1,41 @@ +package cn.forevermc.spigot.core.serializable; + +import cn.forevermc.spigot.core.bean.Region; +import com.google.gson.Gson; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import org.bukkit.Location; + +import java.lang.reflect.Type; + +/** + * @author 夜雨 + * @since 2024-08-29 10:05 + */ +public class RegionSerializer implements JsonSerializer, JsonDeserializer { + + private static final Gson GSON = new Gson(); + + @Override + public Region deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + Region region = new Region(); + + JsonObject obj = json.getAsJsonObject(); + region.setLeft(GSON.fromJson(obj.get("p1"), Location.class)); + region.setRight(GSON.fromJson(obj.get("p2"), Location.class)); + return region; + } + + @Override + public JsonElement serialize(Region src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + obj.add("p1", GSON.toJsonTree(src.getP1())); + obj.add("p2", GSON.toJsonTree(src.getP2())); + return obj; + } +} diff --git a/src/main/java/cn/forevermc/spigot/core/util/Ref.java b/src/main/java/cn/forevermc/spigot/core/util/Ref.java new file mode 100644 index 0000000..d46b350 --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/util/Ref.java @@ -0,0 +1,255 @@ +package cn.forevermc.spigot.core.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 反射相关 + * + * @author 夜雨 + * @since 2023-05-04 15:05 + */ +public class Ref { + + /** + * 获取类字段列表 + * + * @param clazz 类 + * @return 字段列表 + */ + public static List listFields(Class clazz) { + return listFields(clazz, null); + } + + /** + * 获取类字段列表 + * + * @param clazz 类 + * @param fieldType 字段类过滤 + * @return 字段列表 + */ + public static List listFields(Class clazz, Class fieldType) { + Field[] fields = clazz.getDeclaredFields(); + if (fieldType == null) { + return Arrays.asList(fields); + } + return Arrays.stream(fields).filter(f -> fieldType.isAssignableFrom(f.getType())).collect(Collectors.toList()); + } + + /** + * + * @param keyName + * @return + */ + public static String getFieldName(String keyName) { + String[] splits = {"-", "_", " "}; + StringBuilder full = new StringBuilder(keyName.substring(0, 1).toUpperCase() + keyName.substring(1)); + for (int i = 0; i < splits.length; i++) { + if (keyName.contains(splits[i])) { + // 存在分隔符 + full.setLength(0); + String[] word = keyName.split(splits[i]); + for (int j = 0; j < word.length; j++) { + full.append(word[j].substring(0, 1).toUpperCase()).append(word[j].substring(1)); + } + break; + } + } + return String.valueOf(full.charAt(0)).toLowerCase() + full.substring(1); + } + + /** + * 反射获取对象字段,包括父级类,直至 {@link Object},如果都不存在则返回 null + * + * @param clazz 类 + * @param fieldName 字段名 + * @return 字段 + */ + public static Field getField(Class clazz, String fieldName) { + do { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field; + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } while (clazz != Object.class); + throw new NullPointerException(String.format("not found field: %s in %s", fieldName, clazz)); + } + + /** + * 反射获取对象字段值,包括父级类,直至 {@link Object} + * + * @param object 对象 + * @param fieldName 字段名 + * @param toClass 返回类 + * @param 返回类型 + * @return 字段值 + * @throws IllegalAccessException 反射访问失败 + * @throws NullPointerException 向上反射直至 {@link Object} 也找不到该字段 + */ + public static T getFieldValue(Object object, String fieldName, Class toClass) throws IllegalAccessException, NullPointerException { + return getFieldValue(object, getField(object.getClass(), fieldName), toClass); + } + + /** + * 反射获取对象字段值 + * + * @param object 对象 + * @param field 字段 + * @param toClass 返回类 + * @param 返回类型 + * @return 字段值 + * @throws IllegalAccessException 反射访问失败 + * @throws NullPointerException 向上反射直至 {@link Object} 也找不到该字段 + */ + public static T getFieldValue(Object object, Field field, Class toClass) throws IllegalAccessException, NullPointerException { + if (field == null) { + throw new NullPointerException("field can not be null"); + } + field.setAccessible(true); + return toClass.cast(field.get(object)); + } + + /** + * 反射获取类字段 + * + * @param objectClass 类 + * @param fieldName 字段名 + * @return 字段 + */ + public static Field getClassField(Class objectClass, String fieldName) { + try { + Field field = objectClass.getDeclaredField(fieldName); + field.setAccessible(true); + return field; + } catch (NoSuchFieldException e) { + return null; + } + } + + /** + * 反射获取指定类字段值 + * + * @param object 对象 + * @param objectClass 类 + * @param fieldName 字段名 + * @param toClass 值类型 + * @param 值类型 + * @return 字段值 + * @throws IllegalAccessException 反射访问失败 + * @throws NoSuchFieldException 字段不存在 + */ + public static T getClassFieldValue(Object object, Class objectClass, String fieldName, Class toClass) throws IllegalAccessException, NoSuchFieldException { + Field field = getClassField(objectClass, fieldName); + if (field == null) { + throw new NoSuchFieldException("not found " + fieldName + " field in " + objectClass.getSimpleName()); + } + field.setAccessible(true); + return toClass.cast(field.get(object)); + } + + /** + * 反射设置对象字段值,包括父级类,直至 {@link Object} + * + * @param object 对象 + * @param fieldName 字段名 + * @param value 字段值 + * @throws IllegalAccessException 反射访问失败 + * @throws NoSuchFieldException 向上反射直至 {@link Object} 也找不到该字段 + */ + public static void setFieldValue(Object object, String fieldName, Object value) throws IllegalAccessException, NoSuchFieldException { + Field field = getField(object.getClass(), fieldName); + if (field == null) { + throw new NoSuchFieldException("not found " + fieldName + " field in " + object.getClass().getSimpleName()); + } + setFieldValue(object, field, value); + } + + public static void setFieldValue(Object object, Field field, Object value) throws IllegalAccessException { + field.setAccessible(true); + field.set(object, value); + } + + /** + * 反射设置对象字段值 + * + * @param object 对象 + * @param objectClass 类 + * @param fieldName 字段名 + * @param value 字段值 + * @throws IllegalAccessException 反射访问失败 + * @throws NullPointerException 字段不存在 + */ + public static void setClassFieldValue(Object object, Class objectClass, String fieldName, Object value) throws IllegalAccessException, NullPointerException { + Field field = getClassField(objectClass, fieldName); + if (field == null) { + throw new NullPointerException("not found " + fieldName + " field in " + objectClass.getSimpleName() + " class"); + } + field.setAccessible(true); + field.set(object, value); + } + + /** + * 反射查找方法,包括父级类,直至 {@link Object},如果都不存在则返回 null + * + * @param clazz 类 + * @param methodName 方法名 + * @param parameterTypes 可选参 + * @return 方法对象 + */ + public static Method getMethod(Class clazz, String methodName, Class... parameterTypes) { + if (clazz == null) { + throw new NullPointerException("class can not be null"); + } + do { + try { + Method method = clazz.getDeclaredMethod(methodName, parameterTypes); + method.setAccessible(true); + return method; + } catch (NoSuchMethodException e) { + clazz = clazz.getSuperclass(); + } + } while (clazz != Object.class); + return null; + } + + /** + * 字符串转枚举 + * + * @param clazz 枚举类 + * @param string 字符串 + * @param 泛型 + * @return 泛型 + */ + public static > T toType(Class clazz, String string) { + if (string == null) { + return null; + } + T[] ts = clazz.getEnumConstants(); + if (ts == null) { + throw new IllegalArgumentException(clazz.getName() + " is not an enum type"); + } + for (int i = 0; i < ts.length; i++) { + if (ts[i].name().equalsIgnoreCase(string)) { + return ts[i]; + } + } + return null; + } + + /** + * + * @param type + * @return + * @param + * @throws Exception + */ + public static T newInstance(Class type) throws Exception { + return type.getDeclaredConstructor().newInstance(); + } +} diff --git a/src/main/java/cn/forevermc/spigot/core/util/Util.java b/src/main/java/cn/forevermc/spigot/core/util/Util.java new file mode 100644 index 0000000..1735d05 --- /dev/null +++ b/src/main/java/cn/forevermc/spigot/core/util/Util.java @@ -0,0 +1,163 @@ +package cn.forevermc.spigot.core.util; + +import cn.forevermc.spigot.core.exception.ArgsValueException; +import net.objecthunter.exp4j.ExpressionBuilder; +import net.objecthunter.exp4j.function.Function; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.UUID; + +/** + * 工具 + * + * @author 夜雨 + * @since 2024-08-19 15:05 + */ +public class Util { + + private static final Function EXPRESSION_ROUND = new Function("round") { + + @Override + public double apply(double... args) { + return BigDecimal.valueOf(args[0]).setScale(4, RoundingMode.HALF_UP).doubleValue(); + } + }; + + public static Entity getEntityByUID(UUID uuid) { + List worldList = Bukkit.getWorlds(); + for (int i = 0; i < worldList.size(); i++) { + List entityList = worldList.get(i).getEntities(); + for (int j = 0; j < entityList.size(); j++) { + if (entityList.get(j).getUniqueId().equals(uuid)) { + return entityList.get(j); + } + } + } + return null; + } + + /** + * 看向某个位置 + * + * @param entity + * @param lookAt + */ + public static void lookAt(Entity entity, Location lookAt) { + Location loc = entity.getLocation(); + loc.setDirection(lookAt.toVector().subtract(loc.toVector()).normalize()); + entity.teleport(loc); + } + + /** + * 合并参数 + * + * @param args + * @param from + * @param to + * @return + */ + public static String joinArgs(String[] args, int from, int to) { + StringBuilder sb = new StringBuilder(); + for (int i = from; i < to; i++) { + sb.append(args[i]).append(' '); + } + sb.deleteCharAt(sb.length() - 1); + if (sb.charAt(0) == '"') { + sb.deleteCharAt(0); + } + if (sb.charAt(sb.length() - 1) == '"') { + sb.deleteCharAt(sb.length() - 1); + } + if (sb.charAt(0) == '/') { + sb.deleteCharAt(0); + } + return sb.toString(); + } + + /** + * 计算表达式 + * + * @param expression + * @return + */ + public static double argExpression(String expression) { + return new ExpressionBuilder(String.format("round(%s)", expression)).function(EXPRESSION_ROUND).build().evaluate(); + } + + /** + * 颜色消息 + * + * @param msg + * @return + */ + public static String colorMsg(String msg) { + return ChatColor.translateAlternateColorCodes('&', msg); + } + + /** + * 获取必要参数值 + * + * @param args + * @param index + * @param name + * @return + * @throws ArgsValueException + */ + public static String argsValueRequired(String[] args, int index, String name) throws ArgsValueException { + if (args.length - 1 < index) { + throw new ArgsValueException(String.format("缺少 %s", name)); + } + String value = args[index]; + if (value.isEmpty()) { + throw new ArgsValueException(String.format("缺少 %s", name)); + } + return value; + } + + /** + * 获取参数值 + * + * @param args + * @param index + * @return + */ + public static String argsValue(String[] args, int index) { + if (args.length - 1 < index) { + return null; + } + String value = args[index]; + if (value.isEmpty()) { + return null; + } + return value; + } + + /** + * 获取玩家看向的实体 + * + * @param player 玩家 + * @param range 直线范围 + * @return 实体 + */ + public static Entity getTargetEntity(Player player, double range) { + Vector direction = player.getEyeLocation().getDirection(); + Vector start = player.getEyeLocation().toVector(); + Vector end = start.add(direction.multiply(range)); + + for (Entity entity : player.getWorld().getNearbyEntities(player.getLocation(), range, range, range)) { + if (entity.getLocation().toVector().isInAABB(start, end)) { + return entity; + } + } + return null; + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..ca6ce21 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,9 @@ +name: FMCCore +version: '0.0.1' +main: cn.forevermc.spigot.core.FMCCore +api-version: '1.9' + +commands: + fmc: + description: FMC Command + usage: "/"