Use reflection for VanillaCommandListener

Moved error handling to the wrapper
Fixed commands on Discord getting executed even if the preprocess event got cancelled
This commit is contained in:
Norbi Peti 2020-03-15 23:55:02 +01:00
parent 1b747ab99f
commit 50cc0c8e61
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
5 changed files with 173 additions and 26 deletions

View file

@ -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.");
}
}
/**

View file

@ -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.");
}
}
@Override

View file

@ -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

View file

@ -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 <T extends DiscordSenderBase & IMCPlayer<T>> Object createListener(T player, Player bukkitplayer) {
try {
Object ret;
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);
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.");
}
}

View file

@ -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<T extends DiscordSenderBase & IMCPlayer<T>> {
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 <T extends DiscordSenderBase & IMCPlayer<T>> VanillaCommandListener15<T> 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 <T extends DiscordSenderBase & IMCPlayer<T>> VanillaCommandListener15<T> 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;
}
}