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..7d3b0cf
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,4 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+/CopilotChatHistory.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..dcfa91d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..3b3bdad
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,86 @@
+
+
+ 4.0.0
+
+ cn.forevermc.spigot
+ fmc-core
+ 0.0.1+${mc.version}
+ jar
+
+
+ 1.20.6+
+ 21
+ 21
+ 21
+ UTF-8
+
+
+
+
+ spigot-repo
+ https://hub.spigotmc.org/nexus/content/repositories/snapshots/
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.7.1
+
+ E:\SpigotMC\test1206\plugins
+ FMCCore-${version}+${mc.version}.jar
+
+ jar-with-dependencies
+
+
+
+
+ package
+
+ single
+
+
+
+
+
+
+
+
+
+ org.spigotmc
+ spigot-api
+ 1.20.6-R0.1-20240613.150924-57
+ provided
+
+
+ com.imyeyu.inject
+ timi-inject
+ 0.0.1
+
+
+ net.objecthunter
+ exp4j
+ 0.4.8
+
+
+ org.projectlombok
+ lombok
+ 1.18.36
+
+
+ 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..295c0bb
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/FMCCore.java
@@ -0,0 +1,51 @@
+package cn.forevermc.spigot.core;
+
+import cn.forevermc.spigot.core.command.FMCCommand;
+import cn.forevermc.spigot.core.command.FMCCommandExecutor;
+import com.imyeyu.inject.TimiInject;
+import com.imyeyu.inject.annotation.IOCReturn;
+import com.imyeyu.inject.annotation.Inject;
+import com.imyeyu.inject.annotation.TimiInjectApplication;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.logging.Logger;
+
+/**
+ *
+ *
+ * @author 夜雨
+ * @since 2024-10-10 15:05
+ */
+@TimiInjectApplication
+public final class FMCCore extends JavaPlugin {
+
+ @Inject
+ private FMCCommand fmcCommand;
+
+ @Inject
+ private FMCCommandExecutor commandExecutor;
+
+ private static final List FMC_PLUGIN_LIST = new ArrayList<>();
+
+ public static void register(T fmcPlugin) {
+ FMC_PLUGIN_LIST.add(fmcPlugin);
+ }
+
+ @Override
+ public void onEnable() {
+ // 扫描插件
+ TimiInject.run(this);
+ for (int i = 0; i < FMC_PLUGIN_LIST.size(); i++) {
+ FMC_PLUGIN_LIST.get(i).fmcCommand = fmcCommand;
+ }
+ Objects.requireNonNull(getCommand("fmc")).setExecutor(commandExecutor);
+ }
+
+ @IOCReturn
+ public Logger log() {
+ return getLogger();
+ }
+}
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..a0f64bc
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/FMCPlugin.java
@@ -0,0 +1,90 @@
+package cn.forevermc.spigot.core;
+
+import cn.forevermc.spigot.core.bean.ConfigPath;
+import cn.forevermc.spigot.core.command.FMCCommand;
+import com.imyeyu.java.bean.timi.TimiCode;
+import com.imyeyu.java.bean.timi.TimiException;
+import com.imyeyu.java.ref.Ref;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.lang.reflect.Field;
+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 saveConfigAs(Object obj, FileConfiguration config) {
+ try {
+ serializeConfig(obj, config);
+ super.saveConfig();
+ } catch (Exception e) {
+ throw new TimiException(TimiCode.ERROR, "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 TimiException(TimiCode.ERROR, "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/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/InteractiveMsg.java b/src/main/java/cn/forevermc/spigot/core/bean/InteractiveMsg.java
new file mode 100644
index 0000000..54f90c7
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/bean/InteractiveMsg.java
@@ -0,0 +1,170 @@
+package cn.forevermc.spigot.core.bean;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.chat.HoverEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 可交互消息,发送给玩家可操作的消息
+ *
+ * @author 夜雨
+ * @since 2024-09-04 14:46
+ */
+@Data
+public class InteractiveMsg {
+
+ /** 模板 */
+ private String path;
+
+ /** 模板内容 */
+ private String data;
+
+ /** 行列表 */
+ private List lineList;
+
+ /**
+ * 换行
+ *
+ * @return 本对象
+ */
+ public InteractiveMsg breakLine() {
+ return appendLine("");
+ }
+
+ /**
+ * 追加行
+ *
+ * @param line 行内容
+ * @return 本对象
+ */
+ public InteractiveMsg appendLine(String line) {
+ return appendLine(new Line(line));
+ }
+
+ /**
+ * 追加行
+ *
+ * @param line 行对象
+ * @return 本对象
+ */
+ public InteractiveMsg appendLine(Line line) {
+ if (lineList == null) {
+ lineList = new ArrayList<>();
+ }
+ lineList.add(line);
+ return this;
+ }
+
+ /**
+ * 可交互消息行
+ *
+ * @author 夜雨
+ * @since 2024-09-04 14:46
+ */
+ @Data
+ @NoArgsConstructor
+ public static class Line {
+
+ /** 行内容列表 */
+ private List textList;
+
+ /** @param text 行内容 */
+ public Line(String text) {
+ appendText(text);
+ }
+
+ /** @param text 行内容对象 */
+ public Line(Text text) {
+ appendText(text);
+ }
+
+ /**
+ * 追加行内文本
+ *
+ * @param text 行内文本
+ * @return 行对象
+ */
+ public Line appendText(String text) {
+ return appendText(new Text(text));
+ }
+
+ /**
+ * 追加行内文本
+ *
+ * @param text 行内文本对象
+ * @return 行对象
+ */
+ public Line appendText(Text text) {
+ if (textList == null) {
+ textList = new ArrayList<>();
+ }
+ textList.add(text);
+ return this;
+ }
+
+ /**
+ * 行内文本
+ *
+ * @author 夜雨
+ * @since 2024-09-04 14:46
+ */
+ @Data
+ @NoArgsConstructor
+ public static class Text {
+
+ /**
+ * 对齐方式
+ *
+ * @author 夜雨
+ * @since 2024-09-05 00:34
+ */
+ public enum Align {
+
+ /** 左对齐 */
+ LEFT,
+
+ /** 居中 */
+ CENTER,
+
+ /** 右对齐 */
+ RIGHT
+ }
+
+ /** 条件 */
+ private String condition;
+
+ /** 字符宽度 */
+ private int width;
+
+ /** 对齐方式 */
+ private Align align = Align.LEFT;
+
+ /** 点击事件 */
+ private ClickEvent.Action action;
+
+ /** 点击事件值 */
+ private String actionValue;
+
+ /** true 为加粗 */
+ private boolean bold;
+
+ /** 指向事件 */
+ private HoverEvent.Action hoverType;
+
+ /** 指向事件值 */
+ private String hoverValue;
+
+ /** 内容 */
+ private String content;
+
+ /** @param content 行内文本 */
+ public Text(String content) {
+ this.content = content;
+ }
+ }
+ }
+}
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..9b58bfc
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/bean/NameEntity.java
@@ -0,0 +1,139 @@
+package cn.forevermc.spigot.core.bean;
+
+import cn.forevermc.spigot.core.FMCCore;
+import lombok.Getter;
+import lombok.Setter;
+import com.imyeyu.inject.annotation.Inject;
+import com.imyeyu.inject.annotation.StaticInject;
+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
+ */
+@StaticInject
+public class NameEntity {
+
+ @Inject
+ private static FMCCore fmcCore;
+
+ /** 实体 UUID */
+ 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, () -> 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 Bukkit.getEntity(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();
+ }
+
+ public String getUUID() {
+ return uuid;
+ }
+
+}
diff --git a/src/main/java/cn/forevermc/spigot/core/bean/PageList.java b/src/main/java/cn/forevermc/spigot/core/bean/PageList.java
new file mode 100644
index 0000000..19ba1e1
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/bean/PageList.java
@@ -0,0 +1,87 @@
+package cn.forevermc.spigot.core.bean;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import com.imyeyu.utils.Calc;
+
+import java.util.List;
+
+/**
+ * 分页列表
+ *
+ * @author 夜雨
+ * @since 2024-09-16 22:21
+ */
+@Data
+public class PageList {
+
+ /** 标题 */
+ private String title;
+
+ /** 当前索引 */
+ private int index = 0;
+
+ /** 单页数量 */
+ private int size = 5;
+
+ /** 总数据列表 */
+ private List items;
+
+ /** 上一页 */
+ public void previous() {
+ index = Math.max(index - 1, 0);
+ }
+
+ /** 下一页 */
+ public void next() {
+ index = Math.min(index + 1, getPages() - 1);
+ }
+
+ /** @return 获取当前分页数据 */
+ public List getIndexItems() {
+ if (items == null) {
+ throw new NullPointerException("not found items");
+ }
+ int fromIndex = index * size;
+ return items.subList(fromIndex, Math.min(fromIndex + size, items.size()));
+ }
+
+ /** @return 页面数量 */
+ public int getPages() {
+ return Calc.ceil(1D * items.size() / size);
+ }
+
+ /**
+ * 实现分页的数据对象
+ *
+ * @author 夜雨
+ * @since 2024-09-16 23:03
+ */
+ public interface Item {
+
+ /** @return 数据对象可交互操作列表 */
+ List pageItemActionList();
+
+ /** @return 数据对象内容 */
+ String pageItemContent();
+ }
+
+ /**
+ * 数据对象可交互操作
+ *
+ * @author 夜雨
+ * @since 2024-09-16 23:03
+ */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class ItemAction {
+
+ /** 显示名 */
+ private String name;
+
+ /** 点击触发指令 */
+ private String command;
+ }
+}
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..85b73b8
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/bean/Region.java
@@ -0,0 +1,192 @@
+package cn.forevermc.spigot.core.bean;
+
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.util.BoundingBox;
+
+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);
+ }
+
+ /** 区域顶点 */
+ 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);
+ }
+ }
+
+ public Location getLeft() {
+ return left;
+ }
+
+ public void setLeft(Location left) {
+ this.left = left;
+ }
+
+ public Location getRight() {
+ return right;
+ }
+
+ public void setRight(Location right) {
+ this.right = right;
+ }
+}
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..100e87d
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/command/AbstractCommand.java
@@ -0,0 +1,147 @@
+package cn.forevermc.spigot.core.command;
+
+import cn.forevermc.spigot.core.exception.ArgsValueException;
+import com.imyeyu.inject.annotation.Inject;
+import com.imyeyu.inject.annotation.StaticInject;
+import com.imyeyu.java.TimiJava;
+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.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * 抽象指令
+ *
+ * @author 夜雨
+ * @since 2024-08-15 09:59
+ */
+@StaticInject
+public abstract class AbstractCommand {
+
+ @Inject
+ private static Logger log;
+
+ /** 指令参数提示(子级) */
+ protected final Map> argsTabCompleterMap = new HashMap<>();
+
+ /** 子级 */ // TODO 不可直接操作,需要使用 register 防止覆盖注册
+ 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 (sender instanceof Player player && !player.isOp()) {
+ error("没有权限操作");
+ }
+ } else {
+ 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(List.of(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) {
+ ChatColor color = level == Level.INFO ? ChatColor.GREEN : ChatColor.RED;
+ player.spawnParticle(Particle.SMOKE, new Location(player.getWorld(), 0, 0, 0), 10);
+ player.sendMessage(color + msg);
+ } else {
+ log.log(level, msg);
+ }
+ }
+
+ AbstractCommand getCommand(String command) {
+ return this.childrenCommandMap.get(command);
+ }
+
+ public final boolean hasChildren() {
+ return TimiJava.isNotEmpty(childrenCommandMap);
+ }
+
+ public List getTabCompleterList(CommandSender sender) {
+ List result = new ArrayList<>();
+ if (sender instanceof Player player && !player.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..a85df9b
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/command/FMCCommand.java
@@ -0,0 +1,20 @@
+package cn.forevermc.spigot.core.command;
+
+import com.imyeyu.inject.annotation.Component;
+
+/**
+ * ForeverMC 指令
+ *
+ * @author 夜雨
+ * @since 2024-08-15 10:21
+ */
+@Component
+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..3d62174
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/command/FMCCommandExecutor.java
@@ -0,0 +1,97 @@
+package cn.forevermc.spigot.core.command;
+
+import cn.forevermc.spigot.core.FMCCore;
+import com.imyeyu.inject.annotation.Component;
+import com.imyeyu.inject.annotation.Inject;
+import com.imyeyu.inject.annotation.InvokeForInjected;
+import com.imyeyu.java.TimiJava;
+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.logging.Logger;
+
+/**
+ * 指令执行器
+ *
+ * @author 夜雨
+ * @since 2024-08-15 19:39
+ */
+@Component
+public class FMCCommandExecutor implements CommandExecutor {
+
+ @Inject
+ private Logger log;
+
+ @Inject
+ private FMCCore fmcCore;
+
+ @Inject
+ private FMCCommand fmcCommand;
+
+ @InvokeForInjected
+ private void injected() {
+ // 指令提示
+ Objects.requireNonNull(fmcCore.getCommand("fmc")).setTabCompleter((sender, command, label, args) -> {
+ AbstractCommand cmd = fmcCommand;
+ int i = 0;
+
+ List result = new ArrayList<>();
+ while (TimiJava.isNotEmpty(cmd.childrenCommandMap)) {
+ final int j = i;
+ if (i == args.length - 1) {
+ result.addAll(cmd.getTabCompleterList(sender).stream().filter(item -> item.contains(args[j])).toList());
+ break;
+ }
+ if (cmd.getCommand(args[i]) == null) {
+ break;
+ }
+ cmd = cmd.getCommand(args[i]);
+ i++;
+ }
+ // 无子级指令
+ if (TimiJava.isNotEmpty(cmd.argsTabCompleterMap)) {
+ 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)).toList());
+ }
+ }
+ return result;
+ });
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ List action = new ArrayList<>();
+ AbstractCommand cmd = fmcCommand;
+ if (TimiJava.isEmpty(args)) {
+ 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..671419a
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/exception/ArgsValueException.java
@@ -0,0 +1,15 @@
+package cn.forevermc.spigot.core.exception;
+
+/**
+ * @author 夜雨
+ * @since 2024-08-31 00:20
+ */
+public class ArgsValueException extends Exception {
+
+ public ArgsValueException() {
+ }
+
+ 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..74c7c5f
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/serializable/LocationSerializer.java
@@ -0,0 +1,54 @@
+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 com.imyeyu.inject.annotation.Inject;
+import com.imyeyu.inject.annotation.StaticInject;
+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
+ */
+@StaticInject
+public class LocationSerializer implements JsonSerializer, JsonDeserializer {
+
+ @Inject
+ private static FMCCore fmcCore;
+
+ @Override
+ public Location deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+ JsonObject obj = json.getAsJsonObject();
+
+ World world = fmcCore.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..b111fd8
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/serializable/RegionSerializer.java
@@ -0,0 +1,45 @@
+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 com.imyeyu.inject.annotation.Inject;
+import com.imyeyu.inject.annotation.StaticInject;
+import org.bukkit.Location;
+
+import java.lang.reflect.Type;
+
+/**
+ * @author 夜雨
+ * @since 2024-08-29 10:05
+ */
+@StaticInject
+public class RegionSerializer implements JsonSerializer, JsonDeserializer {
+
+ @Inject
+ private static Gson 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/service/InteractiveMsgService.java b/src/main/java/cn/forevermc/spigot/core/service/InteractiveMsgService.java
new file mode 100644
index 0000000..666eea9
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/service/InteractiveMsgService.java
@@ -0,0 +1,253 @@
+package cn.forevermc.spigot.core.service;
+
+import cn.forevermc.spigot.core.FMCCore;
+import cn.forevermc.spigot.core.bean.InteractiveMsg;
+import cn.forevermc.spigot.core.bean.PageList;
+import cn.forevermc.spigot.core.util.FMCUtil;
+import com.imyeyu.inject.annotation.Inject;
+import com.imyeyu.inject.annotation.Service;
+import com.imyeyu.io.IO;
+import com.imyeyu.java.TimiJava;
+import com.imyeyu.java.ref.Ref;
+import com.imyeyu.utils.StringInterpolator;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.chat.ComponentBuilder;
+import net.md_5.bungee.api.chat.HoverEvent;
+import net.md_5.bungee.api.chat.TextComponent;
+import net.md_5.bungee.api.chat.hover.content.Text;
+import org.dom4j.DocumentException;
+import org.dom4j.Element;
+import org.dom4j.io.SAXReader;
+import org.xml.sax.InputSource;
+
+import java.io.ByteArrayInputStream;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-04 14:44
+ */
+@Service
+public class InteractiveMsgService {
+
+ public static final String ACTION_CMD_FLAG = "#INTERACTIVE_MSG";
+
+ private static final String TEMPLATE_PATH = "msg_template/";
+ private static final TextComponent BREAK = new TextComponent("\n");
+ private static final StringInterpolator ARG_INTERPOLATOR = new StringInterpolator(StringInterpolator.DOLLAR_OBJ);
+
+ @Inject
+ private FMCCore fmcCore;
+
+ static {
+ ARG_INTERPOLATOR.putFilter("nullable", arg -> {
+ if (TimiJava.isEmpty(arg)) {
+ return "N/A";
+ }
+ return arg;
+ });
+ ARG_INTERPOLATOR.putFilter("command", arg -> {
+ if (TimiJava.isEmpty(arg)) {
+ return "N/A";
+ }
+ return "/" + arg;
+ });
+ }
+
+ private final Map cache;
+
+ public InteractiveMsgService() {
+ cache = new HashMap<>();
+ }
+
+ public InteractiveMsg get(String path) {
+ try {
+ InteractiveMsg interactiveMsg = cache.get(path);
+ if (interactiveMsg == null) {
+ interactiveMsg = build(path);
+ cache.put(path, interactiveMsg);
+ }
+ return interactiveMsg;
+ } catch (Exception e) {
+ throw new RuntimeException("构建模板消息失败", e);
+ }
+ }
+
+ public TextComponent build(PageList pageList) {
+ InteractiveMsg msg = new InteractiveMsg();
+ msg.breakLine().breakLine().breakLine();
+ // 标题
+ msg.appendLine(pageList.getTitle());
+ // 列表
+ {
+ List indexItems = pageList.getIndexItems();
+ for (int i = 0; i < indexItems.size(); i++) {
+ T item = indexItems.get(i);
+
+ InteractiveMsg.Line line = new InteractiveMsg.Line();
+ List actionList = item.pageItemActionList();
+ if (TimiJava.isNotEmpty(actionList)) {
+ for (int j = 0; j < actionList.size(); j++) {
+ PageList.ItemAction itemAction = actionList.get(j);
+
+ InteractiveMsg.Line.Text text = new InteractiveMsg.Line.Text();
+ text.setContent(FMCUtil.colorMsg(itemAction.getName()));
+ text.setAction(ClickEvent.Action.RUN_COMMAND);
+ text.setActionValue(itemAction.getCommand());
+ line.appendText(text);
+ }
+ line.appendText(" ");
+ }
+ line.appendText(FMCUtil.colorMsg(item.pageItemContent()));
+ msg.appendLine(line);
+ }
+ // 填充空项
+ for (int i = 0, l = pageList.getSize() - indexItems.size(); i < l; i++) {
+ msg.breakLine();
+ }
+ }
+ // 分页
+ {
+ InteractiveMsg.Line line = new InteractiveMsg.Line();
+ {
+ InteractiveMsg.Line.Text previous = new InteractiveMsg.Line.Text();
+ previous.setContent("&a[上一页]");
+ previous.setAction(ClickEvent.Action.RUN_COMMAND);
+ previous.setActionValue("/fmc page previous");
+ line.appendText(previous);
+ }
+ {
+ InteractiveMsg.Line.Text previous = new InteractiveMsg.Line.Text();
+ previous.setContent("&a[下一页]");
+ previous.setAction(ClickEvent.Action.RUN_COMMAND);
+ previous.setActionValue("/fmc page next");
+ line.appendText(previous);
+ }
+ line.appendText(" [&e%s/%s&f]".formatted(pageList.getIndex() + 1, pageList.getPages()));
+ msg.appendLine(line);
+ }
+ return build(msg, new HashMap<>());
+ }
+
+ public TextComponent build(String path, Map argsMap) {
+ return build(get(path), argsMap);
+ }
+
+ public TextComponent build(InteractiveMsg msg, Map argsMap) {
+ TextComponent textComponent = new TextComponent();
+ List lineList = msg.getLineList();
+ for (int i = 0; i < lineList.size(); i++) {
+ // 行
+ InteractiveMsg.Line line = lineList.get(i);
+ ComponentBuilder lineBuilder = new ComponentBuilder();
+
+ List textList = line.getTextList();
+ if (TimiJava.isNotEmpty(textList)) {
+ for (int j = 0; j < textList.size(); j++) {
+ // 行内文本
+ ComponentBuilder textBuilder = new ComponentBuilder();
+ InteractiveMsg.Line.Text text = textList.get(j);
+ if (text.getCondition() != null && !Boolean.parseBoolean(argsMap.get(text.getCondition()).toString())) {
+ // 不符合条件
+ continue;
+ }
+ {
+ // 内容
+ String content = ARG_INTERPOLATOR.inject(text.getContent(), argsMap);
+ if (text.getAlign() != null && content.length() < text.getWidth()) {
+ int totalSpace = text.getWidth() - content.length();
+ content = switch (text.getAlign()) {
+ case LEFT -> com.imyeyu.utils.Text.paddedSpaceEnd(content, text.getWidth());
+ case RIGHT -> com.imyeyu.utils.Text.paddedSpaceStart(content, text.getWidth());
+ case CENTER -> {
+ StringBuilder sb = new StringBuilder();
+ for (int k = 0; k < totalSpace; k++) {
+ sb.append(' ');
+ }
+ sb.insert(totalSpace / 2, content);
+ yield sb.toString();
+ }
+ };
+ }
+ textBuilder.append(FMCUtil.colorMsg(content));
+ }
+ // 事件
+ if (text.getAction() != null) {
+ String actionValue = ARG_INTERPOLATOR.inject(text.getActionValue(), argsMap);
+ if (text.getAction() == ClickEvent.Action.RUN_COMMAND) {
+ // 追加触发来源标记
+ actionValue += " " + ACTION_CMD_FLAG;
+ }
+ textBuilder.event(new ClickEvent(text.getAction(), actionValue));
+ }
+ // 样式
+ if (text.getHoverType() != null) {
+ HoverEvent event = new HoverEvent(text.getHoverType(), new Text(""));
+ if (Objects.requireNonNull(text.getHoverType()) == HoverEvent.Action.SHOW_TEXT) {
+ event.addContent(new Text(text.getHoverValue()));
+ }
+ textBuilder.event(event);
+ }
+ textBuilder.bold(text.isBold());
+
+ lineBuilder.append(textBuilder.build());
+ }
+ }
+ textComponent.addExtra(lineBuilder.build());
+ if (i != lineList.size() - 1) {
+ textComponent.addExtra(BREAK);
+ }
+ }
+ return textComponent;
+ }
+
+ private InteractiveMsg build(String path) throws DocumentException {
+ InteractiveMsg msg = new InteractiveMsg();
+ msg.breakLine();
+ msg.breakLine();
+ msg.breakLine();
+
+ String data = IO.resourceToString(getClass(), path);
+ msg.setData(data);
+
+ SAXReader reader = new SAXReader();
+ reader.setEntityResolver((publicId, systemId) -> {
+ // 忽略 dtd
+ if (systemId != null && systemId.contains(".dtd")) {
+ return new InputSource(new StringReader(""));
+ } else {
+ return null;
+ }
+ });
+ Element root = reader.read(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8))).getRootElement();
+ List linesEl = root.elements("line");
+ for (int i = 0; i < linesEl.size(); i++) {
+ Element lineEl = linesEl.get(i);
+
+ InteractiveMsg.Line line = new InteractiveMsg.Line();
+ List textsEl = lineEl.elements("text");
+ for (int j = 0; j < textsEl.size(); j++) {
+ Element textEl = textsEl.get(j);
+ InteractiveMsg.Line.Text text = new InteractiveMsg.Line.Text();
+ text.setCondition(textEl.attributeValue("if"));
+ text.setWidth(Integer.parseInt(TimiJava.firstNotNull(textEl.attributeValue("width"), "-1")));
+ text.setAlign(Ref.toType(InteractiveMsg.Line.Text.Align.class, textEl.attributeValue("align")));
+ text.setAction(Ref.toType(ClickEvent.Action.class, textEl.attributeValue("action")));
+ text.setActionValue(textEl.attributeValue("actionValue"));
+ text.setHoverType(Ref.toType(HoverEvent.Action.class, textEl.attributeValue("hoverType")));
+ text.setHoverValue(textEl.attributeValue("hoverValue"));
+ text.setBold(Boolean.parseBoolean(textEl.attributeValue("bold")));
+ text.setContent(textEl.getText());
+
+ line.appendText(text);
+ }
+ msg.appendLine(line);
+ }
+ return msg;
+ }
+}
diff --git a/src/main/java/cn/forevermc/spigot/core/util/FMCUtil.java b/src/main/java/cn/forevermc/spigot/core/util/FMCUtil.java
new file mode 100644
index 0000000..9fc3896
--- /dev/null
+++ b/src/main/java/cn/forevermc/spigot/core/util/FMCUtil.java
@@ -0,0 +1,151 @@
+package cn.forevermc.spigot.core.util;
+
+import cn.forevermc.spigot.core.exception.ArgsValueException;
+import com.imyeyu.java.TimiJava;
+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 FMCUtil {
+
+ 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();
+ }
+ };
+
+ /**
+ * 看向某个位置
+ *
+ * @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("round(%s)".formatted(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("缺少 %s".formatted(name));
+ }
+ String value = args[index];
+ if (TimiJava.isEmpty(value)) {
+ throw new ArgsValueException("缺少 %s".formatted(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 (TimiJava.isEmpty(value)) {
+ 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..38d72af
--- /dev/null
+++ b/src/main/resources/plugin.yml
@@ -0,0 +1,9 @@
+name: fmc-core
+version: '0.0.1'
+main: cn.forevermc.spigot.core.FMCCore
+api-version: '1.20'
+
+commands:
+ fmc:
+ description: FMC Command
+ usage: "/"