diff --git a/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java b/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java index 0c39c56..2c9af57 100644 --- a/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java @@ -67,13 +67,7 @@ public abstract class DiscordConnectedPlayer extends DiscordSenderBase implement this.module = module; uniqueId = uuid; displayName = mcname; - try { - vanillaCmdListener = new VCMDWrapper(VCMDWrapper.createListener(this)); - if (vanillaCmdListener.getListener() == null) - DPUtils.getLogger().warning("Vanilla commands won't be available from Discord due to a compatibility error."); - } catch (NoClassDefFoundError e) { - DPUtils.getLogger().warning("Vanilla commands won't be available from Discord due to a compatibility error."); - } + vanillaCmdListener = new VCMDWrapper(VCMDWrapper.createListener(this)); } /** diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java b/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java index ba823c9..fb7a75a 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java @@ -17,13 +17,7 @@ public abstract class DiscordPlayerSender extends DiscordSenderBase implements I public DiscordPlayerSender(User user, MessageChannel channel, Player player) { super(user, channel); this.player = player; - try { - vanillaCmdListener = new VCMDWrapper(VCMDWrapper.createListener(this, player)); - if (vanillaCmdListener.getListener() == null) - DPUtils.getLogger().warning("Vanilla commands won't be available from Discord due to a compatibility error."); - } catch (NoClassDefFoundError e) { - DPUtils.getLogger().warning("Vanilla commands won't be available from Discord due to a compatibility error."); - } + vanillaCmdListener = new VCMDWrapper(VCMDWrapper.createListener(this, player)); } @Override diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java index 1274df0..815814b 100755 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java @@ -6,6 +6,7 @@ import buttondevteam.discordplugin.listeners.CommandListener; import buttondevteam.discordplugin.listeners.CommonListeners; import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; import buttondevteam.discordplugin.playerfaker.VanillaCommandListener14; +import buttondevteam.discordplugin.playerfaker.VanillaCommandListener15; import buttondevteam.discordplugin.util.Timings; import buttondevteam.lib.*; import buttondevteam.lib.chat.ChatMessage; @@ -361,27 +362,31 @@ public class MCChatListener implements Listener { + "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!"); return true; } + Bukkit.getLogger().info(dsender.getName() + " issued command from Discord: /" + cmd); val channel = clmd == null ? user.channel().get() : clmd.mcchannel; val ev = new TBMCCommandPreprocessEvent(dsender, channel, dmessage, clmd == null ? dsender : clmd.dcp); - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> Bukkit.getPluginManager().callEvent(ev)); - if (ev.isCancelled()) - return true; Bukkit.getScheduler().runTask(DiscordPlugin.plugin, //Commands need to be run sync () -> { + Bukkit.getPluginManager().callEvent(ev); + if (ev.isCancelled()) + return; try { String mcpackage = Bukkit.getServer().getClass().getPackage().getName(); if (mcpackage.contains("1_12")) VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd); - else if (mcpackage.contains("1_14") || mcpackage.contains("1_15")) + else if (mcpackage.contains("1_14")) VanillaCommandListener14.runBukkitOrVanillaCommand(dsender, cmd); + else if (mcpackage.contains("1_15")) + VanillaCommandListener15.runBukkitOrVanillaCommand(dsender, cmd); else Bukkit.dispatchCommand(dsender, cmd); } catch (NoClassDefFoundError e) { TBMCCoreAPI.SendException("A class is not found when trying to run command " + cmd + "!", e); + } catch (Exception e) { + TBMCCoreAPI.SendException("An error occurred when trying to run command " + cmd + "!", e); } - Bukkit.getLogger().info(dsender.getName() + " issued command from Discord: /" + cmd); }); - return false; + return true; } @FunctionalInterface diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java b/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java index 81ca607..fe55919 100644 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java @@ -1,5 +1,6 @@ package buttondevteam.discordplugin.playerfaker; +import buttondevteam.discordplugin.DPUtils; import buttondevteam.discordplugin.DiscordSenderBase; import buttondevteam.discordplugin.IMCPlayer; import lombok.Getter; @@ -28,12 +29,29 @@ public class VCMDWrapper { * @param bukkitplayer The Bukkit player to send the raw message to */ public static > Object createListener(T player, Player bukkitplayer) { - String mcpackage = Bukkit.getServer().getClass().getPackage().getName(); - if (mcpackage.contains("1_12")) - return bukkitplayer == null ? new VanillaCommandListener<>(player) : new VanillaCommandListener<>(player, bukkitplayer); - else if (mcpackage.contains("1_14") || mcpackage.contains("1_15")) - return bukkitplayer == null ? new VanillaCommandListener14<>(player) : new VanillaCommandListener14<>(player, bukkitplayer); - else + try { + Object ret; + String mcpackage = Bukkit.getServer().getClass().getPackage().getName(); + if (mcpackage.contains("1_12")) + ret = new VanillaCommandListener<>(player, bukkitplayer); + else if (mcpackage.contains("1_14")) + ret = new VanillaCommandListener14<>(player, bukkitplayer); + else if (mcpackage.contains("1_15")) + ret = VanillaCommandListener15.create(player, bukkitplayer); //bukkitplayer may be null but that's fine + else + ret = null; + if (ret == null) + compatWarning(); + return ret; + } catch (NoClassDefFoundError | Exception e) { + compatWarning(); + if (!(e instanceof NoClassDefFoundError)) + e.printStackTrace(); return null; + } + } + + private static void compatWarning() { + DPUtils.getLogger().warning("Vanilla commands won't be available from Discord due to a compatibility error."); } } diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener15.java b/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener15.java new file mode 100644 index 0000000..31829e9 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener15.java @@ -0,0 +1,136 @@ +package buttondevteam.discordplugin.playerfaker; + +import buttondevteam.discordplugin.DiscordSenderBase; +import buttondevteam.discordplugin.IMCPlayer; +import lombok.Getter; +import lombok.val; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.command.SimpleCommandMap; +import org.bukkit.entity.Player; +import org.mockito.Answers; +import org.mockito.Mockito; + +import java.lang.reflect.Modifier; +import java.util.Arrays; + +/** + * Same as {@link VanillaCommandListener14} but with reflection + */ +public class VanillaCommandListener15> { + private @Getter T player; + private static Class vcwcl; + private static String nms; + + protected VanillaCommandListener15(T player, Player bukkitplayer) { + this.player = player; + if (bukkitplayer != null && !bukkitplayer.getClass().getSimpleName().endsWith("CraftPlayer")) + throw new ClassCastException("bukkitplayer must be a Bukkit player!"); + } + + /** + * This method will only send raw vanilla messages to the sender in plain text. + * + * @param player The Discord sender player (the wrapper) + */ + public static > VanillaCommandListener15 create(T player) throws Exception { + return create(player, null); + } + + /** + * This method will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player. + * + * @param player The Discord sender player (the wrapper) + * @param bukkitplayer The Bukkit player to send the raw message to + */ + @SuppressWarnings("unchecked") + public static > VanillaCommandListener15 create(T player, Player bukkitplayer) throws Exception { + if (vcwcl == null) { + String pkg = Bukkit.getServer().getClass().getPackage().getName(); + vcwcl = Class.forName(pkg + ".command.VanillaCommandWrapper"); + } + if (nms == null) { + var server = Bukkit.getServer(); + nms = server.getClass().getMethod("getServer").invoke(server).getClass().getPackage().getName(); //org.mockito.codegen + } + var iclcl = Class.forName(nms + ".ICommandListener"); + return Mockito.mock(VanillaCommandListener15.class, Mockito.withSettings().stubOnly() + .useConstructor(player, bukkitplayer).extraInterfaces(iclcl).defaultAnswer(invocation -> { + if (invocation.getMethod().getName().equals("sendMessage")) { + var icbc = invocation.getArgument(0); + player.sendMessage((String) icbc.getClass().getMethod("getString").invoke(icbc)); + if (bukkitplayer != null) { + var handle = bukkitplayer.getClass().getMethod("getHandle").invoke(bukkitplayer); + handle.getClass().getMethod("sendMessage", icbc.getClass()).invoke(handle, icbc); + } + return null; + } + if (!Modifier.isAbstract(invocation.getMethod().getModifiers())) + return invocation.callRealMethod(); + if (invocation.getMethod().getReturnType() == boolean.class) + return true; //shouldSend... shouldBroadcast... + if (invocation.getMethod().getReturnType() == CommandSender.class) + return player; + return Answers.RETURNS_DEFAULTS.answer(invocation); + })); + } + + public static boolean runBukkitOrVanillaCommand(DiscordSenderBase dsender, String cmdstr) throws Exception { + var server = Bukkit.getServer(); + var cmap = (SimpleCommandMap) server.getClass().getMethod("getCommandMap").invoke(server); + val cmd = cmap.getCommand(cmdstr.split(" ")[0].toLowerCase()); + if (!(dsender instanceof Player) || !vcwcl.isAssignableFrom(dsender.getClass())) + return Bukkit.dispatchCommand(dsender, cmdstr); // Unconnected users are treated well in vanilla cmds + + if (!(dsender instanceof IMCPlayer)) + throw new ClassCastException( + "dsender needs to implement IMCPlayer to use vanilla commands as it implements Player."); + + IMCPlayer sender = (IMCPlayer) dsender; // Don't use val on recursive interfaces :P + + if (!(Boolean) vcwcl.getMethod("testPermission", CommandSender.class).invoke(cmd, sender)) + return true; + + var cworld = Bukkit.getWorlds().get(0); + val world = cworld.getClass().getMethod("getHandle").invoke(cworld); + var icommandlistener = sender.getVanillaCmdListener().getListener(); + var nms = icommandlistener.getClass().getPackage().getName(); + var clwcl = Class.forName(nms + ".CommandListenerWrapper"); + var v3dcl = Class.forName(nms + ".Vec3D"); + var v2fcl = Class.forName(nms + ".Vec2F"); + var icbcl = Class.forName(nms + ".IChatBaseComponent"); + var mcscl = Class.forName(nms + ".MinecraftServer"); + var ecl = Class.forName(nms + ".Entity"); + var cctcl = Class.forName(nms + ".ChatComponentText"); + Object wrapper = clwcl.getConstructor(icommandlistener.getClass(), v3dcl, v2fcl, world.getClass(), int.class, String.class, icbcl, mcscl, ecl) + .newInstance(icommandlistener, + v3dcl.getConstructor(double.class, double.class, double.class).newInstance(0, 0, 0), + v2fcl.getConstructor(float.class, float.class).newInstance(0, 0), + world, 0, sender.getName(), cctcl.getConstructor(String.class).newInstance(sender.getName()), + world.getClass().getMethod("getMinecraftServer").invoke(world), null); + /*val wrapper = new CommandListenerWrapper(icommandlistener, new Vec3D(0, 0, 0), + new Vec2F(0, 0), world, 0, sender.getName(), + new ChatComponentText(sender.getName()), world.getMinecraftServer(), null);*/ + var pncscl = Class.forName(vcwcl.getPackage().getName() + ".ProxiedNativeCommandSender"); + Object pncs = pncscl.getConstructor(clwcl, sender.getClass(), sender.getClass()) + .newInstance(wrapper, sender, sender); + String[] args = cmdstr.split(" "); + args = Arrays.copyOfRange(args, 1, args.length); + try { + return cmd.execute((CommandSender) pncs, cmd.getLabel(), args); + } catch (Exception commandexception) { + if (!commandexception.getClass().getSimpleName().equals("CommandException")) + throw commandexception; + // Taken from CommandHandler + var cmcl = Class.forName(nms + ".ChatMessage"); + var chatmessage = cmcl.getConstructor(String.class, Object[].class) + .newInstance(commandexception.getMessage(), + new Object[]{commandexception.getClass().getMethod("a").invoke(commandexception)}); + var modifier = cmcl.getMethod("getChatModifier").invoke(chatmessage); + var ecfcl = Class.forName(nms + ".EnumChatFormat"); + modifier.getClass().getMethod("setColor", ecfcl).invoke(modifier, ecfcl.getField("RED").get(null)); + icommandlistener.getClass().getMethod("sendMessage", icbcl).invoke(icommandlistener, chatmessage); + } + return true; + } +}