blob: 2a8cd7d3313442d8be42a70ff73fe21dcb345231 [file] [log] [blame] [raw]
package us.myles.ViaVersion;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import lombok.Getter;
import lombok.NonNull;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.java.JavaPlugin;
import us.myles.ViaVersion.api.Pair;
import us.myles.ViaVersion.api.ViaVersion;
import us.myles.ViaVersion.api.ViaVersionAPI;
import us.myles.ViaVersion.api.boss.BossBar;
import us.myles.ViaVersion.api.boss.BossColor;
import us.myles.ViaVersion.api.boss.BossStyle;
import us.myles.ViaVersion.api.command.ViaVersionCommand;
import us.myles.ViaVersion.api.data.UserConnection;
import us.myles.ViaVersion.api.protocol.ProtocolRegistry;
import us.myles.ViaVersion.api.protocol.ProtocolVersion;
import us.myles.ViaVersion.boss.ViaBossBar;
import us.myles.ViaVersion.classgenerator.ClassGenerator;
import us.myles.ViaVersion.commands.ViaCommandHandler;
import us.myles.ViaVersion.handlers.ViaVersionInitializer;
import us.myles.ViaVersion.protocols.base.ProtocolInfo;
import us.myles.ViaVersion.update.UpdateListener;
import us.myles.ViaVersion.update.UpdateUtil;
import us.myles.ViaVersion.util.ConcurrentList;
import us.myles.ViaVersion.util.ListWrapper;
import us.myles.ViaVersion.util.ProtocolSupportUtil;
import us.myles.ViaVersion.util.ReflectionUtil;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class ViaVersionPlugin extends JavaPlugin implements ViaVersionAPI {
private final Map<UUID, UserConnection> portedPlayers = new ConcurrentHashMap<>();
private List<ChannelFuture> injectedFutures = new ArrayList<>();
private List<Pair<Field, Object>> injectedLists = new ArrayList<>();
private ViaCommandHandler commandHandler;
private boolean debug = false;
private boolean compatSpigotBuild = false;
private boolean spigot = true;
private boolean lateBind = false;
private boolean protocolSupport = false;
@Getter
private ViaConfig conf;
public ViaVersionPlugin() {
// 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 {
patchLists();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void onLoad() {
// Config magic
conf = new ViaConfig(this);
ViaVersion.setInstance(this);
// Handle reloads
if (System.getProperty("ViaVersion") != null) {
if (Bukkit.getPluginManager().getPlugin("ProtocolLib") != null) {
getLogger().severe("ViaVersion is already loaded, we're going to kick all the players... because otherwise we'll crash because of ProtocolLib.");
for (Player player : Bukkit.getOnlinePlayers()) {
player.kickPlayer(ChatColor.translateAlternateColorCodes('&', getConf().getReloadDisconnectMsg()));
}
} else {
getLogger().severe("ViaVersion is already loaded, this should work fine. If you get any console errors, try rebooting.");
}
}
// Spigot detector
try {
Class.forName("org.spigotmc.SpigotConfig");
} catch (ClassNotFoundException e) {
spigot = false;
}
// Check if it's a spigot build with a protocol mod
try {
compatSpigotBuild = ReflectionUtil.nms("PacketEncoder").getDeclaredField("version") != null;
} catch (Exception e) {
compatSpigotBuild = false;
}
// Generate classes needed (only works if it's compat or ps)
ClassGenerator.generate();
lateBind = !isBinded();
getLogger().info("ViaVersion " + getDescription().getVersion() + (compatSpigotBuild ? "compat" : "") + " is now loaded" + (lateBind ? ", waiting for boot. (late-bind)" : ", injecting!"));
if (!lateBind)
injectPacketHandler();
}
@Override
public void onEnable() {
if (lateBind)
injectPacketHandler();
if (conf.isCheckForUpdates())
UpdateUtil.sendUpdateMessage(this);
// Gather version :)
Bukkit.getScheduler().scheduleSyncDelayedTask(this, new Runnable() {
@Override
public void run() {
gatherProtocolVersion();
// Check if there are any pipes to this version
if (ProtocolRegistry.SERVER_PROTOCOL != -1) {
getLogger().info("ViaVersion detected server version: " + ProtocolVersion.getProtocol(ProtocolRegistry.SERVER_PROTOCOL));
if (!ProtocolRegistry.isWorkingPipe()) {
getLogger().warning("ViaVersion does not have any compatible versions for this server version, please read our resource page carefully.");
}
}
ProtocolRegistry.refreshVersions();
}
});
Bukkit.getPluginManager().registerEvents(new UpdateListener(this), this);
getCommand("viaversion").setExecutor(commandHandler = new ViaCommandHandler());
getCommand("viaversion").setTabCompleter(commandHandler);
// Register Protocol Listeners
ProtocolRegistry.registerListeners();
// Warn them if they have anti-xray on and they aren't using spigot
if (conf.isAntiXRay() && !spigot) {
getLogger().info("You have anti-xray on in your config, since you're not using spigot it won't fix xray!");
}
}
@Override
public void onDisable() {
getLogger().info("ViaVersion is disabling, if this is a reload and you experience issues consider rebooting.");
uninject();
}
public void gatherProtocolVersion() {
try {
Class<?> serverClazz = ReflectionUtil.nms("MinecraftServer");
Object server = ReflectionUtil.invokeStatic(serverClazz, "getServer");
Class<?> pingClazz = ReflectionUtil.nms("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);
}
}
}
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) {
ProtocolRegistry.SERVER_PROTOCOL = protocolVersion;
return;
}
}
}
} catch (Exception e) {
e.printStackTrace();
// We couldn't work it out... We'll just use ping and hope for the best...
}
}
public Object getServerConnection() throws Exception {
Class<?> serverClazz = ReflectionUtil.nms("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);
}
}
}
}
return connection;
}
public void injectPacketHandler() {
try {
Object connection = getServerConnection();
if (connection == null) {
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);
final Object value = field.get(connection);
if (value instanceof List) {
// Inject the list
List wrapper = new ListWrapper((List) value) {
@Override
public synchronized void handleAdd(Object o) {
synchronized (this) {
if (o instanceof ChannelFuture) {
inject((ChannelFuture) o);
}
}
}
};
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) {
inject((ChannelFuture) o);
} else {
break; // not the right list.
}
}
}
}
}
System.setProperty("ViaVersion", getDescription().getVersion());
} catch (Exception e) {
getLogger().severe("Unable to inject ViaVersion, please post these details on our GitHub and ensure you're using a compatible server version.");
e.printStackTrace();
}
}
public void patchLists() throws Exception {
Object connection = getServerConnection();
if (connection == null) {
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);
final Object value = field.get(connection);
if (value instanceof List) {
if (!(value instanceof ConcurrentList)) {
ConcurrentList list = new ConcurrentList();
list.addAll((List) value);
field.set(connection, list);
}
}
}
}
public boolean isBinded() {
try {
Object connection = getServerConnection();
if (connection == null) {
return false;
}
for (Field field : connection.getClass().getDeclaredFields()) {
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.
}
}
}
}
}
} catch (Exception e) {
}
return false;
}
private void inject(ChannelFuture future) {
try {
ChannelHandler bootstrapAcceptor = future.channel().pipeline().first();
try {
ChannelInitializer<SocketChannel> oldInit = ReflectionUtil.get(bootstrapAcceptor, "childHandler", ChannelInitializer.class);
ChannelInitializer newInit = new ViaVersionInitializer(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) {
getLogger().severe("We failed to inject ViaVersion, have you got late-bind enabled with something else?");
e.printStackTrace();
}
}
private void uninject() {
// TODO: Uninject from players currently online to prevent protocol lib issues.
for (ChannelFuture future : injectedFutures) {
ChannelHandler bootstrapAcceptor = future.channel().pipeline().first();
try {
ChannelInitializer<SocketChannel> oldInit = ReflectionUtil.get(bootstrapAcceptor, "childHandler", ChannelInitializer.class);
if (oldInit instanceof ViaVersionInitializer) {
ReflectionUtil.set(bootstrapAcceptor, "childHandler", ((ViaVersionInitializer) oldInit).getOriginal());
}
} catch (Exception e) {
System.out.println("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.getKey().get(pair.getValue());
if (o instanceof ListWrapper) {
pair.getKey().set(pair.getValue(), ((ListWrapper) o).getOriginalList());
}
} catch (IllegalAccessException e) {
System.out.println("Failed to remove injection, reload won't work with connections, please reboot!");
}
}
injectedLists.clear();
}
@Override
public boolean isPorted(Player player) {
return isPorted(player.getUniqueId());
}
@Override
public int getPlayerVersion(@NonNull Player player) {
if (!isPorted(player))
return getExternalVersion(player);
return portedPlayers.get(player.getUniqueId()).get(ProtocolInfo.class).getProtocolVersion();
}
@Override
public int getPlayerVersion(@NonNull UUID uuid) {
if (!isPorted(uuid))
return getExternalVersion(Bukkit.getPlayer(uuid));
return portedPlayers.get(uuid).get(ProtocolInfo.class).getProtocolVersion();
}
private int getExternalVersion(Player player) {
if (!isProtocolSupport()) {
return ProtocolRegistry.SERVER_PROTOCOL;
} else {
return ProtocolSupportUtil.getProtocolVersion(player);
}
}
@Override
public boolean isPorted(UUID playerUUID) {
return portedPlayers.containsKey(playerUUID);
}
@Override
public String getVersion() {
return getDescription().getVersion();
}
public UserConnection getConnection(UUID playerUUID) {
return portedPlayers.get(playerUUID);
}
public UserConnection getConnection(Player player) {
return portedPlayers.get(player.getUniqueId());
}
public void sendRawPacket(Player player, ByteBuf packet) throws IllegalArgumentException {
sendRawPacket(player.getUniqueId(), packet);
}
@Override
public void sendRawPacket(UUID uuid, ByteBuf packet) throws IllegalArgumentException {
if (!isPorted(uuid)) throw new IllegalArgumentException("This player is not controlled by ViaVersion!");
UserConnection ci = portedPlayers.get(uuid);
ci.sendRawPacket(packet);
}
@Override
public BossBar createBossBar(String title, BossColor color, BossStyle style) {
return new ViaBossBar(title, 1F, color, style);
}
@Override
public BossBar createBossBar(String title, float health, BossColor color, BossStyle style) {
return new ViaBossBar(title, health, color, style);
}
@Override
public boolean isDebug() {
return this.debug;
}
public void setDebug(boolean value) {
this.debug = value;
}
@Override
public ViaVersionCommand getCommandHandler() {
return commandHandler;
}
@Override
public boolean isCompatSpigotBuild() {
return compatSpigotBuild;
}
@Override
public SortedSet<Integer> getSupportedVersions() {
SortedSet<Integer> outputSet = new TreeSet<>(ProtocolRegistry.getSupportedVersions());
outputSet.removeAll(getConf().getBlockedProtocols());
return outputSet;
}
@Override
public boolean isSpigot() {
return this.spigot;
}
public void addPortedClient(UserConnection info) {
portedPlayers.put(info.get(ProtocolInfo.class).getUuid(), info);
}
public void removePortedClient(UUID clientID) {
portedPlayers.remove(clientID);
}
public void run(final Runnable runnable, boolean wait) {
try {
Future f = Bukkit.getScheduler().callSyncMethod(Bukkit.getPluginManager().getPlugin("ViaVersion"), new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
runnable.run();
return true;
}
});
if (wait) {
f.get(10, TimeUnit.SECONDS);
}
} catch (Exception e) {
System.out.println("Failed to run task: " + e.getClass().getName());
if (ViaVersion.getInstance().isDebug())
e.printStackTrace();
}
}
public boolean isProtocolSupport() {
return protocolSupport;
}
public Map<UUID, UserConnection> getPortedPlayers() {
return portedPlayers;
}
public boolean handlePPS(UserConnection info) {
// Max PPS Checker
if (conf.getMaxPPS() > 0) {
if (info.getPacketsPerSecond() >= conf.getMaxPPS()) {
info.disconnect(conf.getMaxPPSKickMessage().replace("%pps", ((Long) info.getPacketsPerSecond()).intValue() + ""));
return true; // don't send current packet
}
}
// Tracking PPS Checker
if (conf.getMaxWarnings() > 0 && conf.getTrackingPeriod() > 0) {
if (info.getSecondsObserved() > conf.getTrackingPeriod()) {
// Reset
info.setWarnings(0);
info.setSecondsObserved(1);
} else {
info.setSecondsObserved(info.getSecondsObserved() + 1);
if (info.getPacketsPerSecond() >= conf.getWarningPPS()) {
info.setWarnings(info.getWarnings() + 1);
}
if (info.getWarnings() >= conf.getMaxWarnings()) {
info.disconnect(conf.getMaxWarningsKickMessage().replace("%pps", ((Long) info.getPacketsPerSecond()).intValue() + ""));
return true; // don't send current packet
}
}
}
return false;
}
}