From 6b6013586721d80a62c4c53a8ce1464802be8fae Mon Sep 17 00:00:00 2001 From: NorbiPeti Date: Thu, 30 Jul 2020 01:54:58 +0200 Subject: [PATCH] NorbiPeti has completed the challenge [Bullseye] Using the custom event to detect player /stop as now restarts can come from there too (btw it defaults to stop unless a command is ran) Added support for advancements in 1.16: - Now each player gets effectively a reference to the player list for advancements, and since I simply call the method on the original object, it will pass that on, instead of my mock - I tried calling the method that sends the reference on the mock in that case, but that just results in Mockito's version being called which means infinite recursion or something like that (I didn't get a StackOverflowError actually but the server didn't respond) - I tried implementing the method myself but actually I never tested it because at that point I was convinced I can call the original the right way - I had an educated guess that the mock is a subclass of the original class, so I just need to call super.method() right? - But how do you call that reflectively? Apparently Method.invoke() will always respect overriden methods; but thanks to StackOverflow I found out about MethodHandles, and it has the perfect thing I need, findSpecial / unreflectSpecial - Only problem is, kind of like how you can only use super.method() inside the class, you can only use the 'special' methods inside the class... So how could I make it run inside the mocked class? I have no idea since I can only supply an Answer object which has no connection to it, but apparently all the lookup() method actually does is call a constructor with the caller's class - so let's call the constructor! Which is, of course private - So now I have a reflection call creating a Lookup object which can get a handle to the method without checking any overrides and then using that handle to call the original method with the 'this' parameter being the mock #128 --- pom.xml | 2 +- .../discordplugin/DiscordPlugin.java | 2 +- .../broadcaster/PlayerListWatcher.java | 69 ++++++++++++++++++- .../discordplugin/listeners/MCListener.java | 5 +- 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 9859e28..0b752c5 100755 --- a/pom.xml +++ b/pom.xml @@ -180,7 +180,7 @@ com.discord4j discord4j-core - 3.0.14 + 3.0.15 diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java index 8ba5d77..86c3375 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java @@ -248,7 +248,7 @@ public class DiscordPlugin extends ButtonPlugin { .sanitizeString(Bukkit.getOnlinePlayers().stream() .map(Player::getDisplayName).collect(Collectors.joining(", "))) + (Bukkit.getOnlinePlayers().size() == 1 ? " was " : " were ") - + "kicked the hell out.") //TODO: Make configurable + + "thrown out") //TODO: Make configurable : ""); //If 'restart' is disabled then this isn't shown even if joinleave is enabled })).subscribe(), ChannelconBroadcast.RESTART, false); timings.printElapsed("Updating player list"); diff --git a/src/main/java/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.java b/src/main/java/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.java index 3130092..18e70ff 100755 --- a/src/main/java/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.java +++ b/src/main/java/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.java @@ -3,15 +3,23 @@ package buttondevteam.discordplugin.broadcaster; import buttondevteam.discordplugin.mcchat.MCChatUtils; import buttondevteam.lib.TBMCCoreAPI; import lombok.val; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.matcher.ElementMatchers; import org.bukkit.Bukkit; +import org.bukkit.entity.Player; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import java.io.File; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Map; import java.util.UUID; public class PlayerListWatcher { @@ -98,6 +106,18 @@ public class PlayerListWatcher { } val toPlainText = tpt; val sysb = Class.forName(nms + ".SystemUtils").getField("b"); + + //TODO: 1.16 only + //String cbs = nms.replace("net.minecraft.server", "org.bukkit.craftbukkit"); + val epcl = Class.forName(nms + ".EntityPlayer"); + /*val getUUID = epcl.getMethod("getUniqueID"); + val getPlayer = Class.forName(cbs + ".CraftPlayer").getConstructors()[0]; + val getDataFixer = server.getClass().getMethod("getDataFixer"); + val getAdvancementData = server.getClass().getMethod("getAdvancementData"); + val adpcl = Class.forName(nms + ".AdvancementDataPlayer").getConstructors()[0]; + val setEPlayer = getAdvancementData.getReturnType().getMethod("a", epcl);*/ + val adpcl = Class.forName(nms + ".AdvancementDataPlayer"); + //Find the original method without overrides mock = Mockito.mock(dplc, Mockito.withSettings().defaultAnswer(new Answer<>() { // Cannot call super constructor @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -107,10 +127,25 @@ public class PlayerListWatcher { sendAll(invocation.getArgument(0)); return null; } + //In 1.16 it passes a reference to the player list to advancement data for each player + if (method.getName().equals("f") && method.getParameterCount() > 0 && method.getParameterTypes()[0].getSimpleName().equals("EntityPlayer")) { + //val fHandle = MethodHandles.lookup().findSpecial(dplc, "f", MethodType.methodType(adpcl, epcl), mock.getClass()); + System.out.println("Declaring class: " + method.getDeclaringClass()); + method.setAccessible(true); + //new ByteBuddy().subclass(dplc).method(ElementMatchers.named("f")) + var lookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class); + lookupConstructor.setAccessible(true); //Create lookup with a given class instead of caller + var lookup = lookupConstructor.newInstance(mock.getClass()); + val fHandle = lookup.unreflectSpecial(method, mock.getClass()); //Special: super.method() + return fHandle.invoke(mock, invocation.getArgument(0)); //Invoke with our instance, so it passes that to advancement data, we have the fields as well + } return method.invoke(plist, invocation.getArguments()); } val args = invocation.getArguments(); val params = method.getParameterTypes(); + System.out.println("Method: " + method.getName()); + System.out.println("args: " + Arrays.toString(args)); + System.out.println("params: " + Arrays.toString(params)); if (params.length == 0) { TBMCCoreAPI.SendException("Found a strange method", new Exception("Found a sendMessage() method without arguments.")); @@ -142,8 +177,6 @@ public class PlayerListWatcher { var comp = fixComponent.invoke(null, chatComponent); var packet = ppocC.getParameterCount() == 3 ? ppocC.newInstance(comp, chatmessagetype, sysb.get(null)) - - : ppocC.newInstance(comp, chatmessagetype); this.sendAll(packet); // CraftBukkit end @@ -155,7 +188,14 @@ public class PlayerListWatcher { private void sendAll(Object packet) { try { // Some messages get sent by directly constructing a packet sendAll.invoke(plist, packet); + /*if(packet.getClass().getName().contains("Chat")) + System.out.println("Chat packet: "+packet); + if(packet.getClass().getName().contains("Advancement")) + System.out.println("Advancement packet: "+packet);*/ + if (!packet.getClass().getName().contains("KeepAlive")) + System.out.println("Packet: " + packet); if (packet.getClass() == ppoc) { + //System.out.println("Indeed a chat packet"); Field msgf = ppoc.getDeclaredField("a"); msgf.setAccessible(true); MCChatUtils.forAllMCChat(MCChatUtils.send((String) toPlainText.invoke(msgf.get(packet)))); @@ -164,6 +204,31 @@ public class PlayerListWatcher { TBMCCoreAPI.SendException("Failed to broadcast message sent to all players - hacking failed.", e); } } + + /*public Object f(Object entityplayer) { //Returns advancement data + try { + UUID uuid = (UUID) getUUID.invoke(entityplayer); + @SuppressWarnings("unchecked") Map map = (Map) dplc.getField("p").get(plist); + var advancementdataplayer = map.get(uuid); + + if (advancementdataplayer == null) { + var player = (Player) getPlayer.newInstance(Bukkit.getServer(), entityplayer); + var file = new File(player.getWorld().getWorldFolder(), "advancements"); + File file1 = new File(file, uuid + ".json"); + + var dataFixer = getDataFixer.invoke(server); + var advData = getAdvancementData.invoke(server); + advancementdataplayer = adpcl.newInstance(dataFixer, this, advData, file1, entityplayer); + //advancementdataplayer = new AdvancementDataPlayer(this.server.getDataFixer(), this, this.server.getAdvancementData(), file1, entityplayer); + map.put(uuid, advancementdataplayer); + } + + setEPlayer.invoke(advancementdataplayer, entityplayer); + return advancementdataplayer; + } catch (Exception e) { + TBMCCoreAPI.SendException("An error occurred while getting advancement data!", e); + } + }*/ }).stubOnly()); plist = currentPL; for (var plc = dplc; plc != null; plc = plc.getSuperclass()) { //Set all fields diff --git a/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java b/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java index 3afc038..313e4e8 100755 --- a/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java +++ b/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java @@ -3,6 +3,7 @@ package buttondevteam.discordplugin.listeners; import buttondevteam.discordplugin.DiscordPlayer; import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.commands.ConnectCommand; +import buttondevteam.lib.TBMCCommandPreprocessEvent; import buttondevteam.lib.player.TBMCPlayerGetInfoEvent; import buttondevteam.lib.player.TBMCPlayerJoinEvent; import discord4j.core.object.entity.Member; @@ -52,7 +53,7 @@ public class MCListener implements Listener { } @EventHandler - public void onServerCommand(ServerCommandEvent e) { - DiscordPlugin.Restart = !e.getCommand().equalsIgnoreCase("stop"); // The variable is always true except if stopped + public void onCommandPreprocess(TBMCCommandPreprocessEvent e){ + DiscordPlugin.Restart = !e.getMessage().equalsIgnoreCase("/stop"); // The variable is always true except if stopped } }