diff --git a/pom.xml b/pom.xml
index 6d69e8d..38ff5f4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -78,6 +78,40 @@
+
+ maven-compiler-plugin
+
+
+ pre-scala
+
+ -proc:only
+
+ generate-sources
+
+ compile
+
+
+
+
+
+ net.alchim31.maven
+ scala-maven-plugin
+ 4.4.0
+
+
+ generate-sources
+
+ compile
+
+
+
+
+
+ target/generated-sourcess
+ src/main/java
+ src/main/scala
+
+
@@ -123,6 +157,15 @@
papermc
https://papermc.io/repo/repository/maven-public/
+
+
+ true
+
+ ossSonatypeSnapshot
+ OSS Sonatype Snapshots
+ https://oss.sonatype.org/content/repositories/snapshots/
+ default
+
@@ -222,5 +265,15 @@
mockito-core
3.5.13
+
+ org.scala-lang
+ scala-library
+ 2.13.1
+
+
+ io.projectreactor
+ reactor-scala-extensions_2.13
+ 0.7.0
+
diff --git a/src/main/java/buttondevteam/discordplugin/ChromaBot.java b/src/main/java/buttondevteam/discordplugin/ChromaBot.java
deleted file mode 100755
index 92f0e9d..0000000
--- a/src/main/java/buttondevteam/discordplugin/ChromaBot.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package buttondevteam.discordplugin;
-
-import buttondevteam.discordplugin.mcchat.MCChatUtils;
-import discord4j.core.object.entity.Message;
-import discord4j.core.object.entity.channel.MessageChannel;
-import lombok.Getter;
-import org.bukkit.scheduler.BukkitScheduler;
-import reactor.core.publisher.Mono;
-
-import javax.annotation.Nullable;
-import java.util.function.Function;
-
-public class ChromaBot {
- /**
- * May be null if it's not initialized. Initialization happens after the server is done loading (using {@link BukkitScheduler#runTaskAsynchronously(org.bukkit.plugin.Plugin, Runnable)})
- */
- private static @Getter ChromaBot instance;
-
- /**
- * This will set the instance field.
- */
- ChromaBot() {
- instance = this;
- }
-
- static void delete() {
- instance = null;
- }
-
- /**
- * Send a message to the chat channels and private chats.
- *
- * @param message The message to send, duh (use {@link MessageChannel#createMessage(String)})
- */
- public void sendMessage(Function, Mono> message) {
- MCChatUtils.forPublicPrivateChat(message::apply).subscribe();
- }
-
- /**
- * Send a message to the chat channels, private chats and custom chats.
- *
- * @param message The message to send, duh
- * @param toggle The toggle type for channelcon
- */
- public void sendMessageCustomAsWell(Function, Mono> message, @Nullable ChannelconBroadcast toggle) {
- MCChatUtils.forCustomAndAllMCChat(message::apply, toggle, false).subscribe();
- }
-
- public void updatePlayerList() {
- MCChatUtils.updatePlayerList();
- }
-}
diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java
deleted file mode 100755
index 74022e6..0000000
--- a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java
+++ /dev/null
@@ -1,292 +0,0 @@
-package buttondevteam.discordplugin;
-
-import buttondevteam.discordplugin.announcer.AnnouncerModule;
-import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule;
-import buttondevteam.discordplugin.commands.*;
-import buttondevteam.discordplugin.exceptions.ExceptionListenerModule;
-import buttondevteam.discordplugin.fun.FunModule;
-import buttondevteam.discordplugin.listeners.CommonListeners;
-import buttondevteam.discordplugin.listeners.MCListener;
-import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
-import buttondevteam.discordplugin.mccommands.DiscordMCCommand;
-import buttondevteam.discordplugin.role.GameRoleModule;
-import buttondevteam.discordplugin.util.DPState;
-import buttondevteam.discordplugin.util.Timings;
-import buttondevteam.lib.TBMCCoreAPI;
-import buttondevteam.lib.architecture.ButtonPlugin;
-import buttondevteam.lib.architecture.Component;
-import buttondevteam.lib.architecture.ConfigData;
-import buttondevteam.lib.architecture.IHaveConfig;
-import buttondevteam.lib.player.ChromaGamerBase;
-import com.google.common.io.Files;
-import discord4j.common.util.Snowflake;
-import discord4j.core.DiscordClientBuilder;
-import discord4j.core.GatewayDiscordClient;
-import discord4j.core.event.domain.guild.GuildCreateEvent;
-import discord4j.core.event.domain.lifecycle.ReadyEvent;
-import discord4j.core.object.entity.Guild;
-import discord4j.core.object.entity.Role;
-import discord4j.core.object.presence.Activity;
-import discord4j.core.object.presence.Presence;
-import discord4j.core.object.reaction.ReactionEmoji;
-import discord4j.store.jdk.JdkStoreService;
-import lombok.Getter;
-import lombok.val;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.core.Logger;
-import org.bukkit.configuration.file.YamlConfiguration;
-import org.mockito.internal.util.MockUtil;
-import reactor.core.publisher.Mono;
-
-import java.io.File;
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-import java.util.Optional;
-
-@ButtonPlugin.ConfigOpts(disableConfigGen = true)
-public class DiscordPlugin extends ButtonPlugin {
- public static GatewayDiscordClient dc;
- public static DiscordPlugin plugin;
- public static boolean SafeMode = true;
- @Getter
- private Command2DC manager;
- private boolean starting;
- private BukkitLogWatcher logWatcher;
-
- /**
- * The prefix to use with Discord commands like /role. It only works in the bot channel.
- */
- private final ConfigData prefix = getIConfig().getData("prefix", '/', str -> ((String) str).charAt(0), Object::toString);
-
- public static char getPrefix() {
- if (plugin == null) return '/';
- return plugin.prefix.get();
- }
-
- /**
- * The main server where the roles and other information is pulled from. It's automatically set to the first server the bot's invited to.
- */
- private ConfigData> mainServer() {
- return getIConfig().getDataPrimDef("mainServer", 0L,
- id -> {
- //It attempts to get the default as well
- if ((long) id == 0L)
- return Optional.empty(); //Hack?
- return dc.getGuildById(Snowflake.of((long) id))
- .onErrorResume(t -> Mono.fromRunnable(() -> getLogger().warning("Failed to get guild: " + t.getMessage()))).blockOptional();
- },
- g -> g.map(gg -> gg.getId().asLong()).orElse(0L));
- }
-
- /**
- * The (bot) channel to use for Discord commands like /role.
- */
- public ConfigData commandChannel = DPUtils.snowflakeData(getIConfig(), "commandChannel", 0L);
-
- /**
- * The role that allows using mod-only Discord commands.
- * If empty (''), then it will only allow for the owner.
- */
- public ConfigData> modRole;
-
- /**
- * The invite link to show by /discord invite. If empty, it defaults to the first invite if the bot has access.
- */
- public ConfigData inviteLink = getIConfig().getData("inviteLink", "");
-
- private void setupConfig() {
- modRole = DPUtils.roleData(getIConfig(), "modRole", "Moderator");
- }
-
- @Override
- public void onLoad() { //Needed by ServerWatcher
- var thread = Thread.currentThread();
- var cl = thread.getContextClassLoader();
- thread.setContextClassLoader(getClassLoader());
- MockUtil.isMock(null); //Load MockUtil to load Mockito plugins
- thread.setContextClassLoader(cl);
- getLogger().info("Load complete");
- }
-
- @Override
- public void pluginEnable() {
- try {
- getLogger().info("Initializing...");
- plugin = this;
- manager = new Command2DC();
- registerCommand(new DiscordMCCommand()); //Register so that the restart command works
- String token;
- File tokenFile = new File("TBMC", "Token.txt");
- if (tokenFile.exists()) //Legacy support
- //noinspection UnstableApiUsage
- token = Files.readFirstLine(tokenFile, StandardCharsets.UTF_8);
- else {
- File privateFile = new File(getDataFolder(), "private.yml");
- val conf = YamlConfiguration.loadConfiguration(privateFile);
- token = conf.getString("token");
- if (token == null || token.equalsIgnoreCase("Token goes here")) {
- conf.set("token", "Token goes here");
- conf.save(privateFile);
-
- getLogger().severe("Token not found! Please set it in private.yml then do /discord restart");
- getLogger().severe("You need to have a bot account to use with your server.");
- getLogger().severe("If you don't have one, go to https://discordapp.com/developers/applications/ and create an application, then create a bot for it and copy the bot token.");
- return;
- }
- }
- starting = true;
- //System.out.println("This line should show up for sure");
- val cb = DiscordClientBuilder.create(token).build().gateway();
- //System.out.println("Got gateway bootstrap");
- cb.setInitialStatus(si -> Presence.doNotDisturb(Activity.playing("booting")));
- cb.setStoreService(new JdkStoreService()); //The default doesn't work for some reason - it's waaay faster now
- //System.out.println("Initial status and store service set");
- cb.login().doOnError(t -> {
- stopStarting();
- //System.out.println("Got this error: " + t); t.printStackTrace();
- }).subscribe(dc -> {
- //System.out.println("Login successful, got dc: " + dc);
- DiscordPlugin.dc = dc; //Set to gateway client
- dc.on(ReadyEvent.class) // Listen for ReadyEvent(s)
- .map(event -> event.getGuilds().size()) // Get how many guilds the bot is in
- .flatMap(size -> dc
- .on(GuildCreateEvent.class) // Listen for GuildCreateEvent(s)
- .take(size) // Take only the first `size` GuildCreateEvent(s) to be received
- .doOnError(t -> {
- stopStarting();
- //System.out.println("Got error: " + t);
- t.printStackTrace();
- })
- .collectList()).doOnError(t -> stopStarting()).subscribe(this::handleReady); // Take all received GuildCreateEvents and make it a List
- }); /* All guilds have been received, client is fully connected */
- } catch (Exception e) {
- TBMCCoreAPI.SendException("Failed to enable the Discord plugin!", e, this);
- getLogger().severe("You may be able to restart the plugin using /discord restart");
- stopStarting();
- }
- }
-
- private void stopStarting() {
- synchronized (this) {
- starting = false;
- notifyAll();
- }
- }
-
- public static Guild mainServer;
-
- private void handleReady(List event) {
- //System.out.println("Got ready event");
- try {
- if (mainServer != null) { //This is not the first ready event
- getLogger().info("Ready event already handled"); //TODO: It should probably handle disconnections
- dc.updatePresence(Presence.online(Activity.playing("Minecraft"))).subscribe(); //Update from the initial presence
- return;
- }
- mainServer = mainServer().get().orElse(null); //Shouldn't change afterwards
- if (mainServer == null) {
- if (event.size() == 0) {
- getLogger().severe("Main server not found! Invite the bot and do /discord restart");
- dc.getApplicationInfo().subscribe(info ->
- getLogger().severe("Click here: https://discordapp.com/oauth2/authorize?client_id=" + info.getId().asString() + "&scope=bot&permissions=268509264"));
- saveConfig(); //Put default there
- return; //We should have all guilds by now, no need to retry
- }
- mainServer = event.get(0).getGuild();
- getLogger().warning("Main server set to first one: " + mainServer.getName());
- mainServer().set(Optional.of(mainServer)); //Save in config
- }
- SafeMode = false;
- setupConfig();
- DPUtils.disableIfConfigErrorRes(null, commandChannel, DPUtils.getMessageChannel(commandChannel));
- //Won't disable, just prints the warning here
-
- if (MinecraftChatModule.state == DPState.STOPPING_SERVER) {
- stopStarting();
- return; //Reusing that field to check if stopping while still initializing
- }
- CommonListeners.register(dc.getEventDispatcher());
- TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(), this);
- TBMCCoreAPI.RegisterUserClass(DiscordPlayer.class, DiscordPlayer::new);
- ChromaGamerBase.addConverter(sender -> Optional.ofNullable(sender instanceof DiscordSenderBase
- ? ((DiscordSenderBase) sender).getChromaUser() : null));
-
- IHaveConfig.pregenConfig(this, null);
-
- var cb = new ChromaBot(); //Initialize ChromaBot
- Component.registerComponent(this, new GeneralEventBroadcasterModule());
- Component.registerComponent(this, new MinecraftChatModule());
- Component.registerComponent(this, new ExceptionListenerModule());
- Component.registerComponent(this, new GameRoleModule()); //Needs the mainServer to be set
- Component.registerComponent(this, new AnnouncerModule());
- Component.registerComponent(this, new FunModule());
- cb.updatePlayerList(); //The MCChatModule is tested to be enabled
-
- getManager().registerCommand(new VersionCommand());
- getManager().registerCommand(new UserinfoCommand());
- getManager().registerCommand(new HelpCommand());
- getManager().registerCommand(new DebugCommand());
- getManager().registerCommand(new ConnectCommand());
-
- TBMCCoreAPI.SendUnsentExceptions();
- TBMCCoreAPI.SendUnsentDebugMessages();
-
- var blw = new BukkitLogWatcher();
- blw.start();
- ((Logger) LogManager.getRootLogger()).addAppender(blw);
- logWatcher = blw;
-
- if (!TBMCCoreAPI.IsTestServer()) {
- dc.updatePresence(Presence.online(Activity.playing("Minecraft"))).subscribe();
- } else {
- dc.updatePresence(Presence.online(Activity.playing("testing"))).subscribe();
- }
- getLogger().info("Loaded!");
- } catch (Exception e) {
- TBMCCoreAPI.SendException("An error occurred while enabling DiscordPlugin!", e, this);
- }
- stopStarting();
- }
-
- @Override
- public void pluginPreDisable() {
- if (MinecraftChatModule.state == DPState.RUNNING)
- MinecraftChatModule.state = DPState.STOPPING_SERVER;
- synchronized (this) {
- if (starting) {
- try {
- wait(10000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- if (ChromaBot.getInstance() == null) return; //Failed to load
- Timings timings = new Timings();
- timings.printElapsed("Disable start");
- timings.printElapsed("Updating player list");
- ChromaBot.getInstance().updatePlayerList();
- timings.printElapsed("Done");
- }
-
- @Override
- public void pluginDisable() {
- Timings timings = new Timings();
- timings.printElapsed("Actual disable start (logout)");
- if (ChromaBot.getInstance() == null) return; //Failed to load
-
- try {
- SafeMode = true; // Stop interacting with Discord
- ChromaBot.delete();
- ((Logger) LogManager.getRootLogger()).removeAppender(logWatcher);
- timings.printElapsed("Logging out...");
- dc.logout().block();
- mainServer = null; //Allow ReadyEvent again
- //Configs are emptied so channels and servers are fetched again
- } catch (Exception e) {
- TBMCCoreAPI.SendException("An error occured while disabling DiscordPlugin!", e, this);
- }
- }
-
- public static final ReactionEmoji DELIVERED_REACTION = ReactionEmoji.unicode("✅");
-}
diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.scala b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.scala
new file mode 100644
index 0000000..7624ec8
--- /dev/null
+++ b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.scala
@@ -0,0 +1,257 @@
+package buttondevteam.discordplugin
+
+import buttondevteam.discordplugin.announcer.AnnouncerModule
+import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule
+import buttondevteam.discordplugin.commands._
+import buttondevteam.discordplugin.exceptions.ExceptionListenerModule
+import buttondevteam.discordplugin.fun.FunModule
+import buttondevteam.discordplugin.listeners.{CommonListeners, MCListener}
+import buttondevteam.discordplugin.mcchat.MinecraftChatModule
+import buttondevteam.discordplugin.mccommands.DiscordMCCommand
+import buttondevteam.discordplugin.role.GameRoleModule
+import buttondevteam.discordplugin.util.{DPState, Timings}
+import buttondevteam.lib.TBMCCoreAPI
+import buttondevteam.lib.architecture._
+import buttondevteam.lib.player.ChromaGamerBase
+import com.google.common.io.Files
+import discord4j.common.util.Snowflake
+import discord4j.core.{DiscordClientBuilder, GatewayDiscordClient}
+import discord4j.core.`object`.entity.{ApplicationInfo, Guild, Role}
+import discord4j.core.`object`.presence.{Activity, Presence}
+import discord4j.core.`object`.reaction.ReactionEmoji
+import discord4j.core.event.domain.guild.GuildCreateEvent
+import discord4j.core.event.domain.lifecycle.ReadyEvent
+import discord4j.gateway.ShardInfo
+import discord4j.store.jdk.JdkStoreService
+import org.apache.logging.log4j.LogManager
+import org.apache.logging.log4j.core.Logger
+import org.bukkit.command.CommandSender
+import org.bukkit.configuration.file.YamlConfiguration
+import org.mockito.internal.util.MockUtil
+import reactor.core.Disposable
+import reactor.core.publisher.Mono
+
+import java.io.File
+import java.nio.charset.StandardCharsets
+import java.util.Optional
+
+@ButtonPlugin.ConfigOpts(disableConfigGen = true) object DiscordPlugin {
+ private[discordplugin] var dc: GatewayDiscordClient = null
+ private[discordplugin] var plugin: DiscordPlugin = null
+ private[discordplugin] var SafeMode = true
+
+ def getPrefix: Char = {
+ if (plugin == null) return '/'
+ plugin.prefix.get
+ }
+
+ private[discordplugin] var mainServer: Guild = null
+ private[discordplugin] val DELIVERED_REACTION = ReactionEmoji.unicode("✅")
+}
+
+@ButtonPlugin.ConfigOpts(disableConfigGen = true) class DiscordPlugin extends ButtonPlugin {
+ private var _manager: Command2DC = null
+
+ def manager: Command2DC = _manager
+
+ private var starting = false
+ private var logWatcher: BukkitLogWatcher = null
+ /**
+ * The prefix to use with Discord commands like /role. It only works in the bot channel.
+ */
+ final private val prefix = getIConfig.getData("prefix", '/', (str: Any) => str.asInstanceOf[String].charAt(0), (_: Char).toString)
+
+ /**
+ * The main server where the roles and other information is pulled from. It's automatically set to the first server the bot's invited to.
+ */
+ private def mainServer = getIConfig.getDataPrimDef("mainServer", 0L, (id: Any) => {
+ def foo(id: Any) = { //It attempts to get the default as well
+ if (id.asInstanceOf[Long] == 0L) Optional.empty //Hack?
+ else DiscordPlugin.dc.getGuildById(Snowflake.of(id.asInstanceOf[Long])).onErrorResume((t: Throwable) => Mono.fromRunnable(() => getLogger.warning("Failed to get guild: " + t.getMessage))).blockOptional
+ }
+
+ foo(id)
+ }, (g: Optional[Guild]) => (g.map((gg: Guild) => gg.getId.asLong): Optional[Long]).orElse(0L))
+
+ /**
+ * The (bot) channel to use for Discord commands like /role.
+ */
+ var commandChannel: ReadOnlyConfigData[Snowflake] = DPUtils.snowflakeData(getIConfig, "commandChannel", 0L)
+ /**
+ * The role that allows using mod-only Discord commands.
+ * If empty (''), then it will only allow for the owner.
+ */
+ var modRole: ReadOnlyConfigData[Mono[Role]] = null
+ /**
+ * The invite link to show by /discord invite. If empty, it defaults to the first invite if the bot has access.
+ */
+ var inviteLink: ConfigData[String] = getIConfig.getData("inviteLink", "")
+
+ private def setupConfig(): Unit = modRole = DPUtils.roleData(getIConfig, "modRole", "Moderator")
+
+ override def onLoad(): Unit = { //Needed by ServerWatcher
+ val thread = Thread.currentThread
+ val cl = thread.getContextClassLoader
+ thread.setContextClassLoader(getClassLoader)
+ MockUtil.isMock(null) //Load MockUtil to load Mockito plugins
+ thread.setContextClassLoader(cl)
+ getLogger.info("Load complete")
+ }
+
+ override def pluginEnable(): Unit = try {
+ getLogger.info("Initializing...")
+ DiscordPlugin.plugin = this
+ _manager = new Command2DC
+ registerCommand(new DiscordMCCommand) //Register so that the restart command works
+ var token: String = null
+ val tokenFile = new File("TBMC", "Token.txt")
+ if (tokenFile.exists) { //Legacy support
+ //noinspection UnstableApiUsage
+ token = Files.readFirstLine(tokenFile, StandardCharsets.UTF_8)
+ }
+ else {
+ val privateFile = new File(getDataFolder, "private.yml")
+ val conf = YamlConfiguration.loadConfiguration(privateFile)
+ token = conf.getString("token")
+ if (token == null || token.equalsIgnoreCase("Token goes here")) {
+ conf.set("token", "Token goes here")
+ conf.save(privateFile)
+ getLogger.severe("Token not found! Please set it in private.yml then do /discord restart")
+ getLogger.severe("You need to have a bot account to use with your server.")
+ getLogger.severe("If you don't have one, go to https://discordapp.com/developers/applications/ and create an application, then create a bot for it and copy the bot token.")
+ return
+ }
+ }
+ starting = true
+ //System.out.println("This line should show up for sure");
+ val cb = DiscordClientBuilder.create(token).build.gateway
+ //System.out.println("Got gateway bootstrap");
+ cb.setInitialStatus((si: ShardInfo) => Presence.doNotDisturb(Activity.playing("booting")))
+ cb.setStoreService(new JdkStoreService) //The default doesn't work for some reason - it's waaay faster now
+ //System.out.println("Initial status and store service set");
+ cb.login.doOnError((t: Throwable) => {
+ def foo(t: Throwable): Unit = {
+ stopStarting()
+ //System.out.println("Got this error: " + t); t.printStackTrace();
+ }
+
+ foo(t)
+ }).subscribe((dc: GatewayDiscordClient) => {
+ def foo(dc: GatewayDiscordClient): Disposable = { //System.out.println("Login successful, got dc: " + dc);
+ DiscordPlugin.dc = dc //Set to gateway client
+ dc.on(classOf[ReadyEvent]).map(_.getGuilds.size).flatMap(dc.on(classOf[GuildCreateEvent]).take(_).collectList)
+ .doOnError(_ => stopStarting()).subscribe(this.handleReady _) // Take all received GuildCreateEvents and make it a List
+ }
+
+ foo(dc)
+ }) /* All guilds have been received, client is fully connected */
+ } catch {
+ case e: Exception =>
+ TBMCCoreAPI.SendException("Failed to enable the Discord plugin!", e, this)
+ getLogger.severe("You may be able to restart the plugin using /discord restart")
+ stopStarting()
+ }
+
+ private def stopStarting(): Unit = {
+ this synchronized (starting = false)
+ notifyAll()
+ }
+
+ private def handleReady(event: java.util.List[GuildCreateEvent]): Unit = { //System.out.println("Got ready event");
+ try {
+ if (DiscordPlugin.mainServer != null) { //This is not the first ready event
+ getLogger.info("Ready event already handled") //TODO: It should probably handle disconnections
+ DiscordPlugin.dc.updatePresence(Presence.online(Activity.playing("Minecraft"))).subscribe //Update from the initial presence
+ return
+ }
+ DiscordPlugin.mainServer = mainServer.get.orElse(null) //Shouldn't change afterwards
+ if (DiscordPlugin.mainServer == null) {
+ if (event.size == 0) {
+ getLogger.severe("Main server not found! Invite the bot and do /discord restart")
+ DiscordPlugin.dc.getApplicationInfo.subscribe((info: ApplicationInfo) => getLogger.severe("Click here: https://discordapp.com/oauth2/authorize?client_id=" + info.getId.asString + "&scope=bot&permissions=268509264"))
+ saveConfig() //Put default there
+ return //We should have all guilds by now, no need to retry
+ }
+ DiscordPlugin.mainServer = event.get(0).getGuild
+ getLogger.warning("Main server set to first one: " + DiscordPlugin.mainServer.getName)
+ mainServer.set(Optional.of(DiscordPlugin.mainServer)) //Save in config
+ }
+ DiscordPlugin.SafeMode = false
+ setupConfig()
+ DPUtils.disableIfConfigErrorRes(null, commandChannel, DPUtils.getMessageChannel(commandChannel))
+ //Won't disable, just prints the warning here
+ if (MinecraftChatModule.state eq DPState.STOPPING_SERVER) {
+ stopStarting()
+ return //Reusing that field to check if stopping while still initializing
+ }
+ CommonListeners.register(DiscordPlugin.dc.getEventDispatcher)
+ TBMCCoreAPI.RegisterEventsForExceptions(new MCListener, this)
+ TBMCCoreAPI.RegisterUserClass(classOf[DiscordPlayer], () => new DiscordPlayer)
+ ChromaGamerBase.addConverter((sender: CommandSender) => Optional.ofNullable(sender match {
+ case dsender: DiscordSenderBase => dsender.getChromaUser
+ case _ => null
+ }))
+ IHaveConfig.pregenConfig(this, null)
+ ChromaBot.enabled = true //Initialize ChromaBot
+ Component.registerComponent(this, new GeneralEventBroadcasterModule)
+ Component.registerComponent(this, new MinecraftChatModule)
+ Component.registerComponent(this, new ExceptionListenerModule)
+ Component.registerComponent(this, new GameRoleModule) //Needs the mainServer to be set
+ Component.registerComponent(this, new AnnouncerModule)
+ Component.registerComponent(this, new FunModule)
+ ChromaBot.updatePlayerList() //The MCChatModule is tested to be enabled
+ manager.registerCommand(new VersionCommand)
+ manager.registerCommand(new UserinfoCommand)
+ manager.registerCommand(new HelpCommand)
+ manager.registerCommand(new DebugCommand)
+ manager.registerCommand(new ConnectCommand)
+ TBMCCoreAPI.SendUnsentExceptions()
+ TBMCCoreAPI.SendUnsentDebugMessages()
+ val blw = new BukkitLogWatcher
+ blw.start()
+ LogManager.getRootLogger.asInstanceOf[Logger].addAppender(blw)
+ logWatcher = blw
+ if (!TBMCCoreAPI.IsTestServer) DiscordPlugin.dc.updatePresence(Presence.online(Activity.playing("Minecraft"))).subscribe
+ else DiscordPlugin.dc.updatePresence(Presence.online(Activity.playing("testing"))).subscribe
+ getLogger.info("Loaded!")
+ } catch {
+ case e: Exception =>
+ TBMCCoreAPI.SendException("An error occurred while enabling DiscordPlugin!", e, this)
+ }
+ stopStarting()
+ }
+
+ override def pluginPreDisable(): Unit = {
+ if (MinecraftChatModule.state eq DPState.RUNNING) MinecraftChatModule.state = DPState.STOPPING_SERVER
+ this synchronized {
+ if (starting) try wait(10000)
+ catch {
+ case e: InterruptedException =>
+ e.printStackTrace()
+ }
+ }
+ if (!ChromaBot.enabled) return //Failed to load
+ val timings = new Timings
+ timings.printElapsed("Disable start")
+ timings.printElapsed("Updating player list")
+ ChromaBot.updatePlayerList()
+ timings.printElapsed("Done")
+ }
+
+ override def pluginDisable(): Unit = {
+ val timings = new Timings
+ timings.printElapsed("Actual disable start (logout)")
+ if (!ChromaBot.enabled) return
+ try {
+ DiscordPlugin.SafeMode = true // Stop interacting with Discord
+ ChromaBot.enabled = false
+ LogManager.getRootLogger.asInstanceOf[Logger].removeAppender(logWatcher)
+ timings.printElapsed("Logging out...")
+ DiscordPlugin.dc.logout.block
+ DiscordPlugin.mainServer = null //Allow ReadyEvent again
+ } catch {
+ case e: Exception =>
+ TBMCCoreAPI.SendException("An error occured while disabling DiscordPlugin!", e, this)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java b/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java
deleted file mode 100644
index 08bd40f..0000000
--- a/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package buttondevteam.discordplugin.listeners;
-
-import buttondevteam.discordplugin.DPUtils;
-import buttondevteam.discordplugin.DiscordPlugin;
-import buttondevteam.discordplugin.commands.Command2DCSender;
-import buttondevteam.discordplugin.util.Timings;
-import buttondevteam.lib.TBMCCoreAPI;
-import discord4j.common.util.Snowflake;
-import discord4j.core.object.entity.Message;
-import discord4j.core.object.entity.Role;
-import discord4j.core.object.entity.channel.PrivateChannel;
-import lombok.val;
-import reactor.core.publisher.Mono;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-
-public class CommandListener {
- /**
- * Runs a ChromaBot command. If mentionedonly is false, it will only execute the command if it was in #bot with the correct prefix or in private.
- *
- * @param message The Discord message
- * @param mentionedonly Only run the command if ChromaBot is mentioned at the start of the message
- * @return Whether it did not run the command
- */
- public static Mono runCommand(Message message, Snowflake commandChannelID, boolean mentionedonly) {
- Timings timings = CommonListeners.timings;
- Mono ret = Mono.just(true);
- if (message.getContent().length() == 0)
- return ret; //Pin messages and such, let the mcchat listener deal with it
- val content = message.getContent();
- timings.printElapsed("A");
- return message.getChannel().flatMap(channel -> {
- Mono> tmp = ret;
- if (!mentionedonly) { //mentionedonly conditions are in CommonListeners
- timings.printElapsed("B");
- if (!(channel instanceof PrivateChannel)
- && !(content.charAt(0) == DiscordPlugin.getPrefix()
- && channel.getId().asLong() == commandChannelID.asLong())) //
- return ret;
- timings.printElapsed("C");
- tmp = ret.then(channel.type()).thenReturn(true); // Fun (this true is ignored - x)
- }
- final StringBuilder cmdwithargs = new StringBuilder(content);
- val gotmention = new AtomicBoolean();
- timings.printElapsed("Before self");
- return tmp.flatMapMany(x ->
- DiscordPlugin.dc.getSelf().flatMap(self -> self.asMember(DiscordPlugin.mainServer.getId()))
- .flatMapMany(self -> {
- timings.printElapsed("D");
- gotmention.set(checkanddeletemention(cmdwithargs, self.getMention(), message));
- gotmention.set(checkanddeletemention(cmdwithargs, self.getNicknameMention(), message) || gotmention.get());
- val mentions = message.getRoleMentions();
- return self.getRoles().filterWhen(r -> mentions.any(rr -> rr.getName().equals(r.getName())))
- .map(Role::getMention);
- }).map(mentionRole -> {
- timings.printElapsed("E");
- gotmention.set(checkanddeletemention(cmdwithargs, mentionRole, message) || gotmention.get()); // Delete all mentions
- return !mentionedonly || gotmention.get(); //Stops here if false
- }).switchIfEmpty(Mono.fromSupplier(() -> !mentionedonly || gotmention.get())))
- .filter(b -> b).last(false).filter(b -> b).doOnNext(b -> channel.type().subscribe()).flatMap(b -> {
- String cmdwithargsString = cmdwithargs.toString();
- try {
- timings.printElapsed("F");
- if (!DiscordPlugin.plugin.getManager().handleCommand(new Command2DCSender(message), cmdwithargsString))
- return DPUtils.reply(message, channel, "unknown command. Do " + DiscordPlugin.getPrefix() + "help for help.")
- .map(m -> false);
- } catch (Exception e) {
- TBMCCoreAPI.SendException("Failed to process Discord command: " + cmdwithargsString, e, DiscordPlugin.plugin);
- }
- return Mono.just(false); //If the command succeeded or there was an error, return false
- }).defaultIfEmpty(true);
- });
- }
-
- private static boolean checkanddeletemention(StringBuilder cmdwithargs, String mention, Message message) {
- final char prefix = DiscordPlugin.getPrefix();
- if (message.getContent().startsWith(mention)) // TODO: Resolve mentions: Compound arguments, either a mention or text
- if (cmdwithargs.length() > mention.length() + 1) {
- int i = cmdwithargs.indexOf(" ", mention.length());
- if (i == -1)
- i = mention.length();
- else
- //noinspection StatementWithEmptyBody
- for (; i < cmdwithargs.length() && cmdwithargs.charAt(i) == ' '; i++)
- ; //Removes any space before the command
- cmdwithargs.delete(0, i);
- cmdwithargs.insert(0, prefix); //Always use the prefix for processing
- } else
- cmdwithargs.replace(0, cmdwithargs.length(), prefix + "help");
- else {
- if (cmdwithargs.length() == 0)
- cmdwithargs.replace(0, 0, prefix + "help");
- else if (cmdwithargs.charAt(0) != prefix)
- cmdwithargs.insert(0, prefix);
- return false; //Don't treat / as mention, mentions can be used in public mcchat
- }
- return true;
- }
-}
diff --git a/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.scala b/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.scala
new file mode 100644
index 0000000..a810307
--- /dev/null
+++ b/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.scala
@@ -0,0 +1,105 @@
+package buttondevteam.discordplugin.listeners
+
+import buttondevteam.discordplugin.{DPUtils, DiscordPlugin}
+import buttondevteam.discordplugin.commands.Command2DCSender
+import buttondevteam.lib.TBMCCoreAPI
+import discord4j.common.util.Snowflake
+import discord4j.core.`object`.entity.channel.{MessageChannel, PrivateChannel}
+import discord4j.core.`object`.entity.{Member, Message, Role, User}
+import reactor.core.publisher.Mono
+
+import java.util.concurrent.atomic.AtomicBoolean
+
+object CommandListener {
+ /**
+ * Runs a ChromaBot command. If mentionedonly is false, it will only execute the command if it was in #bot with the correct prefix or in private.
+ *
+ * @param message The Discord message
+ * @param mentionedonly Only run the command if ChromaBot is mentioned at the start of the message
+ * @return Whether it did not run the command
+ */
+ def runCommand(message: Message, commandChannelID: Snowflake, mentionedonly: Boolean): Mono[Boolean] = {
+ val timings = CommonListeners.timings
+ val ret = Mono.just(true)
+ if (message.getContent.isEmpty) return ret //Pin messages and such, let the mcchat listener deal with it
+ val content = message.getContent
+ timings.printElapsed("A")
+ message.getChannel.flatMap((channel: MessageChannel) => {
+ def foo(channel: MessageChannel): Mono[Boolean] = {
+ var tmp = ret
+ if (!mentionedonly) { //mentionedonly conditions are in CommonListeners
+ timings.printElapsed("B")
+ if (!channel.isInstanceOf[PrivateChannel] && !(content.charAt(0) == DiscordPlugin.getPrefix && channel.getId.asLong == commandChannelID.asLong)) { //
+ return ret
+ }
+ timings.printElapsed("C")
+ tmp = ret.`then`(channel.`type`).thenReturn(true) // Fun (this true is ignored - x)
+ }
+ val cmdwithargs = new StringBuilder(content)
+ val gotmention = new AtomicBoolean
+ timings.printElapsed("Before self")
+ tmp.flatMapMany((x: Boolean) => DiscordPlugin.dc.getSelf.flatMap((self: User) => self.asMember(DiscordPlugin.mainServer.getId)).flatMapMany((self: Member) => {
+ def foo(self: Member) = {
+ timings.printElapsed("D")
+ gotmention.set(checkanddeletemention(cmdwithargs, self.getMention, message))
+ gotmention.set(checkanddeletemention(cmdwithargs, self.getNicknameMention, message) || gotmention.get)
+ val mentions = message.getRoleMentions
+ self.getRoles.filterWhen((r: Role) => mentions.any((rr: Role) => rr.getName == r.getName)).map(_.getMention)
+ }
+
+ foo(self)
+ }).map((mentionRole: String) => {
+ def foo(mentionRole: String) = {
+ timings.printElapsed("E")
+ gotmention.set(checkanddeletemention(cmdwithargs, mentionRole, message) || gotmention.get) // Delete all mentions
+ !mentionedonly || gotmention.get //Stops here if false
+ }
+
+ foo(mentionRole)
+ }).switchIfEmpty(Mono.fromSupplier(() => !mentionedonly || gotmention.get))).filter((b: Boolean) => b).last(false).filter((b: Boolean) => b).doOnNext((b: Boolean) => channel.`type`.subscribe).flatMap((b: Boolean) => {
+ def foo(): Mono[Boolean] = {
+ val cmdwithargsString = cmdwithargs.toString
+ try {
+ timings.printElapsed("F")
+ if (!DiscordPlugin.plugin.manager.handleCommand(new Command2DCSender(message), cmdwithargsString)) return DPUtils.reply(message, channel, "unknown command. Do " + DiscordPlugin.getPrefix + "help for help.").map((_: Message) => false)
+ } catch {
+ case e: Exception =>
+ TBMCCoreAPI.SendException("Failed to process Discord command: " + cmdwithargsString, e, DiscordPlugin.plugin)
+ }
+ Mono.just(false) //If the command succeeded or there was an error, return false
+ }
+
+ foo()
+ }).defaultIfEmpty(true)
+ }
+
+ foo(channel)
+ })
+ }
+
+ private def checkanddeletemention(cmdwithargs: StringBuilder, mention: String, message: Message): Boolean = {
+ val prefix = DiscordPlugin.getPrefix
+ if (message.getContent.startsWith(mention)) { // TODO: Resolve mentions: Compound arguments, either a mention or text
+ if (cmdwithargs.length > mention.length + 1) {
+ var i = cmdwithargs.indexOf(" ", mention.length)
+ if (i == -1) i = mention.length
+ else { //noinspection StatementWithEmptyBody
+ while ( {
+ i < cmdwithargs.length && cmdwithargs.charAt(i) == ' '
+ }) { //Removes any space before the command
+ i += 1
+ }
+ }
+ cmdwithargs.delete(0, i)
+ cmdwithargs.insert(0, prefix) //Always use the prefix for processing
+ }
+ else cmdwithargs.replace(0, cmdwithargs.length, prefix + "help")
+ }
+ else {
+ if (cmdwithargs.isEmpty) cmdwithargs.replace(0, 0, prefix + "help")
+ else if (cmdwithargs.charAt(0) != prefix) cmdwithargs.insert(0, prefix)
+ return false //Don't treat / as mention, mentions can be used in public mcchat
+ }
+ true
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java
index 19cc441..f76c5a9 100644
--- a/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java
+++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java
@@ -68,7 +68,7 @@ class MCListener implements Listener {
final String message = e.getJoinMessage();
if (message != null && message.trim().length() > 0)
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true).subscribe();
- ChromaBot.getInstance().updatePlayerList();
+ ChromaBot.updatePlayerList();
});
}
@@ -81,7 +81,7 @@ class MCListener implements Listener {
Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin,
() -> Optional.ofNullable(MCChatUtils.LoggedInPlayers.get(e.getPlayer().getUniqueId())).ifPresent(MCChatUtils::callLoginEvents));
Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin,
- ChromaBot.getInstance()::updatePlayerList, 5);
+ ChromaBot::updatePlayerList, 5);
final String message = e.getQuitMessage();
if (message != null && message.trim().length() > 0)
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true).subscribe();
@@ -119,7 +119,7 @@ class MCListener implements Listener {
final DiscordPlayer p = TBMCPlayerBase.getPlayer(source.getPlayer().getUniqueId(), TBMCPlayer.class)
.getAs(DiscordPlayer.class);
if (p == null) return;
- DPUtils.ignoreError(DiscordPlugin.dc.getUserById(Snowflake.of(p.getDiscordID()))
+ DPUtils.ignoreError(DiscordPlugin.dc().getUserById(Snowflake.of(p.getDiscordID()))
.flatMap(user -> user.asMember(DiscordPlugin.mainServer.getId()))
.flatMap(user -> role.flatMap(r -> {
if (e.getValue())
diff --git a/src/main/scala/Test.scala b/src/main/scala/Test.scala
new file mode 100644
index 0000000..743d12a
--- /dev/null
+++ b/src/main/scala/Test.scala
@@ -0,0 +1,5 @@
+import buttondevteam.discordplugin.DiscordPlugin
+
+object Test extends App {
+ println(DiscordPlugin.plugin)
+}
\ No newline at end of file
diff --git a/src/main/scala/buttondevteam/discordplugin/ChromaBot.scala b/src/main/scala/buttondevteam/discordplugin/ChromaBot.scala
new file mode 100644
index 0000000..6a8b7e0
--- /dev/null
+++ b/src/main/scala/buttondevteam/discordplugin/ChromaBot.scala
@@ -0,0 +1,36 @@
+package buttondevteam.discordplugin
+
+import buttondevteam.discordplugin.mcchat.MCChatUtils
+import discord4j.core.`object`.entity.Message
+import discord4j.core.`object`.entity.channel.MessageChannel
+import reactor.core.publisher.Mono
+
+import javax.annotation.Nullable
+
+object ChromaBot {
+ private var _enabled = false
+
+ def enabled = _enabled
+
+ private[discordplugin] def enabled_=(en: Boolean): Unit = _enabled = en
+
+ /**
+ * Send a message to the chat channels and private chats.
+ *
+ * @param message The message to send, duh (use {@link MessageChannel# createMessage ( String )})
+ */
+ def sendMessage(message: java.util.function.Function[Mono[MessageChannel], Mono[Message]]): Unit =
+ MCChatUtils.forPublicPrivateChat(message.apply(_)).subscribe
+
+ /**
+ * Send a message to the chat channels, private chats and custom chats.
+ *
+ * @param message The message to send, duh
+ * @param toggle The toggle type for channelcon
+ */
+ def sendMessageCustomAsWell(message: Function[Mono[MessageChannel], Mono[Message]], @Nullable toggle: ChannelconBroadcast): Unit =
+ MCChatUtils.forCustomAndAllMCChat(message.apply, toggle, false).subscribe
+
+ def updatePlayerList(): Unit =
+ MCChatUtils.updatePlayerList()
+}