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