Mirror von
https://github.com/ViaVersion/ViaVersion.git
synchronisiert 2024-12-26 00:00:28 +01:00
Cleanup Bukkit/Sponge injectors
Dieser Commit ist enthalten in:
Ursprung
a306c222d4
Commit
427b0a68d0
@ -78,14 +78,18 @@ public interface ViaInjector {
|
||||
*
|
||||
* @return The name
|
||||
*/
|
||||
String getEncoderName();
|
||||
default String getEncoderName() {
|
||||
return "via-encoder";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the decoder for then netty pipeline for this platform.
|
||||
*
|
||||
* @return The name
|
||||
*/
|
||||
String getDecoderName();
|
||||
default String getDecoderName() {
|
||||
return "via-decoder";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any relevant data for debugging injection issues.
|
||||
|
@ -84,14 +84,6 @@ public class ViaVersionPlugin extends JavaPlugin implements ViaPlatform<Player>
|
||||
|
||||
// Check if we're using protocol support too
|
||||
protocolSupport = Bukkit.getPluginManager().getPlugin("ProtocolSupport") != null;
|
||||
if (protocolSupport) {
|
||||
getLogger().info("Hooking into ProtocolSupport, to prevent issues!");
|
||||
try {
|
||||
BukkitViaInjector.patchLists();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -124,7 +116,7 @@ public class ViaVersionPlugin extends JavaPlugin implements ViaPlatform<Player>
|
||||
|
||||
// Generate classes needed (only works if it's compat or ps)
|
||||
ClassGenerator.generate();
|
||||
lateBind = !BukkitViaInjector.isBinded();
|
||||
lateBind = !((BukkitViaInjector) Via.getManager().getInjector()).isBinded();
|
||||
|
||||
getLogger().info("ViaVersion " + getDescription().getVersion() + (compatSpigotBuild ? "compat" : "") + " is now loaded" + (lateBind ? ", waiting for boot. (late-bind)" : ", injecting!"));
|
||||
if (!lateBind) {
|
||||
|
@ -22,6 +22,7 @@ import com.viaversion.viaversion.bukkit.classgenerator.ClassGenerator;
|
||||
import com.viaversion.viaversion.bukkit.platform.PaperViaInjector;
|
||||
import com.viaversion.viaversion.classgenerator.generated.HandlerConstructor;
|
||||
import com.viaversion.viaversion.connection.UserConnectionImpl;
|
||||
import com.viaversion.viaversion.platform.WrappedChannelInitializer;
|
||||
import com.viaversion.viaversion.protocol.ProtocolPipelineImpl;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
@ -30,21 +31,25 @@ import io.netty.handler.codec.MessageToByteEncoder;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class BukkitChannelInitializer extends ChannelInitializer<Channel> {
|
||||
public class BukkitChannelInitializer extends ChannelInitializer<Channel> implements WrappedChannelInitializer {
|
||||
|
||||
private static final Method INIT_CHANNEL_METHOD;
|
||||
private final ChannelInitializer<Channel> original;
|
||||
private Method method;
|
||||
|
||||
public BukkitChannelInitializer(ChannelInitializer<Channel> oldInit) {
|
||||
this.original = oldInit;
|
||||
static {
|
||||
try {
|
||||
this.method = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
|
||||
this.method.setAccessible(true);
|
||||
INIT_CHANNEL_METHOD = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
|
||||
INIT_CHANNEL_METHOD.setAccessible(true);
|
||||
} catch (NoSuchMethodException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public BukkitChannelInitializer(ChannelInitializer<Channel> oldInit) {
|
||||
this.original = oldInit;
|
||||
}
|
||||
|
||||
@Deprecated/*(forRemoval = true)*/
|
||||
public ChannelInitializer<Channel> getOriginal() {
|
||||
return original;
|
||||
}
|
||||
@ -52,7 +57,7 @@ public class BukkitChannelInitializer extends ChannelInitializer<Channel> {
|
||||
@Override
|
||||
protected void initChannel(Channel channel) throws Exception {
|
||||
// Add originals
|
||||
this.method.invoke(this.original, channel);
|
||||
INIT_CHANNEL_METHOD.invoke(this.original, channel);
|
||||
afterChannelInitialize(channel);
|
||||
}
|
||||
|
||||
@ -72,4 +77,9 @@ public class BukkitChannelInitializer extends ChannelInitializer<Channel> {
|
||||
channel.pipeline().replace("encoder", "encoder", encoder);
|
||||
channel.pipeline().replace("decoder", "decoder", decoder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelInitializer<Channel> original() {
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
@ -17,15 +17,10 @@
|
||||
*/
|
||||
package com.viaversion.viaversion.bukkit.platform;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.viaversion.viaversion.api.Via;
|
||||
import com.viaversion.viaversion.api.platform.ViaInjector;
|
||||
import com.viaversion.viaversion.bukkit.handlers.BukkitChannelInitializer;
|
||||
import com.viaversion.viaversion.bukkit.util.NMSUtil;
|
||||
import com.viaversion.viaversion.util.ConcurrentList;
|
||||
import com.viaversion.viaversion.util.ListWrapper;
|
||||
import com.viaversion.viaversion.util.Pair;
|
||||
import com.viaversion.viaversion.platform.LegacyViaInjector;
|
||||
import com.viaversion.viaversion.platform.WrappedChannelInitializer;
|
||||
import com.viaversion.viaversion.util.ReflectionUtil;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
@ -33,230 +28,88 @@ import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.PluginDescriptionFile;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
//TODO screams
|
||||
public class BukkitViaInjector implements ViaInjector {
|
||||
private final List<ChannelFuture> injectedFutures = new ArrayList<>();
|
||||
private final List<Pair<Field, Object>> injectedLists = new ArrayList<>();
|
||||
|
||||
public class BukkitViaInjector extends LegacyViaInjector {
|
||||
private boolean protocolLib;
|
||||
|
||||
@Override
|
||||
public void inject() throws Exception {
|
||||
public void inject() throws ReflectiveOperationException {
|
||||
if (PaperViaInjector.PAPER_INJECTION_METHOD) {
|
||||
PaperViaInjector.setPaperChannelInitializeListener();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Object connection = getServerConnection();
|
||||
if (connection == null) {
|
||||
throw new Exception("We failed to find the core component 'ServerConnection', please file an issue on our GitHub.");
|
||||
}
|
||||
for (Field field : connection.getClass().getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(connection);
|
||||
if (value instanceof List) {
|
||||
// Inject the list
|
||||
List wrapper = new ListWrapper((List) value) {
|
||||
@Override
|
||||
public void handleAdd(Object o) {
|
||||
if (o instanceof ChannelFuture) {
|
||||
try {
|
||||
injectChannelFuture((ChannelFuture) o);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
injectedLists.add(new Pair<>(field, connection));
|
||||
field.set(connection, wrapper);
|
||||
// Iterate through current list
|
||||
synchronized (wrapper) {
|
||||
for (Object o : (List) value) {
|
||||
if (o instanceof ChannelFuture) {
|
||||
injectChannelFuture((ChannelFuture) o);
|
||||
} else {
|
||||
break; // not the right list.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Via.getPlatform().getLogger().severe("Unable to inject ViaVersion, please post these details on our GitHub and ensure you're using a compatible server version.");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void injectChannelFuture(ChannelFuture future) throws Exception {
|
||||
try {
|
||||
List<String> names = future.channel().pipeline().names();
|
||||
ChannelHandler bootstrapAcceptor = null;
|
||||
// Pick best
|
||||
for (String name : names) {
|
||||
ChannelHandler handler = future.channel().pipeline().get(name);
|
||||
try {
|
||||
ReflectionUtil.get(handler, "childHandler", ChannelInitializer.class);
|
||||
bootstrapAcceptor = handler;
|
||||
} catch (Exception e) {
|
||||
// Not this one
|
||||
}
|
||||
}
|
||||
// Default to first (Also allows blame to work)
|
||||
if (bootstrapAcceptor == null) {
|
||||
bootstrapAcceptor = future.channel().pipeline().first();
|
||||
}
|
||||
try {
|
||||
ChannelInitializer<Channel> oldInit = ReflectionUtil.get(bootstrapAcceptor, "childHandler", ChannelInitializer.class);
|
||||
ChannelInitializer newInit = new BukkitChannelInitializer(oldInit);
|
||||
|
||||
ReflectionUtil.set(bootstrapAcceptor, "childHandler", newInit);
|
||||
injectedFutures.add(future);
|
||||
} catch (NoSuchFieldException e) {
|
||||
// let's find who to blame!
|
||||
ClassLoader cl = bootstrapAcceptor.getClass().getClassLoader();
|
||||
if (cl.getClass().getName().equals("org.bukkit.plugin.java.PluginClassLoader")) {
|
||||
PluginDescriptionFile yaml = ReflectionUtil.get(cl, "description", PluginDescriptionFile.class);
|
||||
throw new Exception("Unable to inject, due to " + bootstrapAcceptor.getClass().getName() + ", try without the plugin " + yaml.getName() + "?");
|
||||
} else {
|
||||
throw new Exception("Unable to find core component 'childHandler', please check your plugins. issue: " + bootstrapAcceptor.getClass().getName());
|
||||
}
|
||||
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Via.getPlatform().getLogger().severe("We failed to inject ViaVersion, have you got late-bind enabled with something else?");
|
||||
throw e;
|
||||
}
|
||||
super.inject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninject() throws Exception {
|
||||
// TODO: Uninject from players currently online to prevent protocol lib issues.
|
||||
public void uninject() throws ReflectiveOperationException {
|
||||
if (PaperViaInjector.PAPER_INJECTION_METHOD) {
|
||||
PaperViaInjector.removePaperChannelInitializeListener();
|
||||
return;
|
||||
}
|
||||
|
||||
for (ChannelFuture future : injectedFutures) {
|
||||
List<String> names = future.channel().pipeline().names();
|
||||
ChannelHandler bootstrapAcceptor = null;
|
||||
// Pick best
|
||||
for (String name : names) {
|
||||
ChannelHandler handler = future.channel().pipeline().get(name);
|
||||
try {
|
||||
ChannelInitializer<Channel> oldInit = ReflectionUtil.get(handler, "childHandler", ChannelInitializer.class);
|
||||
if (oldInit instanceof BukkitChannelInitializer) {
|
||||
bootstrapAcceptor = handler;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Not this one
|
||||
}
|
||||
}
|
||||
// Default to first
|
||||
if (bootstrapAcceptor == null) {
|
||||
bootstrapAcceptor = future.channel().pipeline().first();
|
||||
}
|
||||
|
||||
try {
|
||||
ChannelInitializer<Channel> oldInit = ReflectionUtil.get(bootstrapAcceptor, "childHandler", ChannelInitializer.class);
|
||||
if (oldInit instanceof BukkitChannelInitializer) {
|
||||
ReflectionUtil.set(bootstrapAcceptor, "childHandler", ((BukkitChannelInitializer) oldInit).getOriginal());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Via.getPlatform().getLogger().severe("Failed to remove injection handler, reload won't work with connections, please reboot!");
|
||||
}
|
||||
}
|
||||
injectedFutures.clear();
|
||||
|
||||
for (Pair<Field, Object> pair : injectedLists) {
|
||||
try {
|
||||
Object o = pair.key().get(pair.value());
|
||||
if (o instanceof ListWrapper) {
|
||||
pair.key().set(pair.value(), ((ListWrapper) o).getOriginalList());
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
Via.getPlatform().getLogger().severe("Failed to remove injection, reload won't work with connections, please reboot!");
|
||||
}
|
||||
}
|
||||
|
||||
injectedLists.clear();
|
||||
super.uninject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean lateProtocolVersionSetting() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getServerProtocolVersion() throws Exception {
|
||||
public int getServerProtocolVersion() throws ReflectiveOperationException {
|
||||
if (PaperViaInjector.PAPER_PROTOCOL_METHOD) {
|
||||
//noinspection deprecation
|
||||
return Bukkit.getUnsafe().getProtocolVersion();
|
||||
}
|
||||
|
||||
try {
|
||||
// Grab a static instance of the server
|
||||
Class<?> serverClazz = NMSUtil.nms("MinecraftServer", "net.minecraft.server.MinecraftServer");
|
||||
Object server = ReflectionUtil.invokeStatic(serverClazz, "getServer");
|
||||
// Time to go on a journey! The protocol version is hidden inside an int in ServerPing.ServerData
|
||||
// Grab a static instance of the server
|
||||
Class<?> serverClazz = NMSUtil.nms("MinecraftServer", "net.minecraft.server.MinecraftServer");
|
||||
Object server = ReflectionUtil.invokeStatic(serverClazz, "getServer");
|
||||
|
||||
// Grab the ping class and find the field to access it
|
||||
Class<?> pingClazz = NMSUtil.nms(
|
||||
"ServerPing",
|
||||
"net.minecraft.network.protocol.status.ServerPing"
|
||||
);
|
||||
Object ping = null;
|
||||
// Search for ping method
|
||||
for (Field f : serverClazz.getDeclaredFields()) {
|
||||
if (f.getType() != null) {
|
||||
if (f.getType().getSimpleName().equals("ServerPing")) {
|
||||
f.setAccessible(true);
|
||||
ping = f.get(server);
|
||||
}
|
||||
}
|
||||
// Grab the ping class and find the field to access it
|
||||
Class<?> pingClazz = NMSUtil.nms(
|
||||
"ServerPing",
|
||||
"net.minecraft.network.protocol.status.ServerPing"
|
||||
);
|
||||
Object ping = null;
|
||||
for (Field field : serverClazz.getDeclaredFields()) {
|
||||
if (field.getType() == pingClazz) {
|
||||
field.setAccessible(true);
|
||||
ping = field.get(server);
|
||||
break;
|
||||
}
|
||||
if (ping != null) {
|
||||
Object serverData = null;
|
||||
for (Field f : pingClazz.getDeclaredFields()) {
|
||||
if (f.getType() != null) {
|
||||
if (f.getType().getSimpleName().endsWith("ServerData")) {
|
||||
f.setAccessible(true);
|
||||
serverData = f.get(ping);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (serverData != null) {
|
||||
int protocolVersion = -1;
|
||||
for (Field f : serverData.getClass().getDeclaredFields()) {
|
||||
if (f.getType() != null) {
|
||||
if (f.getType() == int.class) {
|
||||
f.setAccessible(true);
|
||||
protocolVersion = (int) f.get(serverData);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (protocolVersion != -1) {
|
||||
return protocolVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new Exception("Failed to get server", e);
|
||||
}
|
||||
throw new Exception("Failed to get server");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEncoderName() {
|
||||
return "encoder";
|
||||
// Now get the ServerData inside ServerPing
|
||||
Class<?> serverDataClass = NMSUtil.nms(
|
||||
"ServerPing$ServerData",
|
||||
"net.minecraft.network.protocol.status.ServerPing$ServerData"
|
||||
);
|
||||
Object serverData = null;
|
||||
for (Field field : pingClazz.getDeclaredFields()) {
|
||||
if (field.getType() == serverDataClass) {
|
||||
field.setAccessible(true);
|
||||
serverData = field.get(ping);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Get protocol version field
|
||||
for (Field field : serverDataClass.getDeclaredFields()) {
|
||||
if (field.getType() != int.class) {
|
||||
continue;
|
||||
}
|
||||
|
||||
field.setAccessible(true);
|
||||
int protocolVersion = (int) field.get(serverData);
|
||||
if (protocolVersion != -1) {
|
||||
return protocolVersion;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Failed to get server");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -264,135 +117,79 @@ public class BukkitViaInjector implements ViaInjector {
|
||||
return protocolLib ? "protocol_lib_decoder" : "decoder";
|
||||
}
|
||||
|
||||
public static Object getServerConnection() throws Exception {
|
||||
Class<?> serverClazz = NMSUtil.nms(
|
||||
@Override
|
||||
protected @Nullable Object getServerConnection() throws ReflectiveOperationException {
|
||||
Class<?> serverClass = NMSUtil.nms(
|
||||
"MinecraftServer",
|
||||
"net.minecraft.server.MinecraftServer"
|
||||
);
|
||||
Object server = ReflectionUtil.invokeStatic(serverClazz, "getServer");
|
||||
Object connection = null;
|
||||
for (Method m : serverClazz.getDeclaredMethods()) {
|
||||
if (m.getReturnType() != null) {
|
||||
if (m.getReturnType().getSimpleName().equals("ServerConnection")) {
|
||||
if (m.getParameterTypes().length == 0) {
|
||||
connection = m.invoke(server);
|
||||
}
|
||||
}
|
||||
Class<?> connectionClass = NMSUtil.nms(
|
||||
"ServerConnection",
|
||||
"net.minecraft.server.network.ServerConnection"
|
||||
);
|
||||
|
||||
Object server = ReflectionUtil.invokeStatic(serverClass, "getServer");
|
||||
for (Method method : serverClass.getDeclaredMethods()) {
|
||||
if (method.getReturnType() != connectionClass || method.getParameterTypes().length != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We need the method that initiates the connection if not yet set
|
||||
Object connection = method.invoke(server);
|
||||
if (connection != null) {
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
return connection;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isBinded() {
|
||||
if (PaperViaInjector.PAPER_INJECTION_METHOD) return true;
|
||||
@Override
|
||||
protected WrappedChannelInitializer createChannelInitializer(ChannelInitializer<Channel> oldInitializer) {
|
||||
return new BukkitChannelInitializer(oldInitializer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void blame(ChannelHandler bootstrapAcceptor) throws ReflectiveOperationException {
|
||||
// Let's find who to blame!
|
||||
ClassLoader classLoader = bootstrapAcceptor.getClass().getClassLoader();
|
||||
if (classLoader.getClass().getName().equals("org.bukkit.plugin.java.PluginClassLoader")) {
|
||||
PluginDescriptionFile description = ReflectionUtil.get(classLoader, "description", PluginDescriptionFile.class);
|
||||
throw new RuntimeException("Unable to inject, due to " + bootstrapAcceptor.getClass().getName() + ", try without the plugin " + description.getName() + "?");
|
||||
} else {
|
||||
throw new RuntimeException("Unable to find core component 'childHandler', please check your plugins. issue: " + bootstrapAcceptor.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isBinded() {
|
||||
if (PaperViaInjector.PAPER_INJECTION_METHOD) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Object connection = getServerConnection();
|
||||
if (connection == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Field field : connection.getClass().getDeclaredFields()) {
|
||||
if (!List.class.isAssignableFrom(field.getType())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
field.setAccessible(true);
|
||||
final Object value = field.get(connection);
|
||||
if (value instanceof List) {
|
||||
// Inject the list
|
||||
synchronized (value) {
|
||||
for (Object o : (List) value) {
|
||||
if (o instanceof ChannelFuture) {
|
||||
return true;
|
||||
} else {
|
||||
break; // not the right list.
|
||||
}
|
||||
}
|
||||
List<?> value = (List<?>) field.get(connection);
|
||||
// Check if the list has at least one element
|
||||
synchronized (value) {
|
||||
if (!value.isEmpty() && value.get(0) instanceof ChannelFuture) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObject getDump() {
|
||||
JsonObject data = new JsonObject();
|
||||
|
||||
// Generate information about current injections
|
||||
JsonArray injectedChannelInitializers = new JsonArray();
|
||||
for (ChannelFuture cf : injectedFutures) {
|
||||
JsonObject info = new JsonObject();
|
||||
info.addProperty("futureClass", cf.getClass().getName());
|
||||
info.addProperty("channelClass", cf.channel().getClass().getName());
|
||||
|
||||
// Get information about the pipes for this channel future
|
||||
JsonArray pipeline = new JsonArray();
|
||||
for (String pipeName : cf.channel().pipeline().names()) {
|
||||
JsonObject pipe = new JsonObject();
|
||||
pipe.addProperty("name", pipeName);
|
||||
if (cf.channel().pipeline().get(pipeName) != null) {
|
||||
pipe.addProperty("class", cf.channel().pipeline().get(pipeName).getClass().getName());
|
||||
try {
|
||||
Object child = ReflectionUtil.get(cf.channel().pipeline().get(pipeName), "childHandler", ChannelInitializer.class);
|
||||
pipe.addProperty("childClass", child.getClass().getName());
|
||||
if (child instanceof BukkitChannelInitializer) {
|
||||
pipe.addProperty("oldInit", ((BukkitChannelInitializer) child).getOriginal().getClass().getName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Don't display
|
||||
}
|
||||
}
|
||||
// Add to the pipeline array
|
||||
pipeline.add(pipe);
|
||||
}
|
||||
info.add("pipeline", pipeline);
|
||||
|
||||
// Add to the list
|
||||
injectedChannelInitializers.add(info);
|
||||
}
|
||||
data.add("injectedChannelInitializers", injectedChannelInitializers);
|
||||
|
||||
// Generate information about lists we've injected into
|
||||
JsonObject wrappedLists = new JsonObject();
|
||||
JsonObject currentLists = new JsonObject();
|
||||
try {
|
||||
for (Pair<Field, Object> pair : injectedLists) {
|
||||
Object list = pair.key().get(pair.value());
|
||||
// Note down the current value (could be overridden by another plugin)
|
||||
currentLists.addProperty(pair.key().getName(), list.getClass().getName());
|
||||
// Also if it's not overridden we can display what's inside our list (possibly another plugin)
|
||||
if (list instanceof ListWrapper) {
|
||||
wrappedLists.addProperty(pair.key().getName(), ((ListWrapper) list).getOriginalList().getClass().getName());
|
||||
}
|
||||
}
|
||||
data.add("wrappedLists", wrappedLists);
|
||||
data.add("currentLists", currentLists);
|
||||
} catch (Exception e) {
|
||||
// Ignored, fields won't be present
|
||||
}
|
||||
|
||||
data.addProperty("binded", isBinded());
|
||||
return data;
|
||||
}
|
||||
|
||||
public static void patchLists() throws Exception {
|
||||
if (PaperViaInjector.PAPER_INJECTION_METHOD) return;
|
||||
|
||||
Object connection = getServerConnection();
|
||||
if (connection == null) {
|
||||
Via.getPlatform().getLogger().warning("We failed to find the core component 'ServerConnection', please file an issue on our GitHub.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (Field field : connection.getClass().getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(connection);
|
||||
if (!(value instanceof List)) continue;
|
||||
if (value instanceof ConcurrentList) continue;
|
||||
|
||||
ConcurrentList list = new ConcurrentList();
|
||||
list.addAll((Collection) value);
|
||||
field.set(connection, list);
|
||||
}
|
||||
}
|
||||
|
||||
public void setProtocolLib(boolean protocolLib) {
|
||||
this.protocolLib = protocolLib;
|
||||
}
|
||||
|
@ -80,16 +80,6 @@ public class BungeeViaInjector implements ViaInjector {
|
||||
return ReflectionUtil.getStatic(Class.forName("net.md_5.bungee.protocol.ProtocolConstants"), "SUPPORTED_VERSION_IDS", List.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEncoderName() {
|
||||
return "via-encoder";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDecoderName() {
|
||||
return "via-decoder";
|
||||
}
|
||||
|
||||
private ChannelInitializer<Channel> getChannelInitializer() throws Exception {
|
||||
Class<?> pipelineUtils = Class.forName("net.md_5.bungee.netty.PipelineUtils");
|
||||
Field field = pipelineUtils.getDeclaredField("SERVER_CHILD");
|
||||
|
@ -0,0 +1,246 @@
|
||||
/*
|
||||
* This file is part of ViaVersion - https://github.com/ViaVersion/ViaVersion
|
||||
* Copyright (C) 2016-2021 ViaVersion and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.viaversion.viaversion.platform;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.viaversion.viaversion.api.Via;
|
||||
import com.viaversion.viaversion.api.platform.ViaInjector;
|
||||
import com.viaversion.viaversion.util.Pair;
|
||||
import com.viaversion.viaversion.util.ReflectionUtil;
|
||||
import com.viaversion.viaversion.util.SynchronizedListWrapper;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class LegacyViaInjector implements ViaInjector {
|
||||
protected final List<ChannelFuture> injectedFutures = new ArrayList<>();
|
||||
protected final List<Pair<Field, Object>> injectedLists = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void inject() throws ReflectiveOperationException {
|
||||
Object connection = getServerConnection();
|
||||
if (connection == null) {
|
||||
throw new RuntimeException("Failed to find the core component 'ServerConnection'");
|
||||
}
|
||||
|
||||
// Inject into channels list
|
||||
for (Field field : connection.getClass().getDeclaredFields()) {
|
||||
// Check for list with the correct generic type
|
||||
if (!List.class.isAssignableFrom(field.getType()) || !field.getGenericType().getTypeName().contains(ChannelFuture.class.getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
field.setAccessible(true);
|
||||
List<ChannelFuture> list = (List<ChannelFuture>) field.get(connection);
|
||||
List<ChannelFuture> wrappedList = new SynchronizedListWrapper(list, o -> {
|
||||
// Inject newly added entries
|
||||
try {
|
||||
injectChannelFuture((ChannelFuture) o);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Synchronize over original list before setting the field
|
||||
synchronized (list) {
|
||||
// Iterate through current list
|
||||
for (ChannelFuture future : list) {
|
||||
injectChannelFuture(future);
|
||||
}
|
||||
|
||||
field.set(connection, wrappedList);
|
||||
}
|
||||
|
||||
injectedLists.add(new Pair<>(field, connection));
|
||||
}
|
||||
}
|
||||
|
||||
private void injectChannelFuture(ChannelFuture future) throws ReflectiveOperationException {
|
||||
List<String> names = future.channel().pipeline().names();
|
||||
ChannelHandler bootstrapAcceptor = null;
|
||||
// Find the right channelhandler
|
||||
for (String name : names) {
|
||||
ChannelHandler handler = future.channel().pipeline().get(name);
|
||||
try {
|
||||
ReflectionUtil.get(handler, "childHandler", ChannelInitializer.class);
|
||||
bootstrapAcceptor = handler;
|
||||
break;
|
||||
} catch (ReflectiveOperationException ignored) {
|
||||
// Not this one
|
||||
}
|
||||
}
|
||||
|
||||
if (bootstrapAcceptor == null) {
|
||||
// Default to first (also allows blame to work)
|
||||
bootstrapAcceptor = future.channel().pipeline().first();
|
||||
}
|
||||
|
||||
try {
|
||||
ChannelInitializer<Channel> oldInitializer = ReflectionUtil.get(bootstrapAcceptor, "childHandler", ChannelInitializer.class);
|
||||
ReflectionUtil.set(bootstrapAcceptor, "childHandler", createChannelInitializer(oldInitializer));
|
||||
injectedFutures.add(future);
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
blame(bootstrapAcceptor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninject() throws ReflectiveOperationException {
|
||||
//TODO uninject connections
|
||||
for (ChannelFuture future : injectedFutures) {
|
||||
List<String> names = future.channel().pipeline().names();
|
||||
ChannelHandler bootstrapAcceptor = null;
|
||||
// Pick best
|
||||
for (String name : names) {
|
||||
ChannelHandler handler = future.channel().pipeline().get(name);
|
||||
try {
|
||||
if (ReflectionUtil.get(handler, "childHandler", ChannelInitializer.class) instanceof WrappedChannelInitializer) {
|
||||
bootstrapAcceptor = handler;
|
||||
break;
|
||||
}
|
||||
} catch (ReflectiveOperationException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (bootstrapAcceptor == null) {
|
||||
// Default to first
|
||||
bootstrapAcceptor = future.channel().pipeline().first();
|
||||
}
|
||||
|
||||
try {
|
||||
ChannelInitializer<Channel> initializer = ReflectionUtil.get(bootstrapAcceptor, "childHandler", ChannelInitializer.class);
|
||||
if (initializer instanceof WrappedChannelInitializer) {
|
||||
ReflectionUtil.set(bootstrapAcceptor, "childHandler", ((WrappedChannelInitializer) initializer).original());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Via.getPlatform().getLogger().severe("Failed to remove injection handler, reload won't work with connections, please reboot!");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
injectedFutures.clear();
|
||||
|
||||
for (Pair<Field, Object> pair : injectedLists) {
|
||||
try {
|
||||
Field field = pair.key();
|
||||
Object o = field.get(pair.value());
|
||||
if (o instanceof SynchronizedListWrapper) {
|
||||
List<ChannelFuture> originalList = ((SynchronizedListWrapper) o).originalList();
|
||||
synchronized (originalList) {
|
||||
field.set(pair.value(), originalList);
|
||||
}
|
||||
}
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Via.getPlatform().getLogger().severe("Failed to remove injection, reload won't work with connections, please reboot!");
|
||||
}
|
||||
}
|
||||
|
||||
injectedLists.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean lateProtocolVersionSetting() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObject getDump() {
|
||||
JsonObject data = new JsonObject();
|
||||
|
||||
// Generate information about current injections
|
||||
JsonArray injectedChannelInitializers = new JsonArray();
|
||||
data.add("injectedChannelInitializers", injectedChannelInitializers);
|
||||
for (ChannelFuture future : injectedFutures) {
|
||||
JsonObject futureInfo = new JsonObject();
|
||||
injectedChannelInitializers.add(futureInfo);
|
||||
|
||||
futureInfo.addProperty("futureClass", future.getClass().getName());
|
||||
futureInfo.addProperty("channelClass", future.channel().getClass().getName());
|
||||
|
||||
// Get information about the pipes for this channel future
|
||||
JsonArray pipeline = new JsonArray();
|
||||
futureInfo.add("pipeline", pipeline);
|
||||
for (String pipeName : future.channel().pipeline().names()) {
|
||||
JsonObject handlerInfo = new JsonObject();
|
||||
pipeline.add(handlerInfo);
|
||||
|
||||
handlerInfo.addProperty("name", pipeName);
|
||||
ChannelHandler channelHandler = future.channel().pipeline().get(pipeName);
|
||||
if (channelHandler == null) {
|
||||
handlerInfo.addProperty("status", "INVALID");
|
||||
continue;
|
||||
}
|
||||
|
||||
handlerInfo.addProperty("class", channelHandler.getClass().getName());
|
||||
try {
|
||||
Object child = ReflectionUtil.get(channelHandler, "childHandler", ChannelInitializer.class);
|
||||
handlerInfo.addProperty("childClass", child.getClass().getName());
|
||||
if (child instanceof WrappedChannelInitializer) {
|
||||
handlerInfo.addProperty("oldInit", ((WrappedChannelInitializer) child).original().getClass().getName());
|
||||
}
|
||||
} catch (ReflectiveOperationException ignored) {
|
||||
// Don't display
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate information about lists we've injected into
|
||||
JsonObject wrappedLists = new JsonObject();
|
||||
JsonObject currentLists = new JsonObject();
|
||||
try {
|
||||
for (Pair<Field, Object> pair : injectedLists) {
|
||||
Field field = pair.key();
|
||||
Object list = field.get(pair.value());
|
||||
// Note down the current value (could be overridden by another plugin)
|
||||
currentLists.addProperty(field.getName(), list.getClass().getName());
|
||||
// Also, if it's not overridden we can display what's inside our list (possibly another plugin)
|
||||
if (list instanceof SynchronizedListWrapper) {
|
||||
wrappedLists.addProperty(field.getName(), ((SynchronizedListWrapper) list).originalList().getClass().getName());
|
||||
}
|
||||
}
|
||||
data.add("wrappedLists", wrappedLists);
|
||||
data.add("currentLists", currentLists);
|
||||
} catch (ReflectiveOperationException ignored) {
|
||||
// Ignored, fields won't be present
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEncoderName() {
|
||||
return "encoder";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDecoderName() {
|
||||
return "decoder";
|
||||
}
|
||||
|
||||
protected abstract @Nullable Object getServerConnection() throws ReflectiveOperationException;
|
||||
|
||||
protected abstract WrappedChannelInitializer createChannelInitializer(ChannelInitializer<Channel> oldInitializer);
|
||||
|
||||
protected abstract void blame(ChannelHandler bootstrapAcceptor) throws ReflectiveOperationException;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* This file is part of ViaVersion - https://github.com/ViaVersion/ViaVersion
|
||||
* Copyright (C) 2016-2021 ViaVersion and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.viaversion.viaversion.platform;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
|
||||
public interface WrappedChannelInitializer {
|
||||
|
||||
ChannelInitializer<Channel> original();
|
||||
}
|
@ -1,283 +0,0 @@
|
||||
/*
|
||||
* This file is part of ViaVersion - https://github.com/ViaVersion/ViaVersion
|
||||
* Copyright (C) 2016-2021 ViaVersion and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.viaversion.viaversion.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Created by wea_ondara licensed under MIT
|
||||
* Same license as in LICENSE
|
||||
* <p>
|
||||
* Taken from:
|
||||
* https://github.com/weaondara/BungeePerms/blob/master/src/main/java/net/alpenblock/bungeeperms/util/ConcurrentList.java
|
||||
*
|
||||
* @param <E> List Type
|
||||
* @deprecated get rid of this at some point
|
||||
*/
|
||||
@Deprecated/*(forRemoval = true)*/
|
||||
public class ConcurrentList<E> extends ArrayList<E> {
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
@Override
|
||||
public boolean add(E e) {
|
||||
synchronized (lock) {
|
||||
return super.add(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(int index, E element) {
|
||||
synchronized (lock) {
|
||||
super.add(index, element);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(Collection<? extends E> c) {
|
||||
synchronized (lock) {
|
||||
return super.addAll(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(int index, Collection<? extends E> c) {
|
||||
synchronized (lock) {
|
||||
return super.addAll(index, c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
synchronized (lock) {
|
||||
super.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone() {
|
||||
synchronized (lock) {
|
||||
return super.clone();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o) {
|
||||
synchronized (lock) {
|
||||
return super.contains(o);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensureCapacity(int minCapacity) {
|
||||
synchronized (lock) {
|
||||
super.ensureCapacity(minCapacity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public E get(int index) {
|
||||
synchronized (lock) {
|
||||
return super.get(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOf(Object o) {
|
||||
synchronized (lock) {
|
||||
return super.indexOf(o);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int lastIndexOf(Object o) {
|
||||
synchronized (lock) {
|
||||
return super.lastIndexOf(o);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public E remove(int index) {
|
||||
synchronized (lock) {
|
||||
return super.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object o) {
|
||||
synchronized (lock) {
|
||||
return super.remove(o);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeAll(Collection<?> c) {
|
||||
synchronized (lock) {
|
||||
return super.removeAll(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retainAll(Collection<?> c) {
|
||||
synchronized (lock) {
|
||||
return super.retainAll(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public E set(int index, E element) {
|
||||
synchronized (lock) {
|
||||
return super.set(index, element);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<E> subList(int fromIndex, int toIndex) {
|
||||
synchronized (lock) {
|
||||
return super.subList(fromIndex, toIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] toArray() {
|
||||
synchronized (lock) {
|
||||
return super.toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T[] toArray(T[] a) {
|
||||
synchronized (lock) {
|
||||
return super.toArray(a);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trimToSize() {
|
||||
synchronized (lock) {
|
||||
super.trimToSize();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListIterator<E> listIterator() {
|
||||
return new ListItr(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<E> iterator() {
|
||||
return new Itr();
|
||||
}
|
||||
|
||||
private class Itr implements Iterator<E> {
|
||||
|
||||
protected int cursor;
|
||||
protected int lastRet;
|
||||
final ConcurrentList l;
|
||||
|
||||
public Itr() {
|
||||
cursor = 0;
|
||||
lastRet = -1;
|
||||
l = (ConcurrentList) ConcurrentList.this.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return cursor < l.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public E next() {
|
||||
int i = cursor;
|
||||
if (i >= l.size()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
cursor = i + 1;
|
||||
return (E) l.get(lastRet = i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
if (lastRet < 0) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
l.remove(lastRet);
|
||||
ConcurrentList.this.remove(lastRet);
|
||||
cursor = lastRet;
|
||||
lastRet = -1;
|
||||
}
|
||||
}
|
||||
|
||||
public class ListItr extends Itr implements ListIterator<E> {
|
||||
|
||||
ListItr(int index) {
|
||||
super();
|
||||
cursor = index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPrevious() {
|
||||
return cursor > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int nextIndex() {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int previousIndex() {
|
||||
return cursor - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public E previous() {
|
||||
int i = cursor - 1;
|
||||
if (i < 0) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
cursor = i;
|
||||
return (E) l.get(lastRet = i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(E e) {
|
||||
if (lastRet < 0) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
l.set(lastRet, e);
|
||||
ConcurrentList.this.set(lastRet, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(E e) {
|
||||
int i = cursor;
|
||||
l.add(i, e);
|
||||
ConcurrentList.this.add(i, e);
|
||||
cursor = i + 1;
|
||||
lastRet = -1;
|
||||
}
|
||||
}
|
||||
}
|
@ -17,28 +17,40 @@
|
||||
*/
|
||||
package com.viaversion.viaversion.util;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* @deprecated scary
|
||||
* Synchronized list wrapper with the addition of an add handler called when an element is added to the list.
|
||||
*
|
||||
* @param <E> list type
|
||||
*/
|
||||
@Deprecated/*(forRemoval = true)*/
|
||||
public abstract class ListWrapper implements List {
|
||||
private final List list;
|
||||
public final class SynchronizedListWrapper<E> implements List<E> {
|
||||
private final List<E> list;
|
||||
private final Consumer<E> addHandler;
|
||||
|
||||
public ListWrapper(List inputList) {
|
||||
public SynchronizedListWrapper(final List<E> inputList, final Consumer<E> addHandler) {
|
||||
this.list = inputList;
|
||||
this.addHandler = addHandler;
|
||||
}
|
||||
|
||||
public abstract void handleAdd(Object o);
|
||||
|
||||
public List getOriginalList() {
|
||||
public List<E> originalList() {
|
||||
return list;
|
||||
}
|
||||
|
||||
private void handleAdd(E o) {
|
||||
synchronized (this) {
|
||||
addHandler.accept(o);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
synchronized (this) {
|
||||
@ -53,59 +65,57 @@ public abstract class ListWrapper implements List {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o) {
|
||||
public boolean contains(final Object o) {
|
||||
synchronized (this) {
|
||||
return this.list.contains(o);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator iterator() {
|
||||
synchronized (this) {
|
||||
return listIterator();
|
||||
}
|
||||
public @NonNull Iterator<E> iterator() {
|
||||
// Has to be manually synched
|
||||
return listIterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] toArray() {
|
||||
public Object @NonNull [] toArray() {
|
||||
synchronized (this) {
|
||||
return this.list.toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(Object o) {
|
||||
handleAdd(o);
|
||||
public boolean add(final E o) {
|
||||
synchronized (this) {
|
||||
handleAdd(o);
|
||||
return this.list.add(o);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object o) {
|
||||
public boolean remove(final Object o) {
|
||||
synchronized (this) {
|
||||
return this.list.remove(o);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(Collection c) {
|
||||
for (Object o : c) {
|
||||
handleAdd(o);
|
||||
}
|
||||
public boolean addAll(final Collection<? extends E> c) {
|
||||
synchronized (this) {
|
||||
for (final E o : c) {
|
||||
handleAdd(o);
|
||||
}
|
||||
return this.list.addAll(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(int index, Collection c) {
|
||||
for (Object o : c) {
|
||||
handleAdd(o);
|
||||
}
|
||||
public boolean addAll(final int index, final Collection<? extends E> c) {
|
||||
synchronized (this) {
|
||||
for (final E o : c) {
|
||||
handleAdd(o);
|
||||
}
|
||||
return this.list.addAll(index, c);
|
||||
}
|
||||
}
|
||||
@ -118,93 +128,135 @@ public abstract class ListWrapper implements List {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(int index) {
|
||||
public E get(final int index) {
|
||||
synchronized (this) {
|
||||
return this.list.get(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object set(int index, Object element) {
|
||||
public E set(final int index, final E element) {
|
||||
synchronized (this) {
|
||||
return this.list.set(index, element);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(int index, Object element) {
|
||||
public void add(final int index, final E element) {
|
||||
synchronized (this) {
|
||||
this.list.add(index, element);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object remove(int index) {
|
||||
public E remove(final int index) {
|
||||
synchronized (this) {
|
||||
return this.list.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOf(Object o) {
|
||||
public int indexOf(final Object o) {
|
||||
synchronized (this) {
|
||||
return this.list.indexOf(o);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int lastIndexOf(Object o) {
|
||||
public int lastIndexOf(final Object o) {
|
||||
synchronized (this) {
|
||||
return this.list.lastIndexOf(o);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListIterator listIterator() {
|
||||
synchronized (this) {
|
||||
return this.list.listIterator();
|
||||
}
|
||||
public @NonNull ListIterator<E> listIterator() {
|
||||
// Has to be manually synched
|
||||
return this.list.listIterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListIterator listIterator(int index) {
|
||||
synchronized (this) {
|
||||
return this.list.listIterator(index);
|
||||
}
|
||||
public @NonNull ListIterator<E> listIterator(final int index) {
|
||||
// Has to be manually synched
|
||||
return this.list.listIterator(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List subList(int fromIndex, int toIndex) {
|
||||
public @NonNull List<E> subList(final int fromIndex, final int toIndex) {
|
||||
// Not perfect
|
||||
synchronized (this) {
|
||||
return this.list.subList(fromIndex, toIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retainAll(Collection c) {
|
||||
public boolean retainAll(@NonNull final Collection<?> c) {
|
||||
synchronized (this) {
|
||||
return this.list.retainAll(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeAll(Collection c) {
|
||||
public boolean removeAll(@NonNull final Collection<?> c) {
|
||||
synchronized (this) {
|
||||
return this.list.removeAll(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAll(Collection c) {
|
||||
public boolean containsAll(@NonNull final Collection<?> c) {
|
||||
synchronized (this) {
|
||||
return this.list.containsAll(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] toArray(Object[] a) {
|
||||
public <T> T @NonNull [] toArray(final T @NonNull [] a) {
|
||||
synchronized (this) {
|
||||
return this.list.toArray(a);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sort(final Comparator<? super E> c) {
|
||||
synchronized (this) {
|
||||
list.sort(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(Consumer<? super E> consumer) {
|
||||
synchronized (this) {
|
||||
list.forEach(consumer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeIf(Predicate<? super E> filter) {
|
||||
synchronized (this) {
|
||||
return list.removeIf(filter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
synchronized (this) {
|
||||
return list.equals(o);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
synchronized (this) {
|
||||
return list.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
synchronized (this) {
|
||||
return list.toString();
|
||||
}
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ package com.viaversion.viaversion.sponge.handlers;
|
||||
import com.viaversion.viaversion.api.Via;
|
||||
import com.viaversion.viaversion.api.connection.UserConnection;
|
||||
import com.viaversion.viaversion.connection.UserConnectionImpl;
|
||||
import com.viaversion.viaversion.platform.WrappedChannelInitializer;
|
||||
import com.viaversion.viaversion.protocol.ProtocolPipelineImpl;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
@ -29,21 +30,23 @@ import io.netty.handler.codec.MessageToByteEncoder;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class SpongeChannelInitializer extends ChannelInitializer<Channel> {
|
||||
public class SpongeChannelInitializer extends ChannelInitializer<Channel> implements WrappedChannelInitializer {
|
||||
|
||||
private static final Method INIT_CHANNEL_METHOD;
|
||||
private final ChannelInitializer<Channel> original;
|
||||
private Method method;
|
||||
|
||||
public SpongeChannelInitializer(ChannelInitializer<Channel> oldInit) {
|
||||
this.original = oldInit;
|
||||
static {
|
||||
try {
|
||||
this.method = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
|
||||
this.method.setAccessible(true);
|
||||
INIT_CHANNEL_METHOD = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
|
||||
INIT_CHANNEL_METHOD.setAccessible(true);
|
||||
} catch (NoSuchMethodException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public SpongeChannelInitializer(ChannelInitializer<Channel> oldInit) {
|
||||
this.original = oldInit;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initChannel(Channel channel) throws Exception {
|
||||
@ -54,7 +57,7 @@ public class SpongeChannelInitializer extends ChannelInitializer<Channel> {
|
||||
// init protocol
|
||||
new ProtocolPipelineImpl(info);
|
||||
// Add originals
|
||||
this.method.invoke(this.original, channel);
|
||||
INIT_CHANNEL_METHOD.invoke(this.original, channel);
|
||||
// Add our transformers
|
||||
MessageToByteEncoder encoder = new SpongeEncodeHandler(info, (MessageToByteEncoder) channel.pipeline().get("encoder"));
|
||||
ByteToMessageDecoder decoder = new SpongeDecodeHandler(info, (ByteToMessageDecoder) channel.pipeline().get("decoder"));
|
||||
@ -62,11 +65,17 @@ public class SpongeChannelInitializer extends ChannelInitializer<Channel> {
|
||||
channel.pipeline().replace("encoder", "encoder", encoder);
|
||||
channel.pipeline().replace("decoder", "decoder", decoder);
|
||||
} else {
|
||||
this.method.invoke(this.original, channel);
|
||||
INIT_CHANNEL_METHOD.invoke(this.original, channel);
|
||||
}
|
||||
}
|
||||
|
||||
/*@Deprecated(forRemoval = true)*/
|
||||
public ChannelInitializer<Channel> getOriginal() {
|
||||
return original;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelInitializer<Channel> original() {
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
@ -17,260 +17,47 @@
|
||||
*/
|
||||
package com.viaversion.viaversion.sponge.platform;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.viaversion.viaversion.api.Via;
|
||||
import com.viaversion.viaversion.api.platform.ViaInjector;
|
||||
import com.viaversion.viaversion.platform.LegacyViaInjector;
|
||||
import com.viaversion.viaversion.platform.WrappedChannelInitializer;
|
||||
import com.viaversion.viaversion.sponge.handlers.SpongeChannelInitializer;
|
||||
import com.viaversion.viaversion.util.ListWrapper;
|
||||
import com.viaversion.viaversion.util.Pair;
|
||||
import com.viaversion.viaversion.util.ReflectionUtil;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.spongepowered.api.MinecraftVersion;
|
||||
import org.spongepowered.api.Sponge;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
//TODO screams
|
||||
public class SpongeViaInjector implements ViaInjector {
|
||||
private List<ChannelFuture> injectedFutures = new ArrayList<>();
|
||||
private List<Pair<Field, Object>> injectedLists = new ArrayList<>();
|
||||
public class SpongeViaInjector extends LegacyViaInjector {
|
||||
|
||||
@Override
|
||||
public void inject() throws Exception {
|
||||
try {
|
||||
Object connection = getServerConnection();
|
||||
if (connection == null) {
|
||||
throw new Exception("We failed to find the core component 'ServerConnection', please file an issue on our GitHub.");
|
||||
}
|
||||
for (Field field : connection.getClass().getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
final Object value = field.get(connection);
|
||||
if (value instanceof List) {
|
||||
// Inject the list
|
||||
List wrapper = new ListWrapper((List) value) {
|
||||
@Override
|
||||
public void handleAdd(Object o) {
|
||||
if (o instanceof ChannelFuture) {
|
||||
try {
|
||||
injectChannelFuture((ChannelFuture) o);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
injectedLists.add(new Pair<>(field, connection));
|
||||
field.set(connection, wrapper);
|
||||
// Iterate through current list
|
||||
synchronized (wrapper) {
|
||||
for (Object o : (List) value) {
|
||||
if (o instanceof ChannelFuture) {
|
||||
injectChannelFuture((ChannelFuture) o);
|
||||
} else {
|
||||
break; // not the right list.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Via.getPlatform().getLogger().severe("Unable to inject ViaVersion, please post these details on our GitHub and ensure you're using a compatible server version.");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void injectChannelFuture(ChannelFuture future) throws Exception {
|
||||
try {
|
||||
List<String> names = future.channel().pipeline().names();
|
||||
ChannelHandler bootstrapAcceptor = null;
|
||||
// Pick best
|
||||
for (String name : names) {
|
||||
ChannelHandler handler = future.channel().pipeline().get(name);
|
||||
try {
|
||||
ReflectionUtil.get(handler, "childHandler", ChannelInitializer.class);
|
||||
bootstrapAcceptor = handler;
|
||||
} catch (Exception e) {
|
||||
// Not this one
|
||||
}
|
||||
}
|
||||
// Default to first (Also allows blame to work)
|
||||
if (bootstrapAcceptor == null) {
|
||||
bootstrapAcceptor = future.channel().pipeline().first();
|
||||
}
|
||||
try {
|
||||
ChannelInitializer<Channel> oldInit = ReflectionUtil.get(bootstrapAcceptor, "childHandler", ChannelInitializer.class);
|
||||
ChannelInitializer newInit = new SpongeChannelInitializer(oldInit);
|
||||
|
||||
ReflectionUtil.set(bootstrapAcceptor, "childHandler", newInit);
|
||||
injectedFutures.add(future);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new Exception("Unable to find core component 'childHandler', please check your plugins. issue: " + bootstrapAcceptor.getClass().getName());
|
||||
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Via.getPlatform().getLogger().severe("We failed to inject ViaVersion, have you got late-bind enabled with something else?");
|
||||
throw e;
|
||||
}
|
||||
public int getServerProtocolVersion() throws ReflectiveOperationException {
|
||||
MinecraftVersion version = Sponge.getPlatform().getMinecraftVersion();
|
||||
return (int) version.getClass().getDeclaredMethod("getProtocol").invoke(version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean lateProtocolVersionSetting() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninject() {
|
||||
// TODO: Uninject from players currently online
|
||||
for (ChannelFuture future : injectedFutures) {
|
||||
List<String> names = future.channel().pipeline().names();
|
||||
ChannelHandler bootstrapAcceptor = null;
|
||||
// Pick best
|
||||
for (String name : names) {
|
||||
ChannelHandler handler = future.channel().pipeline().get(name);
|
||||
try {
|
||||
ChannelInitializer<Channel> oldInit = ReflectionUtil.get(handler, "childHandler", ChannelInitializer.class);
|
||||
if (oldInit instanceof SpongeChannelInitializer) {
|
||||
bootstrapAcceptor = handler;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Not this one
|
||||
}
|
||||
}
|
||||
// Default to first
|
||||
if (bootstrapAcceptor == null) {
|
||||
bootstrapAcceptor = future.channel().pipeline().first();
|
||||
}
|
||||
|
||||
try {
|
||||
ChannelInitializer<Channel> oldInit = ReflectionUtil.get(bootstrapAcceptor, "childHandler", ChannelInitializer.class);
|
||||
if (oldInit instanceof SpongeChannelInitializer) {
|
||||
ReflectionUtil.set(bootstrapAcceptor, "childHandler", ((SpongeChannelInitializer) oldInit).getOriginal());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Via.getPlatform().getLogger().severe("Failed to remove injection handler, reload won't work with connections, please reboot!");
|
||||
}
|
||||
}
|
||||
injectedFutures.clear();
|
||||
|
||||
for (Pair<Field, Object> pair : injectedLists) {
|
||||
try {
|
||||
Object o = pair.key().get(pair.value());
|
||||
if (o instanceof ListWrapper) {
|
||||
pair.key().set(pair.value(), ((ListWrapper) o).getOriginalList());
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
Via.getPlatform().getLogger().severe("Failed to remove injection, reload won't work with connections, please reboot!");
|
||||
}
|
||||
}
|
||||
|
||||
injectedLists.clear();
|
||||
}
|
||||
|
||||
public static Object getServer() throws Exception {
|
||||
return Sponge.getServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getServerProtocolVersion() throws Exception {
|
||||
MinecraftVersion mcv = Sponge.getPlatform().getMinecraftVersion();
|
||||
try {
|
||||
return (int) mcv.getClass().getDeclaredMethod("getProtocol").invoke(mcv);
|
||||
} catch (Exception e) {
|
||||
throw new Exception("Failed to get server protocol", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEncoderName() {
|
||||
return "encoder";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDecoderName() {
|
||||
return "decoder";
|
||||
}
|
||||
|
||||
public static Object getServerConnection() throws Exception {
|
||||
protected @Nullable Object getServerConnection() throws ReflectiveOperationException {
|
||||
Class<?> serverClazz = Class.forName("net.minecraft.server.MinecraftServer");
|
||||
Object server = getServer();
|
||||
Object connection = null;
|
||||
for (Method m : serverClazz.getDeclaredMethods()) {
|
||||
if (m.getReturnType() != null) {
|
||||
if (m.getReturnType().getSimpleName().equals("NetworkSystem")) {
|
||||
if (m.getParameterTypes().length == 0) {
|
||||
connection = m.invoke(server);
|
||||
}
|
||||
for (Method method : serverClazz.getDeclaredMethods()) {
|
||||
if (method.getReturnType().getSimpleName().equals("NetworkSystem") && method.getParameterTypes().length == 0) {
|
||||
Object connection = method.invoke(Sponge.getServer());
|
||||
if (connection != null) {
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
return connection;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObject getDump() {
|
||||
JsonObject data = new JsonObject();
|
||||
|
||||
// Generate information about current injections
|
||||
JsonArray injectedChannelInitializers = new JsonArray();
|
||||
for (ChannelFuture cf : injectedFutures) {
|
||||
JsonObject info = new JsonObject();
|
||||
info.addProperty("futureClass", cf.getClass().getName());
|
||||
info.addProperty("channelClass", cf.channel().getClass().getName());
|
||||
|
||||
// Get information about the pipes for this channel future
|
||||
JsonArray pipeline = new JsonArray();
|
||||
for (String pipeName : cf.channel().pipeline().names()) {
|
||||
JsonObject pipe = new JsonObject();
|
||||
pipe.addProperty("name", pipeName);
|
||||
if (cf.channel().pipeline().get(pipeName) != null) {
|
||||
pipe.addProperty("class", cf.channel().pipeline().get(pipeName).getClass().getName());
|
||||
try {
|
||||
Object child = ReflectionUtil.get(cf.channel().pipeline().get(pipeName), "childHandler", ChannelInitializer.class);
|
||||
pipe.addProperty("childClass", child.getClass().getName());
|
||||
if (child instanceof SpongeChannelInitializer) {
|
||||
pipe.addProperty("oldInit", ((SpongeChannelInitializer) child).getOriginal().getClass().getName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Don't display
|
||||
}
|
||||
}
|
||||
// Add to the pipeline array
|
||||
pipeline.add(pipe);
|
||||
}
|
||||
info.add("pipeline", pipeline);
|
||||
|
||||
// Add to the list
|
||||
injectedChannelInitializers.add(info);
|
||||
}
|
||||
data.add("injectedChannelInitializers", injectedChannelInitializers);
|
||||
|
||||
// Generate information about lists we've injected into
|
||||
JsonObject wrappedLists = new JsonObject();
|
||||
JsonObject currentLists = new JsonObject();
|
||||
try {
|
||||
for (Pair<Field, Object> pair : injectedLists) {
|
||||
Object list = pair.key().get(pair.value());
|
||||
// Note down the current value (could be overridden by another plugin)
|
||||
currentLists.addProperty(pair.key().getName(), list.getClass().getName());
|
||||
// Also if it's not overridden we can display what's inside our list (possibly another plugin)
|
||||
if (list instanceof ListWrapper) {
|
||||
wrappedLists.addProperty(pair.key().getName(), ((ListWrapper) list).getOriginalList().getClass().getName());
|
||||
}
|
||||
}
|
||||
data.add("wrappedLists", wrappedLists);
|
||||
data.add("currentLists", currentLists);
|
||||
} catch (Exception e) {
|
||||
// Ignored, fields won't be present
|
||||
}
|
||||
|
||||
return data;
|
||||
protected WrappedChannelInitializer createChannelInitializer(ChannelInitializer<Channel> oldInitializer) {
|
||||
return new SpongeChannelInitializer(oldInitializer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void blame(ChannelHandler bootstrapAcceptor) {
|
||||
throw new RuntimeException("Unable to find core component 'childHandler', please check your plugins. Issue: " + bootstrapAcceptor.getClass().getName());
|
||||
}
|
||||
}
|
@ -106,16 +106,6 @@ public class VelocityViaInjector implements ViaInjector {
|
||||
return com.velocitypowered.api.network.ProtocolVersion.MINIMUM_VERSION.getProtocol();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEncoderName() {
|
||||
return "via-encoder";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDecoderName() {
|
||||
return "via-decoder";
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObject getDump() {
|
||||
JsonObject data = new JsonObject();
|
||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren