diff --git a/pom.xml b/pom.xml index 4b4d005..309105f 100755 --- a/pom.xml +++ b/pom.xml @@ -215,7 +215,7 @@ org.mockito mockito-core - 3.5.10 + 3.5.13 org.mockito diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java index 7d60a22..91db049 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java @@ -35,7 +35,6 @@ import lombok.val; import org.bukkit.Bukkit; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; -import org.mockito.internal.util.MockUtil; import reactor.core.publisher.Mono; import java.awt.*; @@ -102,16 +101,17 @@ public class DiscordPlugin extends ButtonPlugin { return getIConfig().getData("inviteLink", ""); } - @Override + /*@Override public void onLoad() { //Needed by ServerWatcher var thread = Thread.currentThread(); getLogger().info("Setting context class loader for " + thread); var cl = thread.getContextClassLoader(); thread.setContextClassLoader(getClassLoader()); MockUtil.isMock(null); //Load MockUtil to load Mockito plugins + //new ByteBuddy().rebase(Player.class) getLogger().info("Restoring context class loader"); thread.setContextClassLoader(cl); - } + }*/ @Override public void pluginEnable() { diff --git a/src/main/java/buttondevteam/discordplugin/broadcaster/GeneralEventBroadcasterModule.java b/src/main/java/buttondevteam/discordplugin/broadcaster/GeneralEventBroadcasterModule.java index 44492eb..8dd9b52 100644 --- a/src/main/java/buttondevteam/discordplugin/broadcaster/GeneralEventBroadcasterModule.java +++ b/src/main/java/buttondevteam/discordplugin/broadcaster/GeneralEventBroadcasterModule.java @@ -15,7 +15,7 @@ public class GeneralEventBroadcasterModule extends Component { @Override protected void enable() { try { - PlayerListWatcher.hookUpDown(true); + PlayerListWatcher.hookUpDown(true, this); log("Finished hooking into the player list"); hooked = true; } catch (Exception e) { @@ -30,7 +30,7 @@ public class GeneralEventBroadcasterModule extends Component { protected void disable() { try { if (!hooked) return; - if (PlayerListWatcher.hookUpDown(false)) + if (PlayerListWatcher.hookUpDown(false, this)) log("Finished unhooking the player list!"); else log("Didn't have the player list hooked."); diff --git a/src/main/java/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.java b/src/main/java/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.java index 396df84..39f69ce 100755 --- a/src/main/java/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.java +++ b/src/main/java/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.java @@ -21,7 +21,7 @@ public class PlayerListWatcher { private static Object mock; private static MethodHandle fHandle; //Handle for PlayerList.f(EntityPlayer) - Only needed for 1.16 - static boolean hookUpDown(boolean up) throws Exception { + static boolean hookUpDown(boolean up, GeneralEventBroadcasterModule module) throws Exception { val csc = Bukkit.getServer().getClass(); Field conf = csc.getDeclaredField("console"); conf.setAccessible(true); @@ -30,6 +30,10 @@ public class PlayerListWatcher { val dplc = Class.forName(nms + ".DedicatedPlayerList"); val currentPL = server.getClass().getMethod("getPlayerList").invoke(server); if (up) { + if (currentPL == mock) { + module.logWarn("Player list already mocked!"); + return false; + } val icbcl = Class.forName(nms + ".IChatBaseComponent"); Method sendMessageTemp; try { @@ -84,7 +88,11 @@ public class PlayerListWatcher { if (fHandle == null) { assert lookupConstructor != null; var lookup = lookupConstructor.newInstance(mock.getClass()); + //var mcl = method.getDeclaringClass(); fHandle = lookup.unreflectSpecial(method, mock.getClass()); //Special: super.method() + /*if (mcl.getSimpleName().contains("Mock")) //inline mock + lookup.findSpecial(mcl, method.getName(), ) + fHandle.type()*/ } return fHandle.invoke(mock, invocation.getArgument(0)); //Invoke with our instance, so it passes that to advancement data, we have the fields as well } diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java b/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java index bb039f7..038d58e 100644 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java @@ -1,21 +1,38 @@ package buttondevteam.discordplugin.playerfaker; import buttondevteam.discordplugin.mcchat.MCChatUtils; +import buttondevteam.lib.TBMCCoreAPI; import com.destroystokyo.paper.profile.CraftPlayerProfile; import lombok.RequiredArgsConstructor; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.agent.ByteBuddyAgent; +import net.bytebuddy.dynamic.loading.ClassReloadingStrategy; +import net.bytebuddy.dynamic.scaffold.MethodGraph; +import net.bytebuddy.dynamic.scaffold.TypeValidation; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.implementation.MethodCall; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.IgnoreForBinding; +import net.bytebuddy.implementation.bind.annotation.RuntimeType; +import net.bytebuddy.implementation.bind.annotation.SuperCall; import org.bukkit.Bukkit; import org.bukkit.Server; import org.bukkit.entity.Player; -import org.mockito.Mockito; +import org.objenesis.ObjenesisStd; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; +import java.util.concurrent.Callable; + +import static net.bytebuddy.matcher.ElementMatchers.*; public class ServerWatcher { private List playerList; private final List fakePlayers = new ArrayList<>(); private Server origServer; + @IgnoreForBinding public void enableDisable(boolean enable) throws Exception { var serverField = Bukkit.class.getDeclaredField("server"); serverField.setAccessible(true); @@ -30,50 +47,21 @@ public class ServerWatcher { } catch (IOException e) { throw new IllegalStateException("Failed to load " + MockMaker.class, e); }*/ - var settings = Mockito.withSettings().stubOnly() - .defaultAnswer(invocation -> { - var method = invocation.getMethod(); - int pc = method.getParameterCount(); - Player player = null; - switch (method.getName()) { - case "getPlayer": - if (pc == 1 && method.getParameterTypes()[0] == UUID.class) - player = MCChatUtils.LoggedInPlayers.get(invocation.getArgument(0)); - break; - case "getPlayerExact": - if (pc == 1) { - final String argument = invocation.getArgument(0); - player = MCChatUtils.LoggedInPlayers.values().stream() - .filter(dcp -> dcp.getName().equalsIgnoreCase(argument)).findAny().orElse(null); - } - break; - case "getOnlinePlayers": - if (playerList == null) { - @SuppressWarnings("unchecked") var list = (List) method.invoke(origServer, invocation.getArguments()); - playerList = new AppendListView<>(list, fakePlayers); - } - return playerList; - case "createProfile": //Paper's method, casts the player to a CraftPlayer - if (pc == 2) { - UUID uuid = invocation.getArgument(0); - String name = invocation.getArgument(1); - player = uuid != null ? MCChatUtils.LoggedInPlayers.get(uuid) : null; - if (player == null && name != null) - player = MCChatUtils.LoggedInPlayers.values().stream() - .filter(dcp -> dcp.getName().equalsIgnoreCase(name)).findAny().orElse(null); - if (player != null) - return new CraftPlayerProfile(player.getUniqueId(), player.getName()); - } - break; - } - if (player != null) - return player; - return method.invoke(origServer, invocation.getArguments()); - }); - //var mock = mockMaker.createMock(settings, MockHandlerFactory.createMockHandler(settings)); - //thread.setContextClassLoader(cl); - var mock = Mockito.mock(serverClass, settings); + ByteBuddyAgent.install(); var originalServer = serverField.get(null); + var impl = MethodDelegation.to(this); + var names = Arrays.stream(ServerWatcher.class.getMethods()).map(Method::getName).toArray(String[]::new); + var utype = new ByteBuddy() //InlineBytecodeGenerator + .with(TypeValidation.DISABLED) + .with(Implementation.Context.Disabled.Factory.INSTANCE) + .with(MethodGraph.Compiler.ForDeclaredMethods.INSTANCE) + .ignore(isSynthetic().and(not(isConstructor())).or(isDefaultFinalizer())) + .redefine(serverClass) + .method(target -> !target.isStatic()).intercept(MethodCall.invokeSelf().on(originalServer).withAllArguments()) + .method(target -> Arrays.stream(names).anyMatch(target.getActualName()::equalsIgnoreCase)).intercept(impl).make(); + var ltype = utype.load(serverClass.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent()); + var type = ltype.getLoaded(); + var mock = new ObjenesisStd().newInstance(type); for (var field : serverClass.getFields()) //Copy public fields, private fields aren't accessible directly anyways if (!Modifier.isFinal(field.getModifiers()) && !Modifier.isStatic(field.getModifiers())) field.set(mock, field.get(originalServer)); @@ -83,6 +71,62 @@ public class ServerWatcher { serverField.set(null, origServer); } + public Player getPlayer(UUID uuid, @SuperCall Callable original) { + try { + var player = MCChatUtils.LoggedInPlayers.get(uuid); + if (player == null) return original.call(); + return player; + } catch (Exception e) { + TBMCCoreAPI.SendException("Failed to handle getPlayer!", e); + return null; + } + } + + public Player getPlayerExact(String name, @SuperCall Callable original) { + try { + var player = MCChatUtils.LoggedInPlayers.values().stream() + .filter(dcp -> dcp.getName().equalsIgnoreCase(name)).findAny().orElse(null); + if (player == null) return original.call(); + return player; + } catch (Exception e) { + TBMCCoreAPI.SendException("Failed to handle getPlayerExact!", e); + return null; + } + } + + @RuntimeType + //public List getOnlinePlayers(@SuperCall Callable> original) { + public List getOnlinePlayers() { + try { + if (playerList == null) { + //var list = original.call(); + var list = new ArrayList(); + playerList = new AppendListView<>(list, fakePlayers); + } + return playerList; + } catch ( + Exception e) { + TBMCCoreAPI.SendException("Failed to handle getOnlinePlayers!", e); + return null; + } + } + + @RuntimeType + public Object createProfile(UUID uuid, String name) { //Paper's method, casts the player to a CraftPlayer + try { + var player = uuid != null ? MCChatUtils.LoggedInPlayers.get(uuid) : null; + if (player == null && name != null) + player = MCChatUtils.LoggedInPlayers.values().stream() + .filter(dcp -> dcp.getName().equalsIgnoreCase(name)).findAny().orElse(null); + if (player != null) + return new CraftPlayerProfile(player.getUniqueId(), player.getName()); + return null; + } catch (Exception e) { + TBMCCoreAPI.SendException("Failed to handle createProfile!", e); + return null; + } + } + @RequiredArgsConstructor public static class AppendListView extends AbstractSequentialList { private final List originalList;