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 extends T> 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 extends T> 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: "/"