feat: adding command system

This commit is contained in:
wzp 2024-08-01 17:45:28 +08:00
parent fa81e235bb
commit 932f903455
19 changed files with 436 additions and 29 deletions

2
.idea/compiler.xml generated
View File

@ -10,6 +10,6 @@
<module name="MyBot.main" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel target="11" />
<bytecodeTargetLevel target="17" />
</component>
</project>

View File

@ -1 +1 @@
0.0.1-dev
0.0.2-dev

2
.idea/misc.xml generated
View File

@ -4,7 +4,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="temurin-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -1,39 +1,62 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
val projectName = rootProject.name
val groupName by extra("cn.wzpmc")
val projectArtifactId by extra("my-bot")
val projectVersion by extra("0.0.1-dev")
val projectVersion by extra("0.0.2-dev")
plugins {
id("java")
id("maven-publish")
id("com.github.johnrengelman.shadow") version "8.1.1"
}
group = groupName
version = projectVersion
repositories {
mavenCentral()
maven("https://libraries.minecraft.net")
}
dependencies {
implementation("net.minecrell:terminalconsoleappender:1.3.0") {
exclude(group = "org.apache.logging.log4j", module = "log4j-core")
exclude(group = "org.apache.logging.log4j", module = "log4j-api")
}
implementation("com.mojang:brigadier:1.0.18")
// https://mvnrepository.com/artifact/io.netty/netty-all
implementation("io.netty:netty-all:4.1.112.Final")
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
implementation("org.apache.logging.log4j:log4j-core:2.23.1")
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api
implementation("org.apache.logging.log4j:log4j-api:2.23.1")
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-jul
implementation("org.apache.logging.log4j:log4j-jul:2.23.1")
// https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2
implementation("com.alibaba.fastjson2:fastjson2:2.0.52")
// https://mvnrepository.com/artifact/org.yaml/snakeyaml
implementation("org.yaml:snakeyaml:2.2")
// https://mvnrepository.com/artifact/org.jline/jline
implementation("org.jline:jline:3.26.3")
implementation("org.jline:jline-terminal:3.26.3")
implementation("org.jline:jline-reader:3.26.3")
/*implementation("org.jline:jline-terminal-jni:3.26.3")
implementation("org.jline:jline-terminal-ffm:3.26.3")*/
// https://mvnrepository.com/artifact/org.jline/jline-terminal-jansi
implementation("org.jline:jline-terminal-jansi:3.26.3")
// https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
implementation("org.fusesource.jansi:jansi:2.4.1")
/*// https://mvnrepository.com/artifact/org.jline/jline-terminal-jna
implementation("org.jline:jline-terminal-jna:3.26.3")
// https://mvnrepository.com/artifact/net.java.dev.jna/jna
implementation("net.java.dev.jna:jna:5.14.0")*/
// https://mvnrepository.com/artifact/org.projectlombok/lombok
compileOnly("org.projectlombok:lombok:1.18.34")
annotationProcessor("org.projectlombok:lombok:1.18.34")
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
tasks.compileJava {
options.encoding = "UTF-8"
}
@ -48,6 +71,21 @@ tasks.register<Jar>("sourcesJar") {
archiveClassifier.set("sources")
from(sourceSets.main.get().allSource)
}
tasks.withType<ShadowJar> {
manifest {
attributes(
"Main-Class" to "cn.wzpmc.Main"
)
}
archiveBaseName.set("MyBot")
archiveVersion.set(projectVersion)
archiveClassifier.set("")
transform(Log4j2PluginsCacheFileTransformer::class.java)
}
tasks.named("build") {
dependsOn(tasks.named("shadowJar"))
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
@ -88,15 +126,14 @@ publishing {
repositories {
maven {
val releasesRepoUrl = uri("http://server.wzpmc.cn:8081/repository/maven-releases")
val snapshotsRepoUrl = uri("http://server.wzpmc.cn:8081/repository/maven-snapshots")
val releasesRepoUrl = uri("https://wzpmc.cn:90/repository/maven-releases")
val snapshotsRepoUrl = uri("https://wzpmc.cn:90/repository/maven-snapshots")
url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl
credentials {
username = project.findProperty("repo.user") as String? ?: ""
password = project.findProperty("repo.password") as String? ?: ""
}
isAllowInsecureProtocol = true
}
}
}

View File

@ -1,10 +1,13 @@
package cn.wzpmc;
import cn.wzpmc.commands.StopCommand;
import cn.wzpmc.configuration.Configuration;
import cn.wzpmc.console.MyBotConsole;
import cn.wzpmc.entities.user.bot.MyBot;
import cn.wzpmc.network.WebSocketConnectionHandler;
import cn.wzpmc.plugins.CommandManager;
import cn.wzpmc.utils.TemplateFileUtils;
import cn.wzpmc.utils.YamlUtils;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
@ -18,6 +21,8 @@ public class Main {
private static final String DEFAULT_CONFIGURATION_FILE_PATH = "templates/config.yaml";
@SneakyThrows
public static void main(String[] args) {
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
System.setProperty("terminal.jline", "true");
log.info("启动MyBot...");
File configurationFile = new File("config.yaml");
if (TemplateFileUtils.saveDefaultConfig(Main.class.getClassLoader(), DEFAULT_CONFIGURATION_FILE_PATH, configurationFile)) {
@ -36,7 +41,10 @@ public class Main {
}
WebSocketConnectionHandler webSocketConnectionHandler = new WebSocketConnectionHandler();
ChannelFuture future = webSocketConnectionHandler.connect(uri);
Channel channel = future.sync().channel();
MyBot myBot = new MyBot(configuration);
CommandManager commandManager = myBot.getCommandManager();
commandManager.registerCommand(new StopCommand(myBot));
MyBotConsole myBotConsole = new MyBotConsole(myBot, webSocketConnectionHandler);
myBotConsole.start();
}
}

View File

@ -37,7 +37,6 @@ public interface JsonMessagePart {
.append('(')
.append('\n');
Map<String, String> data = this.getData();
int i = 0;
data.forEach((key, value) -> stringBuilder.append('\t').append(key).append('=').append(value).append(',').append('\n'));
int length = stringBuilder.length();
stringBuilder.delete(length - 2, length - 1);

View File

@ -0,0 +1,48 @@
package cn.wzpmc.api.plugins;
import cn.wzpmc.api.user.IBot;
/**
* 插件基类
* @author wzp
* @version 0.0.2-dev
* @since 2024/7/31 下午6:02
*/
public interface BasePlugin {
/**
* 获取插件主类
* @author wzp
* @since 2024/7/31 下午7:07 v0.0.2-dev
* @param pluginClass 插件主类类名
* @return 插件主类
* @param <T> 插件主类类型
*/
static <T extends BasePlugin> T getPlugin(Class<T> pluginClass){
ClassLoader loader = pluginClass.getClassLoader();
if (loader instanceof IPluginClassLoader){
BasePlugin plugin = ((IPluginClassLoader) loader).getPlugin();
if (pluginClass.isInstance(plugin)) {
return pluginClass.cast(plugin);
}
}
throw new RuntimeException(new IllegalAccessException("You shouldn't load plugin class without PluginClassLoader!!!"));
}
/**
* 获取Bot
* @author wzp
* @since 2024/7/31 下午7:06 v0.0.2-dev
* @return Bot对象
*/
default IBot getBot() {
IPluginClassLoader classLoader = this.getClassLoader();
return classLoader.getBot();
}
/**
* 获取插件所使用的类加载器
* @author wzp
* @since 2024/7/31 下午7:11 v0.0.2-dev
* @return 类加载器
*/
IPluginClassLoader getClassLoader();
}

View File

@ -0,0 +1,35 @@
package cn.wzpmc.api.plugins;
import cn.wzpmc.api.user.IBot;
import java.net.URL;
import java.net.URLClassLoader;
/**
* 插件类加载器
* @author wzp
* @version 0.0.2-dev
* @since 2024/7/31 下午6:59
*/
public abstract class IPluginClassLoader extends URLClassLoader {
public IPluginClassLoader(URL[] urls) {
super(urls);
}
/**
* 获取当前插件
* @author wzp
* @since 2024/7/31 下午7:15 v0.0.2-dev
* @return 插件
*/
abstract public BasePlugin getPlugin();
/**
* 获取Bot
* @author wzp
* @since 2024/7/31 下午7:15 v0.0.2-dev
* @return Bot对象
*/
abstract public IBot getBot();
}

View File

@ -1,6 +1,7 @@
package cn.wzpmc.api.user;
import cn.wzpmc.api.message.MessageComponent;
import cn.wzpmc.api.user.permission.Permissions;
/**
* 消息发送者
@ -31,4 +32,22 @@ public interface CommandSender {
* @param messageComponent 消息组件
*/
void sendMessage(MessageComponent messageComponent);
/**
* 获取指令发送者的权限
* @author wzp
* @since 2024/8/1 下午4:50 v0.0.2-dev
* @return 权限
*/
Permissions getPermission();
/**
* 指令发送者是否为管理员
* @author wzp
* @since 2024/8/1 下午4:50 v0.0.2-dev
* @return 是否为管理员
*/
default boolean isAdmin(){
return Permissions.ADMIN.equals(this.getPermission());
}
}

View File

@ -25,4 +25,11 @@ public interface IBot extends CommandSender {
* @return 指令管理器
*/
ICommandManager getCommandManager();
/**
* 停止Bot运行
* @author wzp
* @since 2024/8/1 下午4:57 v0.0.2-dev
*/
void stop();
}

View File

@ -0,0 +1,20 @@
package cn.wzpmc.api.user.permission;
/**
* 权限
* @author wzp
* @version 0.0.2-dev
* @since 2024/8/1 下午4:48
*/
public enum Permissions {
/**
* 普通用户
* @since 2024/8/1 下午4:49 v0.0.2-dev
*/
USER,
/**
* 管理员
* @since 2024/8/1 下午4:49 v0.0.2-dev
*/
ADMIN
}

View File

@ -1,9 +1,27 @@
package cn.wzpmc.commands;
import cn.wzpmc.api.commands.BrigadierCommand;
import cn.wzpmc.api.user.CommandSender;
import cn.wzpmc.api.user.IBot;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import lombok.RequiredArgsConstructor;
/**
* /stop指令
* @author wzp
* @version 0.0.1-dev
* @since 2024/7/31 上午2:26
*/
public class StopCommand {
@RequiredArgsConstructor
public class StopCommand implements BrigadierCommand {
private final IBot bot;
@Override
public LiteralArgumentBuilder<CommandSender> getCommandNode() {
return LiteralArgumentBuilder.<CommandSender>literal("stop")
.requires(CommandSender::isAdmin)
.executes((e) -> {
this.bot.stop();
return 0;
});
}
}

View File

@ -0,0 +1,56 @@
package cn.wzpmc.console;
import cn.wzpmc.entities.user.bot.MyBot;
import cn.wzpmc.network.WebSocketConnectionHandler;
import cn.wzpmc.plugins.CommandManager;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import net.minecrell.terminalconsole.SimpleTerminalConsole;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
/**
* 主控制台
* @author wzp
* @version 0.0.2-dev
* @since 2024/7/31 下午9:47
*/
@Log4j2
@RequiredArgsConstructor
public class MyBotConsole extends SimpleTerminalConsole {
@Getter
private boolean running = true;
private final MyBot bot;
private final CommandManager commandManager;
private final WebSocketConnectionHandler webSocketConnectionHandler;
public MyBotConsole(MyBot bot, WebSocketConnectionHandler webSocketConnectionHandler){
this.bot = bot;
this.commandManager = bot.getCommandManager();
this.webSocketConnectionHandler = webSocketConnectionHandler;
}
@Override
protected LineReader buildReader(LineReaderBuilder builder) {
return super.buildReader(builder.appName("MyBot").completer(commandManager).highlighter(commandManager));
}
@Override
protected void runCommand(String command) {
if (!commandManager.execute(this.bot, command)) {
log.warn("执行指令:`{}`失败!", command);
}
}
@Override
public void shutdown() {
running = false;
this.webSocketConnectionHandler.kill();
}
@Override
public void start() {
this.bot.setConsole(this);
super.start();
}
}

View File

@ -4,10 +4,13 @@ import cn.wzpmc.api.message.MessageComponent;
import cn.wzpmc.api.message.StringMessage;
import cn.wzpmc.api.message.json.JsonMessage;
import cn.wzpmc.api.user.IBot;
import cn.wzpmc.api.user.permission.Permissions;
import cn.wzpmc.configuration.Configuration;
import cn.wzpmc.console.MyBotConsole;
import cn.wzpmc.plugins.CommandManager;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;
/**
@ -21,9 +24,13 @@ import lombok.extern.log4j.Log4j2;
@Getter
public class MyBot implements IBot {
private final Configuration configuration;
private final Long id;
private final Long name;
private final CommandManager commandManager = new CommandManager();
@Setter
private Long id;
@Setter
private Long name;
private final CommandManager commandManager = new CommandManager(this);
@Setter
private MyBotConsole console = null;
@Override
public void sendMessage(MessageComponent messageComponent) {
@ -34,4 +41,16 @@ public class MyBot implements IBot {
log.info(((JsonMessage) messageComponent).toTextDisplay());
}
}
@Override
public Permissions getPermission() {
return Permissions.ADMIN;
}
@Override
public void stop() {
if (this.console != null) {
this.console.shutdown();
}
}
}

View File

@ -27,6 +27,7 @@ public class WebSocketConnectionHandler {
* @author wzp
* @since 2024/7/30 下午11:55 v0.0.1-dev
* @param websocket websocket连接地址
* @return 一个ChannelFuture对象
*/
public ChannelFuture connect(URI websocket){
log.info("正在连接websocket");
@ -44,6 +45,7 @@ public class WebSocketConnectionHandler {
* @since 2024/7/31 上午2:04 v0.0.1-dev
*/
public void kill(){
log.info("结束连接...");
this.eventLoopGroup.shutdownGracefully();
}
}

View File

@ -2,21 +2,31 @@ package cn.wzpmc.plugins;
import cn.wzpmc.api.commands.BrigadierCommand;
import cn.wzpmc.api.commands.RawCommand;
import cn.wzpmc.api.message.StringMessage;
import cn.wzpmc.api.plugins.ICommandManager;
import cn.wzpmc.api.user.CommandSender;
import cn.wzpmc.api.user.IBot;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.ParseResults;
import com.mojang.brigadier.context.ParsedCommandNode;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.Suggestions;
import lombok.NoArgsConstructor;
import com.mojang.brigadier.tree.LiteralCommandNode;
import lombok.SneakyThrows;
import lombok.ToString;
import lombok.extern.log4j.Log4j2;
import org.jline.reader.*;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.regex.Pattern;
/**
* 指令管理器实现类
@ -25,10 +35,14 @@ import java.util.stream.Collectors;
* @since 2024/7/31 上午3:13
*/
@Log4j2
@NoArgsConstructor
public class CommandManager implements ICommandManager {
public class CommandManager implements ICommandManager, Completer, Highlighter {
private final CommandDispatcher<CommandSender> dispatcher = new CommandDispatcher<>();
private final ConcurrentHashMap<String, RawCommand> rawCommands = new ConcurrentHashMap<>();
private static final int[] COLORS = {AttributedStyle.CYAN, AttributedStyle.YELLOW, AttributedStyle.GREEN, AttributedStyle.MAGENTA, AttributedStyle.BLUE};
private final IBot bot;
public CommandManager(IBot bot) {
this.bot = bot;
}
@Override
public void registerCommand(RawCommand rawCommand, String name) {
if (rawCommands.containsKey(name)){
@ -41,6 +55,7 @@ public class CommandManager implements ICommandManager {
public void registerCommand(BrigadierCommand brigadierCommand){
dispatcher.register(brigadierCommand.getCommandNode());
}
@ToString
private static final class CommandPart {
private final String name;
private final List<String> args;
@ -67,6 +82,11 @@ public class CommandManager implements ICommandManager {
try {
dispatcher.execute(rawCommandLine, sender);
} catch (CommandSyntaxException e) {
if (e.getType().equals(CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand())) {
sender.sendMessage(StringMessage.text(this.bot.getConfiguration().getFallback().getCommand()));
return false;
}
sender.sendMessage(StringMessage.text(e.getLocalizedMessage()));
return false;
}
}
@ -79,17 +99,83 @@ public class CommandManager implements ICommandManager {
* @since 2024/7/31 上午3:36 v0.0.1-dev
* @param sender 消息发送者
* @param rawCommandLine 完整命令行
* @param cursor 当前光标位置
* @return 所有被补全的指令
*/
@SneakyThrows
public List<String> tabComplete(CommandSender sender, String rawCommandLine){
public List<String> tabComplete(CommandSender sender, String rawCommandLine, int cursor){
CommandPart commandPart = new CommandPart(rawCommandLine);
List<String> result = new ArrayList<>();
if (rawCommands.containsKey(commandPart.name)) {
result.addAll(rawCommands.get(commandPart.name).onTabComplete(sender, commandPart.args));
}
Suggestions suggestions = dispatcher.getCompletionSuggestions(dispatcher.parse(rawCommandLine, sender)).get();
result.addAll(suggestions.getList().stream().map(Suggestion::getText).collect(Collectors.toList()));
for (Map.Entry<String, RawCommand> stringRawCommandEntry : rawCommands.entrySet()) {
String key = stringRawCommandEntry.getKey();
if (key.contains(commandPart.name)){
result.add(key);
}
}
Suggestions suggestions = dispatcher.getCompletionSuggestions(dispatcher.parse(rawCommandLine, sender), cursor).get();
result.addAll(suggestions.getList().stream().map(Suggestion::getText).toList());
return result;
}
@Override
public void complete(LineReader lineReader, ParsedLine parsedLine, List<Candidate> list) {
String line = parsedLine.line();
int cursor = parsedLine.cursor();
list.addAll(this.tabComplete(this.bot, line, cursor).stream().map(Candidate::new).toList());
}
@Override
public AttributedString highlight(LineReader lineReader, String s) {
final AttributedStringBuilder builder = new AttributedStringBuilder();
String[] strings = s.split(" ");
String commandName = strings[0];
if (rawCommands.containsKey(commandName)){
builder.append(commandName, AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN)).append(' ');
for (int i = 1; i < strings.length; i++) {
builder.append(strings[i]).append(' ');
}
}else {
final ParseResults<CommandSender> results = this.dispatcher.parse(s, this.bot);
int pos = 0;
if (s.startsWith("/")) {
builder.append("/", AttributedStyle.DEFAULT);
pos = 1;
}
int component = -1;
for (final ParsedCommandNode<CommandSender> node : results.getContext().getLastChild().getNodes()) {
if (node.getRange().getStart() >= s.length()) {
break;
}
final int start = node.getRange().getStart();
final int end = Math.min(node.getRange().getEnd(), s.length());
builder.append(s.substring(pos, start), AttributedStyle.DEFAULT);
if (node.getNode() instanceof LiteralCommandNode) {
builder.append(s.substring(start, end), AttributedStyle.DEFAULT);
} else {
if (++component >= COLORS.length) {
component = 0;
}
builder.append(s.substring(start, end), AttributedStyle.DEFAULT.foreground(COLORS[component]));
}
pos = end;
}
if (pos < s.length()) {
builder.append((s.substring(pos)), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
}
}
return builder.toAttributedString();
}
@Override
public void setErrorPattern(Pattern pattern) {
}
@Override
public void setErrorIndex(int i) {
}
}

View File

@ -0,0 +1,23 @@
package cn.wzpmc.plugins;
import cn.wzpmc.api.plugins.BasePlugin;
import cn.wzpmc.api.plugins.IPluginClassLoader;
/**
* Java插件基类
* @author wzp
* @version 0.0.2-dev
* @since 2024/7/31 下午7:01
*/
public class JavaPlugin implements BasePlugin {
@Override
public IPluginClassLoader getClassLoader() {
Class<? extends JavaPlugin> aClass = this.getClass();
ClassLoader loader = aClass.getClassLoader();
if (loader instanceof IPluginClassLoader){
return (IPluginClassLoader) loader;
}
throw new RuntimeException(new IllegalAccessException("You shouldn't load plugin class without PluginClassLoader!!!"));
}
}

View File

@ -0,0 +1,30 @@
package cn.wzpmc.plugins;
import cn.wzpmc.api.plugins.BasePlugin;
import cn.wzpmc.api.plugins.IPluginClassLoader;
import cn.wzpmc.api.user.IBot;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.net.URL;
/**
* 插件类加载器实现
* @author wzp
* @version 0.0.2-dev
* @since 2024/7/31 下午7:12
*/
@Getter
@EqualsAndHashCode(callSuper = true)
@ToString
public class PluginClassLoader extends IPluginClassLoader {
private final IBot bot;
@Setter
private BasePlugin plugin;
public PluginClassLoader(URL[] urls, IBot bot) {
super(urls);
this.bot = bot;
}
}

View File

@ -2,10 +2,10 @@
<Configuration status="WARN" monitorInterval="30">
<Appenders>
<!-- 控制台输出 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%p] %c - %m%n"/>
<TerminalConsole name="Console">
<PatternLayout pattern="%highlight{[%d{HH:mm:ss} %level]: %msg%n%xEx}" disableAnsi="${tca:disableAnsi}"/>
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
</Console>
</TerminalConsole>
<!-- 文件输出 -->
<RollingFile name="File" fileName="./logs/latest.log"
@ -14,7 +14,7 @@
<Policies>
<OnStartupTriggeringPolicy/>
<!-- 按日期滚动日志 -->
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
<TimeBasedTriggeringPolicy interval="86400" modulate="true"/>
</Policies>
<DefaultRolloverStrategy fileIndex="max" max="7"/>
</RollingFile>