Java 16 support, some dodgy code hmm. Has errors with "class loaded by another plugin" which needs to be resolved
This commit is contained in:
		| @@ -67,6 +67,8 @@ public class LibsDisguises extends JavaPlugin { | ||||
|  | ||||
|             instance = this; | ||||
|  | ||||
|             WatcherSanitizer.init(); | ||||
|  | ||||
|             Plugin plugin = Bukkit.getPluginManager().getPlugin("ProtocolLib"); | ||||
|  | ||||
|             if (plugin == null || DisguiseUtilities.isOlderThan(DisguiseUtilities.getProtocolLibRequiredVersion(), plugin.getDescription().getVersion())) { | ||||
| @@ -105,8 +107,6 @@ public class LibsDisguises extends JavaPlugin { | ||||
|             if (!reloaded) { | ||||
|                 commandConfig.load(); | ||||
|             } | ||||
|  | ||||
|             WatcherSanitizer.init(); | ||||
|         } catch (Throwable throwable) { | ||||
|             getUpdateChecker().doUpdate(); | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import com.comphenix.protocol.PacketType; | ||||
| import com.comphenix.protocol.PacketType.Play.Server; | ||||
| import com.comphenix.protocol.ProtocolLibrary; | ||||
| import com.comphenix.protocol.events.PacketContainer; | ||||
| import com.comphenix.protocol.reflect.StructureModifier; | ||||
| import com.comphenix.protocol.wrappers.EnumWrappers.NativeGameMode; | ||||
| import com.comphenix.protocol.wrappers.EnumWrappers.PlayerInfoAction; | ||||
| import com.comphenix.protocol.wrappers.PlayerInfoData; | ||||
| @@ -16,7 +15,10 @@ import me.libraryaddict.disguise.DisguiseAPI; | ||||
| import me.libraryaddict.disguise.DisguiseConfig; | ||||
| import me.libraryaddict.disguise.LibsDisguises; | ||||
| import me.libraryaddict.disguise.disguisetypes.TargetedDisguise.TargetType; | ||||
| import me.libraryaddict.disguise.disguisetypes.watchers.*; | ||||
| import me.libraryaddict.disguise.disguisetypes.watchers.AbstractHorseWatcher; | ||||
| import me.libraryaddict.disguise.disguisetypes.watchers.AgeableWatcher; | ||||
| import me.libraryaddict.disguise.disguisetypes.watchers.BoatWatcher; | ||||
| import me.libraryaddict.disguise.disguisetypes.watchers.ZombieWatcher; | ||||
| import me.libraryaddict.disguise.events.DisguiseEvent; | ||||
| import me.libraryaddict.disguise.events.UndisguiseEvent; | ||||
| import me.libraryaddict.disguise.utilities.DisguiseUtilities; | ||||
| @@ -29,7 +31,6 @@ import me.libraryaddict.disguise.utilities.reflection.ReflectionManager; | ||||
| import me.libraryaddict.disguise.utilities.translations.LibsMsg; | ||||
| import net.md_5.bungee.api.ChatMessageType; | ||||
| import org.bukkit.Bukkit; | ||||
| import org.bukkit.Location; | ||||
| import org.bukkit.NamespacedKey; | ||||
| import org.bukkit.boss.BarColor; | ||||
| import org.bukkit.boss.BarStyle; | ||||
| @@ -38,7 +39,6 @@ import org.bukkit.command.CommandSender; | ||||
| import org.bukkit.entity.*; | ||||
| import org.bukkit.metadata.FixedMetadataValue; | ||||
| import org.bukkit.scheduler.BukkitRunnable; | ||||
| import org.bukkit.util.Vector; | ||||
|  | ||||
| import java.lang.reflect.InvocationTargetException; | ||||
| import java.util.*; | ||||
| @@ -71,7 +71,7 @@ public abstract class Disguise { | ||||
|     /** | ||||
|      * If set, how long before disguise expires | ||||
|      */ | ||||
|     private long disguiseExpires; | ||||
|     protected long disguiseExpires; | ||||
|     /** | ||||
|      * For when plugins may want to assign custom data to a disguise, such as who owns it | ||||
|      */ | ||||
| @@ -370,7 +370,7 @@ public abstract class Disguise { | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     private void doActionBar() { | ||||
|     protected void doActionBar() { | ||||
|         if (getNotifyBar() == DisguiseConfig.NotifyBar.ACTION_BAR && getEntity() instanceof Player && !getEntity().hasPermission("libsdisguises.noactionbar") && | ||||
|                 DisguiseAPI.getDisguise(getEntity()) == Disguise.this) { | ||||
|             ((Player) getEntity()).spigot().sendMessage(ChatMessageType.ACTION_BAR, LibsMsg.ACTION_BAR_MESSAGE.getChat(getDisguiseName())); | ||||
| @@ -394,227 +394,18 @@ public abstract class Disguise { | ||||
|     } | ||||
|  | ||||
|     private void createRunnable() { | ||||
|         if (runnable != null) { | ||||
|         if (runnable != null && !runnable.isCancelled()) { | ||||
|             runnable.cancel(); | ||||
|         } | ||||
|  | ||||
|         final boolean alwaysSendVelocity; | ||||
|  | ||||
|         switch (getType()) { | ||||
|             case EXPERIENCE_ORB: | ||||
|             case WITHER_SKULL: | ||||
|             case FIREWORK: | ||||
|                 alwaysSendVelocity = true; | ||||
|                 break; | ||||
|             default: | ||||
|                 alwaysSendVelocity = false; | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         final Double vectorY; | ||||
|  | ||||
|         switch (getType()) { | ||||
|             case FIREWORK: | ||||
|             case WITHER_SKULL: | ||||
|                 vectorY = 0.000001D; | ||||
|                 break; | ||||
|             case EXPERIENCE_ORB: | ||||
|                 vectorY = 0.0221; | ||||
|                 break; | ||||
|             default: | ||||
|                 vectorY = null; | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         final TargetedDisguise disguise = (TargetedDisguise) this; | ||||
|  | ||||
|         // A scheduler to clean up any unused disguises. | ||||
|         runnable = new BukkitRunnable() { | ||||
|             private int blockX, blockY, blockZ, facing; | ||||
|             private int deadTicks = 0; | ||||
|             private int actionBarTicks = -1; | ||||
|             private long lastRefreshed = System.currentTimeMillis(); | ||||
|  | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 if (!isDisguiseInUse() || getEntity() == null) { | ||||
|                     cancel(); | ||||
|                     runnable = null; | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 if (++actionBarTicks % 15 == 0) { | ||||
|                     actionBarTicks = 0; | ||||
|  | ||||
|                     doActionBar(); | ||||
|                 } | ||||
|  | ||||
|                 // If entity is no longer valid. Remove it. | ||||
|                 if (getEntity() instanceof Player && !((Player) getEntity()).isOnline()) { | ||||
|                     removeDisguise(); | ||||
|                 } else if (disguiseExpires > 0 && | ||||
|                         (DisguiseConfig.isDynamicExpiry() ? disguiseExpires-- == 1 : disguiseExpires < System.currentTimeMillis())) { // If disguise expired | ||||
|                     removeDisguise(); | ||||
|  | ||||
|                     if (getEntity() instanceof Player) { | ||||
|                         LibsMsg.EXPIRED_DISGUISE.send(getEntity()); | ||||
|                     } | ||||
|  | ||||
|                     return; | ||||
|                 } else if (!getEntity().isValid()) { | ||||
|                     // If it has been dead for 30+ ticks | ||||
|                     // This is to ensure that this disguise isn't removed while clients think its the real entity | ||||
|                     // The delay is because if it sends the destroy entity packets straight away, then it means no | ||||
|                     // death animation | ||||
|                     // This is probably still a problem for wither and enderdragon deaths. | ||||
|                     if (deadTicks++ > (getType() == DisguiseType.ENDER_DRAGON ? 200 : 20)) { | ||||
|                         if (isRemoveDisguiseOnDeath()) { | ||||
|                             removeDisguise(); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 deadTicks = 0; | ||||
|  | ||||
|                 // If the disguise type is tnt, we need to resend the entity packet else it will turn invisible | ||||
|                 if (getType() == DisguiseType.FIREWORK || getType() == DisguiseType.EVOKER_FANGS) { | ||||
|                     if (lastRefreshed + ((getType() == DisguiseType.FIREWORK ? 40 : 23) * 50) < System.currentTimeMillis()) { | ||||
|                         lastRefreshed = System.currentTimeMillis(); | ||||
|  | ||||
|                         DisguiseUtilities.refreshTrackers(disguise); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (isModifyBoundingBox()) { | ||||
|                     DisguiseUtilities.doBoundingBox(disguise); | ||||
|                 } | ||||
|  | ||||
|                 if (getType() == DisguiseType.BAT && !((BatWatcher) getWatcher()).isHanging()) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 doVelocity(vectorY, alwaysSendVelocity); | ||||
|  | ||||
|                 if (getType() == DisguiseType.EXPERIENCE_ORB) { | ||||
|                     PacketContainer packet = new PacketContainer(Server.REL_ENTITY_MOVE); | ||||
|  | ||||
|                     packet.getIntegers().write(0, getEntity().getEntityId()); | ||||
|  | ||||
|                     try { | ||||
|                         for (Player player : DisguiseUtilities.getPerverts(disguise)) { | ||||
|                             if (getEntity() != player) { | ||||
|                                 ProtocolLibrary.getProtocolManager().sendServerPacket(player, packet, false); | ||||
|                                 continue; | ||||
|                             } else if (!isSelfDisguiseVisible() || !(getEntity() instanceof Player)) { | ||||
|                                 continue; | ||||
|                             } | ||||
|  | ||||
|                             PacketContainer selfPacket = packet.shallowClone(); | ||||
|  | ||||
|                             selfPacket.getModifier().write(0, DisguiseAPI.getSelfDisguiseId()); | ||||
|  | ||||
|                             try { | ||||
|                                 ProtocolLibrary.getProtocolManager().sendServerPacket((Player) getEntity(), selfPacket, false); | ||||
|                             } catch (InvocationTargetException e) { | ||||
|                                 e.printStackTrace(); | ||||
|                             } | ||||
|                         } | ||||
|                     } catch (InvocationTargetException e) { | ||||
|                         e.printStackTrace(); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|         runnable = new DisguiseRunnable(this); | ||||
|  | ||||
|         runnable.runTaskTimer(LibsDisguises.getInstance(), 1, 1); | ||||
|     } | ||||
|  | ||||
|     private void doVelocity(Double vectorY, boolean alwaysSendVelocity) { | ||||
|         // If the vectorY isn't 0. Cos if it is. Then it doesn't want to send any vectors. | ||||
|         // If this disguise has velocity sending enabled and the entity is flying. | ||||
|         if (isVelocitySent() && vectorY != null && (alwaysSendVelocity || !getEntity().isOnGround())) { | ||||
|             Vector vector = getEntity().getVelocity(); | ||||
|  | ||||
|             // If the entity doesn't have velocity changes already - You know. I really can't wrap my | ||||
|             // head about the | ||||
|             // if statement. | ||||
|             // But it doesn't seem to do anything wrong.. | ||||
|             if (vector.getY() != 0 && !(vector.getY() < 0 && alwaysSendVelocity && getEntity().isOnGround())) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // If disguise isn't a experience orb, or the entity isn't standing on the ground | ||||
|             if (getType() != DisguiseType.EXPERIENCE_ORB || !getEntity().isOnGround()) { | ||||
|                 PacketContainer lookPacket = null; | ||||
|  | ||||
|                 if (getType() == DisguiseType.WITHER_SKULL && DisguiseConfig.isWitherSkullPacketsEnabled()) { | ||||
|                     lookPacket = new PacketContainer(Server.ENTITY_LOOK); | ||||
|  | ||||
|                     StructureModifier<Object> mods = lookPacket.getModifier(); | ||||
|                     lookPacket.getIntegers().write(0, getEntity().getEntityId()); | ||||
|                     Location loc = getEntity().getLocation(); | ||||
|  | ||||
|                     mods.write(4, DisguiseUtilities.getYaw(getType(), getEntity().getType(), (byte) Math.floor(loc.getYaw() * 256.0F / 360.0F))); | ||||
|                     mods.write(5, DisguiseUtilities.getPitch(getType(), getEntity().getType(), (byte) Math.floor(loc.getPitch() * 256.0F / 360.0F))); | ||||
|  | ||||
|                     if (isSelfDisguiseVisible() && getEntity() instanceof Player) { | ||||
|                         PacketContainer selfLookPacket = lookPacket.shallowClone(); | ||||
|  | ||||
|                         selfLookPacket.getIntegers().write(0, DisguiseAPI.getSelfDisguiseId()); | ||||
|  | ||||
|                         try { | ||||
|                             ProtocolLibrary.getProtocolManager().sendServerPacket((Player) getEntity(), selfLookPacket, false); | ||||
|                         } catch (InvocationTargetException e) { | ||||
|                             e.printStackTrace(); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 try { | ||||
|                     PacketContainer velocityPacket = new PacketContainer(Server.ENTITY_VELOCITY); | ||||
|  | ||||
|                     StructureModifier<Integer> mods = velocityPacket.getIntegers(); | ||||
|  | ||||
|                     // Write entity ID | ||||
|                     mods.write(0, getEntity().getEntityId()); | ||||
|                     mods.write(1, (int) (vector.getX() * 8000)); | ||||
|                     mods.write(3, (int) (vector.getZ() * 8000)); | ||||
|  | ||||
|                     for (Player player : DisguiseUtilities.getPerverts(this)) { | ||||
|                         PacketContainer tempVelocityPacket = velocityPacket.shallowClone(); | ||||
|                         mods = tempVelocityPacket.getIntegers(); | ||||
|  | ||||
|                         // If the viewing player is the disguised player | ||||
|                         if (getEntity() == player) { | ||||
|                             // If not using self disguise, continue | ||||
|                             if (!isSelfDisguiseVisible()) { | ||||
|                                 continue; | ||||
|                             } | ||||
|  | ||||
|                             // Write self disguise ID | ||||
|                             mods.write(0, DisguiseAPI.getSelfDisguiseId()); | ||||
|                         } | ||||
|  | ||||
|                         mods.write(2, (int) (8000D * (vectorY * ReflectionManager.getPing(player)) * 0.069D)); | ||||
|  | ||||
|                         if (lookPacket != null && player != getEntity()) { | ||||
|                             ProtocolLibrary.getProtocolManager().sendServerPacket(player, lookPacket, false); | ||||
|                         } | ||||
|  | ||||
|                         ProtocolLibrary.getProtocolManager().sendServerPacket(player, tempVelocityPacket, false); | ||||
|                     } | ||||
|                 } catch (Exception e) { | ||||
|                     e.printStackTrace(); | ||||
|                 } | ||||
|             } | ||||
|             // If we need to send a packet to update the exp position as it likes to gravitate client | ||||
|             // sided to | ||||
|             // players. | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the disguised entity | ||||
|      * | ||||
| @@ -1189,8 +980,7 @@ public abstract class Disguise { | ||||
|         DisguiseUtilities.refreshTrackers((TargetedDisguise) this); | ||||
|  | ||||
|         // If he is a player, then self disguise himself | ||||
|         Bukkit.getScheduler(). | ||||
|                 scheduleSyncDelayedTask(LibsDisguises.getInstance(), new Runnable() { | ||||
|         Bukkit.getScheduler().scheduleSyncDelayedTask(LibsDisguises.getInstance(), new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 DisguiseUtilities.setupFakeDisguise(Disguise.this); | ||||
|   | ||||
| @@ -0,0 +1,227 @@ | ||||
| package me.libraryaddict.disguise.disguisetypes; | ||||
|  | ||||
| import com.comphenix.protocol.PacketType; | ||||
| import com.comphenix.protocol.ProtocolLibrary; | ||||
| import com.comphenix.protocol.events.PacketContainer; | ||||
| import com.comphenix.protocol.reflect.StructureModifier; | ||||
| import me.libraryaddict.disguise.DisguiseAPI; | ||||
| import me.libraryaddict.disguise.DisguiseConfig; | ||||
| import me.libraryaddict.disguise.disguisetypes.watchers.BatWatcher; | ||||
| import me.libraryaddict.disguise.utilities.DisguiseUtilities; | ||||
| import me.libraryaddict.disguise.utilities.reflection.ReflectionManager; | ||||
| import me.libraryaddict.disguise.utilities.translations.LibsMsg; | ||||
| import org.bukkit.Location; | ||||
| import org.bukkit.entity.Player; | ||||
| import org.bukkit.scheduler.BukkitRunnable; | ||||
| import org.bukkit.util.Vector; | ||||
|  | ||||
| import java.lang.reflect.InvocationTargetException; | ||||
|  | ||||
| /** | ||||
|  * Created by libraryaddict on 20/05/2021. | ||||
|  */ | ||||
| class DisguiseRunnable extends BukkitRunnable { | ||||
|     private int blockX, blockY, blockZ, facing; | ||||
|     private int deadTicks = 0; | ||||
|     private int actionBarTicks = -1; | ||||
|     private long lastRefreshed = System.currentTimeMillis(); | ||||
|     private Disguise disguise; | ||||
|     final Double vectorY; | ||||
|     final boolean alwaysSendVelocity; | ||||
|  | ||||
|     public DisguiseRunnable(Disguise disguise) { | ||||
|         switch (disguise.getType()) { | ||||
|             case FIREWORK: | ||||
|             case WITHER_SKULL: | ||||
|                 vectorY = 0.000001D; | ||||
|                 alwaysSendVelocity = true; | ||||
|                 break; | ||||
|             case EXPERIENCE_ORB: | ||||
|                 vectorY = 0.0221; | ||||
|                 alwaysSendVelocity = true; | ||||
|                 break; | ||||
|             default: | ||||
|                 vectorY = null; | ||||
|                 alwaysSendVelocity = false; | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void run() { | ||||
|         if (!disguise.isDisguiseInUse() || disguise.getEntity() == null) { | ||||
|             cancel(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (++actionBarTicks % 15 == 0) { | ||||
|             actionBarTicks = 0; | ||||
|  | ||||
|             disguise.doActionBar(); | ||||
|         } | ||||
|  | ||||
|         // If entity is no longer valid. Remove it. | ||||
|         if (disguise.getEntity() instanceof Player && !((Player) disguise.getEntity()).isOnline()) { | ||||
|             disguise.removeDisguise(); | ||||
|         } else if (disguise.disguiseExpires > 0 && (DisguiseConfig.isDynamicExpiry() ? disguise.disguiseExpires-- == 1 : | ||||
|                 disguise.disguiseExpires < System.currentTimeMillis())) { // If disguise expired | ||||
|             disguise.removeDisguise(); | ||||
|  | ||||
|             if (disguise.getEntity() instanceof Player) { | ||||
|                 LibsMsg.EXPIRED_DISGUISE.send(disguise.getEntity()); | ||||
|             } | ||||
|  | ||||
|             return; | ||||
|         } else if (!disguise.getEntity().isValid()) { | ||||
|             // If it has been dead for 30+ ticks | ||||
|             // This is to ensure that this disguise isn't removed while clients think its the real entity | ||||
|             // The delay is because if it sends the destroy entity packets straight away, then it means no | ||||
|             // death animation | ||||
|             // This is probably still a problem for wither and enderdragon deaths. | ||||
|             if (deadTicks++ > (disguise.getType() == DisguiseType.ENDER_DRAGON ? 200 : 20)) { | ||||
|                 if (disguise.isRemoveDisguiseOnDeath()) { | ||||
|                     disguise.removeDisguise(); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         deadTicks = 0; | ||||
|  | ||||
|         // If the disguise type is tnt, we need to resend the entity packet else it will turn invisible | ||||
|         if (disguise.getType() == DisguiseType.FIREWORK || disguise.getType() == DisguiseType.EVOKER_FANGS) { | ||||
|             if (lastRefreshed + ((disguise.getType() == DisguiseType.FIREWORK ? 40 : 23) * 50) < System.currentTimeMillis()) { | ||||
|                 lastRefreshed = System.currentTimeMillis(); | ||||
|  | ||||
|                 DisguiseUtilities.refreshTrackers((TargetedDisguise) disguise); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (disguise.isModifyBoundingBox()) { | ||||
|             DisguiseUtilities.doBoundingBox((TargetedDisguise) disguise); | ||||
|         } | ||||
|  | ||||
|         if (disguise.getType() == DisguiseType.BAT && !((BatWatcher) disguise.getWatcher()).isHanging()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         doVelocity(vectorY, alwaysSendVelocity); | ||||
|  | ||||
|         if (disguise.getType() == DisguiseType.EXPERIENCE_ORB) { | ||||
|             PacketContainer packet = new PacketContainer(PacketType.Play.Server.REL_ENTITY_MOVE); | ||||
|  | ||||
|             packet.getIntegers().write(0, disguise.getEntity().getEntityId()); | ||||
|  | ||||
|             try { | ||||
|                 for (Player player : DisguiseUtilities.getPerverts(disguise)) { | ||||
|                     if (disguise.getEntity() != player) { | ||||
|                         ProtocolLibrary.getProtocolManager().sendServerPacket(player, packet, false); | ||||
|                         continue; | ||||
|                     } else if (!disguise.isSelfDisguiseVisible() || !(disguise.getEntity() instanceof Player)) { | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
|                     PacketContainer selfPacket = packet.shallowClone(); | ||||
|  | ||||
|                     selfPacket.getModifier().write(0, DisguiseAPI.getSelfDisguiseId()); | ||||
|  | ||||
|                     try { | ||||
|                         ProtocolLibrary.getProtocolManager().sendServerPacket((Player) disguise.getEntity(), selfPacket, false); | ||||
|                     } catch (InvocationTargetException e) { | ||||
|                         e.printStackTrace(); | ||||
|                     } | ||||
|                 } | ||||
|             } catch (InvocationTargetException e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void doVelocity(Double vectorY, boolean alwaysSendVelocity) { | ||||
|         // If the vectorY isn't 0. Cos if it is. Then it doesn't want to send any vectors. | ||||
|         // If this disguise has velocity sending enabled and the entity is flying. | ||||
|         if (disguise.isVelocitySent() && vectorY != null && (alwaysSendVelocity || !disguise.getEntity().isOnGround())) { | ||||
|             Vector vector = disguise.getEntity().getVelocity(); | ||||
|  | ||||
|             // If the entity doesn't have velocity changes already - You know. I really can't wrap my | ||||
|             // head about the | ||||
|             // if statement. | ||||
|             // But it doesn't seem to do anything wrong.. | ||||
|             if (vector.getY() != 0 && !(vector.getY() < 0 && alwaysSendVelocity && disguise.getEntity().isOnGround())) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // If disguise isn't a experience orb, or the entity isn't standing on the ground | ||||
|             if (disguise.getType() != DisguiseType.EXPERIENCE_ORB || !disguise.getEntity().isOnGround()) { | ||||
|                 PacketContainer lookPacket = null; | ||||
|  | ||||
|                 if (disguise.getType() == DisguiseType.WITHER_SKULL && DisguiseConfig.isWitherSkullPacketsEnabled()) { | ||||
|                     lookPacket = new PacketContainer(PacketType.Play.Server.ENTITY_LOOK); | ||||
|  | ||||
|                     StructureModifier<Object> mods = lookPacket.getModifier(); | ||||
|                     lookPacket.getIntegers().write(0, disguise.getEntity().getEntityId()); | ||||
|                     Location loc = disguise.getEntity().getLocation(); | ||||
|  | ||||
|                     mods.write(4, | ||||
|                             DisguiseUtilities.getYaw(disguise.getType(), disguise.getEntity().getType(), (byte) Math.floor(loc.getYaw() * 256.0F / 360.0F))); | ||||
|                     mods.write(5, DisguiseUtilities | ||||
|                             .getPitch(disguise.getType(), disguise.getEntity().getType(), (byte) Math.floor(loc.getPitch() * 256.0F / 360.0F))); | ||||
|  | ||||
|                     if (disguise.isSelfDisguiseVisible() && disguise.getEntity() instanceof Player) { | ||||
|                         PacketContainer selfLookPacket = lookPacket.shallowClone(); | ||||
|  | ||||
|                         selfLookPacket.getIntegers().write(0, DisguiseAPI.getSelfDisguiseId()); | ||||
|  | ||||
|                         try { | ||||
|                             ProtocolLibrary.getProtocolManager().sendServerPacket((Player) disguise.getEntity(), selfLookPacket, false); | ||||
|                         } catch (InvocationTargetException e) { | ||||
|                             e.printStackTrace(); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 try { | ||||
|                     PacketContainer velocityPacket = new PacketContainer(PacketType.Play.Server.ENTITY_VELOCITY); | ||||
|  | ||||
|                     StructureModifier<Integer> mods = velocityPacket.getIntegers(); | ||||
|  | ||||
|                     // Write entity ID | ||||
|                     mods.write(0, disguise.getEntity().getEntityId()); | ||||
|                     mods.write(1, (int) (vector.getX() * 8000)); | ||||
|                     mods.write(3, (int) (vector.getZ() * 8000)); | ||||
|  | ||||
|                     for (Player player : DisguiseUtilities.getPerverts(disguise)) { | ||||
|                         PacketContainer tempVelocityPacket = velocityPacket.shallowClone(); | ||||
|                         mods = tempVelocityPacket.getIntegers(); | ||||
|  | ||||
|                         // If the viewing player is the disguised player | ||||
|                         if (disguise.getEntity() == player) { | ||||
|                             // If not using self disguise, continue | ||||
|                             if (!disguise.isSelfDisguiseVisible()) { | ||||
|                                 continue; | ||||
|                             } | ||||
|  | ||||
|                             // Write self disguise ID | ||||
|                             mods.write(0, DisguiseAPI.getSelfDisguiseId()); | ||||
|                         } | ||||
|  | ||||
|                         mods.write(2, (int) (8000D * (vectorY * ReflectionManager.getPing(player)) * 0.069D)); | ||||
|  | ||||
|                         if (lookPacket != null && player != disguise.getEntity()) { | ||||
|                             ProtocolLibrary.getProtocolManager().sendServerPacket(player, lookPacket, false); | ||||
|                         } | ||||
|  | ||||
|                         ProtocolLibrary.getProtocolManager().sendServerPacket(player, tempVelocityPacket, false); | ||||
|                     } | ||||
|                 } catch (Exception e) { | ||||
|                     e.printStackTrace(); | ||||
|                 } | ||||
|             } | ||||
|             // If we need to send a packet to update the exp position as it likes to gravitate client | ||||
|             // sided to | ||||
|             // players. | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -3,7 +3,6 @@ package me.libraryaddict.disguise.utilities.reflection.asm; | ||||
| import org.objectweb.asm.*; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.lang.reflect.InvocationTargetException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Map; | ||||
|  | ||||
| @@ -11,8 +10,7 @@ import java.util.Map; | ||||
|  * Created by libraryaddict on 17/02/2020. | ||||
|  */ | ||||
| public class Asm13 { | ||||
|     public byte[] createClassWithoutMethods(String className, ArrayList<Map.Entry<String, String>> illegalMethods) | ||||
|             throws IOException, InvocationTargetException, IllegalAccessException, NoSuchFieldException { | ||||
|     public byte[] createClassWithoutMethods(String className, ArrayList<Map.Entry<String, String>> illegalMethods) throws IOException { | ||||
|         className = className.replace(".", "/") + ".class"; | ||||
|  | ||||
|         ClassReader cr = new ClassReader(getClass().getClassLoader().getResourceAsStream(className)); | ||||
| @@ -20,7 +18,6 @@ public class Asm13 { | ||||
|  | ||||
|         cr.accept(new ClassVisitor(Opcodes.ASM5, writer) { | ||||
|             public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { | ||||
|  | ||||
|                 Map.Entry<String, String> entry = | ||||
|                         illegalMethods.stream().filter(e -> e.getKey().equals(name) && e.getValue().equals(desc)).findFirst().orElse(null); | ||||
|  | ||||
|   | ||||
| @@ -7,8 +7,6 @@ import java.io.BufferedInputStream; | ||||
| import java.io.File; | ||||
| import java.io.FileOutputStream; | ||||
| import java.io.IOException; | ||||
| import java.lang.reflect.Field; | ||||
| import java.lang.reflect.Modifier; | ||||
| import java.net.MalformedURLException; | ||||
| import java.net.URL; | ||||
| import java.net.URLClassLoader; | ||||
| @@ -28,7 +26,6 @@ public class AsmLoader { | ||||
|     private final File filePath = new File(LibsDisguises.getInstance().getDataFolder(), "libs/org-ow2-asm-9.1.jar"); | ||||
|     private boolean asmExists; | ||||
|     private URLClassLoader classLoader; | ||||
|     private LibsJarFile libsJarFile; | ||||
|  | ||||
|     public AsmLoader() { | ||||
|         try { | ||||
| @@ -47,28 +44,6 @@ public class AsmLoader { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void load() { | ||||
|         try { | ||||
|             ClassLoader pluginClassLoader = getClass().getClassLoader(); | ||||
|             Class c = Class.forName("org.bukkit.plugin.java.PluginClassLoader"); | ||||
|             Field file = c.getDeclaredField("file"); | ||||
|             file.setAccessible(true); | ||||
|  | ||||
|             libsJarFile = new LibsJarFile((File) file.get(pluginClassLoader)); | ||||
|  | ||||
|             Field field = c.getDeclaredField("jar"); | ||||
|             field.setAccessible(true); | ||||
|  | ||||
|             Field modifiersField = Field.class.getDeclaredField("modifiers"); | ||||
|             modifiersField.setAccessible(true); | ||||
|             modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); | ||||
|  | ||||
|             field.set(pluginClassLoader, libsJarFile); | ||||
|         } catch (Throwable e) { | ||||
|             e.printStackTrace(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void unload() { | ||||
|         try { | ||||
|             classLoader.close(); | ||||
|   | ||||
| @@ -0,0 +1,120 @@ | ||||
| package me.libraryaddict.disguise.utilities.reflection.asm; | ||||
|  | ||||
| import me.libraryaddict.disguise.LibsDisguises; | ||||
| import me.libraryaddict.disguise.utilities.reflection.ReflectionManager; | ||||
| import org.apache.commons.lang.StringUtils; | ||||
| import org.bukkit.configuration.file.YamlConfiguration; | ||||
|  | ||||
| import java.io.*; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.util.Enumeration; | ||||
| import java.util.Map; | ||||
| import java.util.jar.JarEntry; | ||||
| import java.util.jar.JarFile; | ||||
| import java.util.jar.JarOutputStream; | ||||
| import java.util.stream.Collectors; | ||||
| import java.util.zip.ZipEntry; | ||||
|  | ||||
| /** | ||||
|  * Created by libraryaddict on 20/05/2021. | ||||
|  */ | ||||
| public class FakePluginCreator { | ||||
|     public String getPluginYAML() { | ||||
|         return "name: LibsDisguisesVersioning\nmain: " + getPluginClassPath().replace(".class", "").replace("/", ".") + | ||||
|                 "\ndescription: Plugin created by Libs Disguises for " + | ||||
|                 "compatibility with different versions\nversion: 1.0.0\nauthor: libraryaddict\napi-version: '1.13'\nsoftdepend: [ProtocolLib, LibsDisguises]"; | ||||
|     } | ||||
|  | ||||
|     public File getDestination() { | ||||
|         return new File(LibsDisguises.getInstance().getDataFolder(), "libs/LibsDisguisesCompat.jar"); | ||||
|     } | ||||
|  | ||||
|     public String getPluginClassPath() { | ||||
|         return "me/libraryaddict/disguise/utilities/reflection/asm/LibsDisguisesCompat.class"; | ||||
|     } | ||||
|  | ||||
|     public String getVersion() throws Exception { | ||||
|         File dest = getDestination(); | ||||
|  | ||||
|         if (!dest.exists()) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         JarFile jarFile = new JarFile(dest); | ||||
|  | ||||
|         try (InputStream stream = jarFile.getInputStream(jarFile.getEntry("version.txt"))) { | ||||
|             return new BufferedReader(new InputStreamReader(stream)).lines().collect(Collectors.joining("\n")); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public String getOurVersion() { | ||||
|         YamlConfiguration pluginYml = ReflectionManager.getPluginYAML(LibsDisguises.getInstance().getFile()); | ||||
|  | ||||
|         String buildNo = StringUtils.stripToNull(pluginYml.getString("build-number")); | ||||
|  | ||||
|         return buildNo != null && buildNo.matches("[0-9]+") ? ReflectionManager.getBukkitVersion() + " " + Integer.parseInt(buildNo) : | ||||
|                 ReflectionManager.getBukkitVersion() + " CUSTOM"; | ||||
|     } | ||||
|  | ||||
|     public void createJar(String ourVersion, Map<String, byte[]> classes) throws Exception { | ||||
|         File dest = getDestination(); | ||||
|  | ||||
|         if (!dest.getParentFile().exists()) { | ||||
|             dest.getParentFile().mkdirs(); | ||||
|         } | ||||
|  | ||||
|         if (dest.exists()) { | ||||
|             dest.delete(); | ||||
|         } | ||||
|  | ||||
|         JarOutputStream out = new JarOutputStream(new FileOutputStream(dest)); | ||||
|  | ||||
|         out.putNextEntry(new ZipEntry("plugin.yml")); | ||||
|         out.write(getPluginYAML().getBytes(StandardCharsets.UTF_8)); | ||||
|         out.closeEntry(); | ||||
|  | ||||
|         // Write our main plugin class | ||||
|         try (JarFile jar = new JarFile(LibsDisguises.getInstance().getFile())) { | ||||
|             Enumeration<JarEntry> entries = jar.entries(); | ||||
|             String mainPath = getPluginClassPath().replace(".class", ""); | ||||
|  | ||||
|             while (entries.hasMoreElements()) { | ||||
|                 JarEntry entry = entries.nextElement(); | ||||
|  | ||||
|                 if (!entry.getName().equals(mainPath) && !entry.getName().startsWith("me/libraryaddict/disguise/disguisetypes/")) { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 if (classes.containsKey(entry.getName())) { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 out.putNextEntry(new ZipEntry(entry.getName().equals(mainPath) ? getPluginClassPath() : entry.getName())); | ||||
|  | ||||
|                 try (InputStream stream = jar.getInputStream(entry)) { | ||||
|                     int nRead; | ||||
|                     byte[] data = new byte[1024]; | ||||
|                     while ((nRead = stream.read(data, 0, data.length)) != -1) { | ||||
|                         out.write(data, 0, nRead); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 out.closeEntry(); | ||||
|             } | ||||
|         } catch (Exception ex) { | ||||
|             ex.printStackTrace(); | ||||
|         } | ||||
|  | ||||
|         for (Map.Entry<String, byte[]> entry : classes.entrySet()) { | ||||
|             out.putNextEntry(new ZipEntry(entry.getKey())); | ||||
|             out.write(entry.getValue()); | ||||
|             out.closeEntry(); | ||||
|         } | ||||
|  | ||||
|         out.putNextEntry(new ZipEntry("version.txt")); | ||||
|         out.write(ourVersion.getBytes(StandardCharsets.UTF_8)); | ||||
|         out.closeEntry(); | ||||
|  | ||||
|         out.close(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| package me.libraryaddict.disguise.utilities.reflection.asm; | ||||
|  | ||||
| import org.bukkit.plugin.java.JavaPlugin; | ||||
|  | ||||
| /** | ||||
|  * Created by libraryaddict on 20/05/2021. | ||||
|  */ | ||||
| public class LibsDisguisesCompat extends JavaPlugin { | ||||
| } | ||||
| @@ -1,39 +0,0 @@ | ||||
| package me.libraryaddict.disguise.utilities.reflection.asm; | ||||
|  | ||||
| import java.io.ByteArrayInputStream; | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.util.HashMap; | ||||
| import java.util.jar.JarFile; | ||||
| import java.util.zip.ZipEntry; | ||||
|  | ||||
| /** | ||||
|  * Created by libraryaddict on 15/05/2021. | ||||
|  */ | ||||
| public class LibsJarFile extends JarFile { | ||||
|     private final HashMap<String, byte[]> customFiles = new HashMap<>(); | ||||
|  | ||||
|     public LibsJarFile(File file) throws IOException { | ||||
|         super(file); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public synchronized InputStream getInputStream(ZipEntry ze) throws IOException { | ||||
|         if (customFiles.containsKey(ze.getName())) { | ||||
|             return new ByteArrayInputStream(customFiles.get(ze.getName())); | ||||
|         } | ||||
|  | ||||
|         return super.getInputStream(ze); | ||||
|     } | ||||
|  | ||||
|     public void addClass(String name, byte[] bytes) { | ||||
|         customFiles.put(name, bytes); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void close() throws IOException { | ||||
|         customFiles.clear(); | ||||
|         super.close(); | ||||
|     } | ||||
| } | ||||
| @@ -3,9 +3,12 @@ package me.libraryaddict.disguise.utilities.reflection.asm; | ||||
| import com.google.gson.Gson; | ||||
| import me.libraryaddict.disguise.LibsDisguises; | ||||
| import me.libraryaddict.disguise.utilities.reflection.ReflectionManager; | ||||
| import org.bukkit.Bukkit; | ||||
| import org.bukkit.plugin.Plugin; | ||||
| import org.bukkit.plugin.PluginDescriptionFile; | ||||
| import org.bukkit.plugin.java.JavaPluginLoader; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.InputStream; | ||||
| import java.lang.reflect.Field; | ||||
| import java.lang.reflect.Method; | ||||
| @@ -69,11 +72,29 @@ public class WatcherSanitizer { | ||||
|         } catch (NoSuchFieldException | IllegalAccessException ignored) { | ||||
|         } | ||||
|  | ||||
|         if (Bukkit.getPluginManager().getPlugin("LibsDisguisesVersioning") != null) { | ||||
|             throw new IllegalStateException("Why is LibsDisguisesVersioning already active? Did the server owner do something.. Weird?"); | ||||
|         } | ||||
|  | ||||
|         FakePluginCreator fakePluginCreator = new FakePluginCreator(); | ||||
|  | ||||
|         String ourVers = fakePluginCreator.getOurVersion(); | ||||
|  | ||||
|         try { | ||||
|             if (ourVers != null && ourVers.equals(fakePluginCreator.getVersion()) && !ourVers.contains(" CUSTOM")) { | ||||
|                 loadPlugin(fakePluginCreator); | ||||
|                 return; | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             e.printStackTrace(); | ||||
|         } | ||||
|  | ||||
|         LibsDisguises.getInstance().getLogger().info("Creating a new version compatibility jar"); | ||||
|  | ||||
|         ArrayList<String> mapped = new ArrayList<>(); | ||||
|  | ||||
|         try (InputStream stream = LibsDisguises.getInstance().getResource("ANTI_PIRACY_ENCRYPTION")) { | ||||
|             AsmLoader loader = new AsmLoader(); | ||||
|             loader.load(); | ||||
|  | ||||
|             Object obj; | ||||
|             Method getBytes; | ||||
| @@ -109,15 +130,18 @@ public class WatcherSanitizer { | ||||
|                 list.add(new HashMap.SimpleEntry(info.getMethod(), info.getDescriptor())); | ||||
|             } | ||||
|  | ||||
|             Map<String, byte[]> classes = new HashMap<>(); | ||||
|  | ||||
|             for (Map.Entry<String, ArrayList<Map.Entry<String, String>>> entry : toRemove.entrySet()) { | ||||
|                 byte[] bytes = (byte[]) getBytes.invoke(obj, entry.getKey(), entry.getValue()); | ||||
|                 mapped.add(entry.getKey()); | ||||
|  | ||||
|                 String name = entry.getKey().replace(".", "/") + ".class"; | ||||
|  | ||||
|                 loader.getLibsJarFile().addClass(name, bytes); | ||||
|                 classes.put(entry.getKey().replace(".", "/") + ".class", bytes); | ||||
|             } | ||||
|  | ||||
|             fakePluginCreator.createJar(ourVers, classes); | ||||
|             loadPlugin(fakePluginCreator); | ||||
|  | ||||
|             if (!loader.isAsmExists()) { | ||||
|                 loader.unload(); | ||||
|             } | ||||
| @@ -126,4 +150,32 @@ public class WatcherSanitizer { | ||||
|             LibsDisguises.getInstance().getLogger().severe("Registered: " + new Gson().toJson(mapped)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static void loadPlugin(FakePluginCreator fakePluginCreator) throws Exception { | ||||
|         LibsDisguises.getInstance().getLogger().info("Starting version support plugin: LibsDisguisesVersioning"); | ||||
|         Method method = Class.forName("org.bukkit.plugin.PluginManager", false, WatcherSanitizer.class.getClassLoader().getParent()) | ||||
|                 .getMethod("loadPlugin", File.class); | ||||
|  | ||||
|         Plugin plugin = (Plugin) method.invoke(Bukkit.getPluginManager(), fakePluginCreator.getDestination()); | ||||
|  | ||||
|         Class pluginClassLoader = Class.forName("org.bukkit.plugin.java.PluginClassLoader"); | ||||
|  | ||||
|         Field loaderField = JavaPluginLoader.class.getDeclaredField("loaders"); | ||||
|         loaderField.setAccessible(true); | ||||
|         List loaderList = (List) loaderField.get(LibsDisguises.getInstance().getPluginLoader()); | ||||
|  | ||||
|         Field pluginOwner = pluginClassLoader.getDeclaredField("plugin"); | ||||
|         pluginOwner.setAccessible(true); | ||||
|  | ||||
|         // Move Lib's Disguises to load its classes after the new plugin | ||||
|         for (Object o : loaderList) { | ||||
|             if (pluginOwner.get(o) != LibsDisguises.getInstance()) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             loaderList.remove(o); | ||||
|             loaderList.add(o); | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import me.libraryaddict.disguise.utilities.LibsPremium; | ||||
| import me.libraryaddict.disguise.utilities.reflection.ClassGetter; | ||||
| import me.libraryaddict.disguise.utilities.reflection.NmsAddedIn; | ||||
| import me.libraryaddict.disguise.utilities.reflection.NmsRemovedIn; | ||||
| import me.libraryaddict.disguise.utilities.reflection.asm.FakePluginCreator; | ||||
| import me.libraryaddict.disguise.utilities.sounds.DisguiseSoundEnums; | ||||
| import me.libraryaddict.disguise.utilities.sounds.SoundGroup; | ||||
| import org.apache.commons.lang.StringUtils; | ||||
| @@ -34,6 +35,14 @@ public class CompileMethods { | ||||
|     public static void main(String[] args) { | ||||
|         doMethods(); | ||||
|         doSounds(); | ||||
|         moveCompat(); | ||||
|     } | ||||
|  | ||||
|     private static void moveCompat() { | ||||
|         FakePluginCreator creator = new FakePluginCreator(); | ||||
|  | ||||
|         File compatFile = new File("target/classes/" + creator.getPluginClassPath()); | ||||
|         compatFile.renameTo(new File(compatFile.getParentFile(), compatFile.getName().replace(".class", ""))); | ||||
|     } | ||||
|  | ||||
|     private static void doSounds() { | ||||
| @@ -91,8 +100,7 @@ public class CompileMethods { | ||||
|     } | ||||
|  | ||||
|     private static void doMethods() { | ||||
|         ArrayList<Class<?>> classes = | ||||
|                 ClassGetter.getClassesForPackage(FlagWatcher.class, "me.libraryaddict.disguise.disguisetypes.watchers"); | ||||
|         ArrayList<Class<?>> classes = ClassGetter.getClassesForPackage(FlagWatcher.class, "me.libraryaddict.disguise.disguisetypes.watchers"); | ||||
|  | ||||
|         ArrayList<Class> sorted = new ArrayList<>(); | ||||
|  | ||||
| @@ -106,12 +114,10 @@ public class CompileMethods { | ||||
|             for (Method method : c.getMethods()) { | ||||
|                 if (!FlagWatcher.class.isAssignableFrom(method.getDeclaringClass())) { | ||||
|                     continue; | ||||
|                 } else if (method.getParameterCount() > 1 && !method.isAnnotationPresent(NmsAddedIn.class) && | ||||
|                         !method.isAnnotationPresent(NmsRemovedIn.class)) { | ||||
|                 } else if (method.getParameterCount() > 1 && !method.isAnnotationPresent(NmsAddedIn.class) && !method.isAnnotationPresent(NmsRemovedIn.class)) { | ||||
|                     continue; | ||||
|                 } else if (!(method.getName().startsWith("set") && method.getParameterCount() == 1) && | ||||
|                         !method.getName().startsWith("get") && !method.getName().startsWith("has") && | ||||
|                         !method.getName().startsWith("is")) { | ||||
|                 } else if (!(method.getName().startsWith("set") && method.getParameterCount() == 1) && !method.getName().startsWith("get") && | ||||
|                         !method.getName().startsWith("has") && !method.getName().startsWith("is")) { | ||||
|                     continue; | ||||
|                 } else if (method.getName().equals("removePotionEffect")) { | ||||
|                     continue; | ||||
| @@ -141,8 +147,7 @@ public class CompileMethods { | ||||
|                     descriptor = ":" + getMethodDescriptor(method) + ":" + added + ":" + removed; | ||||
|                 } | ||||
|  | ||||
|                 String s = | ||||
|                         method.getDeclaringClass().getSimpleName() + ":" + method.getName() + ":" + param + descriptor; | ||||
|                 String s = method.getDeclaringClass().getSimpleName() + ":" + method.getName() + ":" + param + descriptor; | ||||
|  | ||||
|                 if (methods.contains(s)) { | ||||
|                     continue; | ||||
|   | ||||
| @@ -6,7 +6,7 @@ build-date: ${timestamp} | ||||
| build-number: ${build.number} | ||||
| author: libraryaddict | ||||
| authors: [Byteflux, Navid K.] | ||||
| softdepend: [ProtocolLib] | ||||
| softdepend: [ProtocolLib, LibsDisguisesVersioning] | ||||
| api-version: '1.13' | ||||
| commands: | ||||
|   libsdisguises: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user