Attempt to use ByteBuddy directly for ServerWatcher
This commit is contained in:
parent
beae6e6ce0
commit
3f2fa286fb
5 changed files with 103 additions and 51 deletions
2
pom.xml
2
pom.xml
|
@ -215,7 +215,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mockito</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
<artifactId>mockito-core</artifactId>
|
<artifactId>mockito-core</artifactId>
|
||||||
<version>3.5.10</version>
|
<version>3.5.13</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mockito</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
|
|
|
@ -35,7 +35,6 @@ import lombok.val;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.mockito.internal.util.MockUtil;
|
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
@ -102,16 +101,17 @@ public class DiscordPlugin extends ButtonPlugin {
|
||||||
return getIConfig().getData("inviteLink", "");
|
return getIConfig().getData("inviteLink", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/*@Override
|
||||||
public void onLoad() { //Needed by ServerWatcher
|
public void onLoad() { //Needed by ServerWatcher
|
||||||
var thread = Thread.currentThread();
|
var thread = Thread.currentThread();
|
||||||
getLogger().info("Setting context class loader for " + thread);
|
getLogger().info("Setting context class loader for " + thread);
|
||||||
var cl = thread.getContextClassLoader();
|
var cl = thread.getContextClassLoader();
|
||||||
thread.setContextClassLoader(getClassLoader());
|
thread.setContextClassLoader(getClassLoader());
|
||||||
MockUtil.isMock(null); //Load MockUtil to load Mockito plugins
|
MockUtil.isMock(null); //Load MockUtil to load Mockito plugins
|
||||||
|
//new ByteBuddy().rebase(Player.class)
|
||||||
getLogger().info("Restoring context class loader");
|
getLogger().info("Restoring context class loader");
|
||||||
thread.setContextClassLoader(cl);
|
thread.setContextClassLoader(cl);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void pluginEnable() {
|
public void pluginEnable() {
|
||||||
|
|
|
@ -15,7 +15,7 @@ public class GeneralEventBroadcasterModule extends Component<DiscordPlugin> {
|
||||||
@Override
|
@Override
|
||||||
protected void enable() {
|
protected void enable() {
|
||||||
try {
|
try {
|
||||||
PlayerListWatcher.hookUpDown(true);
|
PlayerListWatcher.hookUpDown(true, this);
|
||||||
log("Finished hooking into the player list");
|
log("Finished hooking into the player list");
|
||||||
hooked = true;
|
hooked = true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -30,7 +30,7 @@ public class GeneralEventBroadcasterModule extends Component<DiscordPlugin> {
|
||||||
protected void disable() {
|
protected void disable() {
|
||||||
try {
|
try {
|
||||||
if (!hooked) return;
|
if (!hooked) return;
|
||||||
if (PlayerListWatcher.hookUpDown(false))
|
if (PlayerListWatcher.hookUpDown(false, this))
|
||||||
log("Finished unhooking the player list!");
|
log("Finished unhooking the player list!");
|
||||||
else
|
else
|
||||||
log("Didn't have the player list hooked.");
|
log("Didn't have the player list hooked.");
|
||||||
|
|
|
@ -21,7 +21,7 @@ public class PlayerListWatcher {
|
||||||
private static Object mock;
|
private static Object mock;
|
||||||
private static MethodHandle fHandle; //Handle for PlayerList.f(EntityPlayer) - Only needed for 1.16
|
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();
|
val csc = Bukkit.getServer().getClass();
|
||||||
Field conf = csc.getDeclaredField("console");
|
Field conf = csc.getDeclaredField("console");
|
||||||
conf.setAccessible(true);
|
conf.setAccessible(true);
|
||||||
|
@ -30,6 +30,10 @@ public class PlayerListWatcher {
|
||||||
val dplc = Class.forName(nms + ".DedicatedPlayerList");
|
val dplc = Class.forName(nms + ".DedicatedPlayerList");
|
||||||
val currentPL = server.getClass().getMethod("getPlayerList").invoke(server);
|
val currentPL = server.getClass().getMethod("getPlayerList").invoke(server);
|
||||||
if (up) {
|
if (up) {
|
||||||
|
if (currentPL == mock) {
|
||||||
|
module.logWarn("Player list already mocked!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
val icbcl = Class.forName(nms + ".IChatBaseComponent");
|
val icbcl = Class.forName(nms + ".IChatBaseComponent");
|
||||||
Method sendMessageTemp;
|
Method sendMessageTemp;
|
||||||
try {
|
try {
|
||||||
|
@ -84,7 +88,11 @@ public class PlayerListWatcher {
|
||||||
if (fHandle == null) {
|
if (fHandle == null) {
|
||||||
assert lookupConstructor != null;
|
assert lookupConstructor != null;
|
||||||
var lookup = lookupConstructor.newInstance(mock.getClass());
|
var lookup = lookupConstructor.newInstance(mock.getClass());
|
||||||
|
//var mcl = method.getDeclaringClass();
|
||||||
fHandle = lookup.unreflectSpecial(method, mock.getClass()); //Special: super.method()
|
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
|
return fHandle.invoke(mock, invocation.getArgument(0)); //Invoke with our instance, so it passes that to advancement data, we have the fields as well
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,38 @@
|
||||||
package buttondevteam.discordplugin.playerfaker;
|
package buttondevteam.discordplugin.playerfaker;
|
||||||
|
|
||||||
import buttondevteam.discordplugin.mcchat.MCChatUtils;
|
import buttondevteam.discordplugin.mcchat.MCChatUtils;
|
||||||
|
import buttondevteam.lib.TBMCCoreAPI;
|
||||||
import com.destroystokyo.paper.profile.CraftPlayerProfile;
|
import com.destroystokyo.paper.profile.CraftPlayerProfile;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.Bukkit;
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
import org.bukkit.entity.Player;
|
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.lang.reflect.Modifier;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
import static net.bytebuddy.matcher.ElementMatchers.*;
|
||||||
|
|
||||||
public class ServerWatcher {
|
public class ServerWatcher {
|
||||||
private List<Player> playerList;
|
private List<Player> playerList;
|
||||||
private final List<Player> fakePlayers = new ArrayList<>();
|
private final List<Player> fakePlayers = new ArrayList<>();
|
||||||
private Server origServer;
|
private Server origServer;
|
||||||
|
|
||||||
|
@IgnoreForBinding
|
||||||
public void enableDisable(boolean enable) throws Exception {
|
public void enableDisable(boolean enable) throws Exception {
|
||||||
var serverField = Bukkit.class.getDeclaredField("server");
|
var serverField = Bukkit.class.getDeclaredField("server");
|
||||||
serverField.setAccessible(true);
|
serverField.setAccessible(true);
|
||||||
|
@ -30,50 +47,21 @@ public class ServerWatcher {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new IllegalStateException("Failed to load " + MockMaker.class, e);
|
throw new IllegalStateException("Failed to load " + MockMaker.class, e);
|
||||||
}*/
|
}*/
|
||||||
var settings = Mockito.withSettings().stubOnly()
|
ByteBuddyAgent.install();
|
||||||
.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.<UUID>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<Player>) 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);
|
|
||||||
var originalServer = serverField.get(null);
|
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
|
for (var field : serverClass.getFields()) //Copy public fields, private fields aren't accessible directly anyways
|
||||||
if (!Modifier.isFinal(field.getModifiers()) && !Modifier.isStatic(field.getModifiers()))
|
if (!Modifier.isFinal(field.getModifiers()) && !Modifier.isStatic(field.getModifiers()))
|
||||||
field.set(mock, field.get(originalServer));
|
field.set(mock, field.get(originalServer));
|
||||||
|
@ -83,6 +71,62 @@ public class ServerWatcher {
|
||||||
serverField.set(null, origServer);
|
serverField.set(null, origServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Player getPlayer(UUID uuid, @SuperCall Callable<Player> 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<Player> 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<Player> getOnlinePlayers(@SuperCall Callable<List<Player>> original) {
|
||||||
|
public List<Player> getOnlinePlayers() {
|
||||||
|
try {
|
||||||
|
if (playerList == null) {
|
||||||
|
//var list = original.call();
|
||||||
|
var list = new ArrayList<Player>();
|
||||||
|
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
|
@RequiredArgsConstructor
|
||||||
public static class AppendListView<T> extends AbstractSequentialList<T> {
|
public static class AppendListView<T> extends AbstractSequentialList<T> {
|
||||||
private final List<T> originalList;
|
private final List<T> originalList;
|
||||||
|
|
Loading…
Reference in a new issue