commit 26959ace6cf57468fc7bb741673df032606e47c7 Author: batchprogrammer314 Date: Sat Jun 26 19:43:58 2021 -0500 bot physics complete diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eab556a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 batchprogrammer314 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PlayerAI.iml b/PlayerAI.iml new file mode 100644 index 0000000..8b10cdd --- /dev/null +++ b/PlayerAI.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/net/nuggetmc/ai/PlayerAI.java b/src/net/nuggetmc/ai/PlayerAI.java new file mode 100644 index 0000000..aa0c4e7 --- /dev/null +++ b/src/net/nuggetmc/ai/PlayerAI.java @@ -0,0 +1,68 @@ +package net.nuggetmc.ai; + +import net.nuggetmc.ai.cmd.CommandHandler; +import net.nuggetmc.ai.cmd.CommandInterface; +import net.nuggetmc.ai.cmd.commands.CreateCommand; +import net.nuggetmc.ai.cmd.commands.DebugCommand; +import net.nuggetmc.ai.cmd.commands.InfoCommand; +import net.nuggetmc.ai.cmd.commands.ResetCommand; +import net.nuggetmc.ai.npc.NPCManager; +import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.java.JavaPlugin; + +public class PlayerAI extends JavaPlugin { + + private static PlayerAI instance; + + private final CommandHandler HANDLER; + private final NPCManager MANAGER; + + public PlayerAI() { + instance = this; + + this.HANDLER = new CommandHandler(); + this.MANAGER = new NPCManager(this); + } + + public static PlayerAI getInstance() { + return instance; + } + + public CommandHandler getHandler() { + return HANDLER; + } + + public NPCManager getManager() { + return MANAGER; + } + + @Override + public void onEnable() { + getServer().getPluginManager().registerEvents(MANAGER, this); + registerCommands(); + + MANAGER.connectAll(); + } + + @Override + public void onDisable() { + MANAGER.reset(); + MANAGER.disconnectAll(); + } + + private void registerCommands() { + HANDLER.register(new CommandInterface[] { + new CreateCommand(), + new InfoCommand(), + new DebugCommand(), + new ResetCommand() + }); + + PluginCommand command = getCommand("playerai"); + + if (command != null) { + command.setExecutor(HANDLER); + command.setTabCompleter(HANDLER); + } + } +} diff --git a/src/net/nuggetmc/ai/cmd/CommandHandler.java b/src/net/nuggetmc/ai/cmd/CommandHandler.java new file mode 100644 index 0000000..ecd4547 --- /dev/null +++ b/src/net/nuggetmc/ai/cmd/CommandHandler.java @@ -0,0 +1,112 @@ +package net.nuggetmc.ai.cmd; + +import net.md_5.bungee.api.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; + +import java.util.*; + +public class CommandHandler implements CommandExecutor, TabCompleter { + + private final Map COMMANDS = new HashMap<>(); + + private final String PREFIX = "bot"; + + public void register(CommandInterface[] subCmds) { + for (CommandInterface cmd : subCmds) { + COMMANDS.put(cmd.getName(), cmd); + } + } + + public String fetchName(CommandInterface subCmd) { + String name = subCmd.getClass().getSimpleName(); + return name.substring(0, name.length() - 7).toLowerCase(); + } + + private boolean exists(String name) { + return COMMANDS.containsKey(name.toLowerCase()); + } + + private CommandInterface getExecutor(String name) { + return COMMANDS.get(name.toLowerCase()); + } + + public String nonPlayerMsg() { + return ChatColor.RED + "You must be a player to execute this command!"; + } + + public String usageMsg(CommandInterface subCmd) { + return ChatColor.RED + "Invalid arguments!\nUsage: /" + PREFIX + " " + subCmd.getName() + " " + subCmd.getUsage(); + } + + private void sendCmdInfo(CommandSender sender) { + sender.sendMessage(ChatColor.GRAY + "----------------------------------------"); + + sender.sendMessage(ChatColor.GOLD + "PlayerAI" + ChatColor.GRAY + " [" + ChatColor.RED + "v1.0" + ChatColor.GRAY + "]"); + + for (Map.Entry entry : COMMANDS.entrySet()) { + sender.sendMessage(ChatColor.GRAY + " ▪ " + ChatColor.YELLOW + "/" + PREFIX + " " + entry.getKey() + ChatColor.GRAY + " ▪ " + + ChatColor.RESET + entry.getValue().getDescription()); + } + + sender.sendMessage(ChatColor.GRAY + "----------------------------------------"); + } + + @Override + public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { + if (args.length == 0 || !exists(args[0])) { + sendCmdInfo(sender); + return true; + } + + getExecutor(args[0]).onCommand(sender, cmd, label, Arrays.copyOfRange(args, 1, args.length)); + return true; + } + + @Override + public List onTabComplete(CommandSender sender, Command cmd, String label, String[] args) { + if (!cmd.getName().equals("playerai")) return null; + + List groupnames; + int n = args.length; + + switch (n) { + case 1: + groupnames = new ArrayList<>(COMMANDS.keySet()); + String arg = args[n - 1]; + + if (!isEmptyTab(arg)) { + return autofill(groupnames, arg); + } + + return groupnames; + + default: + return null; + } + } + + private boolean isEmptyTab(String s) { + return s == null || s.equals("") || s.equals(" ") || s.isEmpty(); + } + + private List autofill(List groupnames, String input) { + List list = new ArrayList<>(); + + for (String entry : groupnames) { + if (entry.length() >= input.length()) { + if (input.equalsIgnoreCase(entry.substring(0, input.length()))) { + list.add(entry); + } + } + } + + if (list.isEmpty()) { + return groupnames; + } + + return list; + } +} diff --git a/src/net/nuggetmc/ai/cmd/CommandInterface.java b/src/net/nuggetmc/ai/cmd/CommandInterface.java new file mode 100644 index 0000000..07b2b49 --- /dev/null +++ b/src/net/nuggetmc/ai/cmd/CommandInterface.java @@ -0,0 +1,12 @@ +package net.nuggetmc.ai.cmd; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; + +public interface CommandInterface { + String getName(); + String getDescription(); + String getUsage(); + + void onCommand(CommandSender sender, Command cmd, String label, String[] args); +} diff --git a/src/net/nuggetmc/ai/cmd/commands/CreateCommand.java b/src/net/nuggetmc/ai/cmd/commands/CreateCommand.java new file mode 100644 index 0000000..230f3bc --- /dev/null +++ b/src/net/nuggetmc/ai/cmd/commands/CreateCommand.java @@ -0,0 +1,63 @@ +package net.nuggetmc.ai.cmd.commands; + +import net.md_5.bungee.api.ChatColor; +import net.nuggetmc.ai.PlayerAI; +import net.nuggetmc.ai.cmd.CommandHandler; +import net.nuggetmc.ai.cmd.CommandInterface; +import net.nuggetmc.ai.npc.NPC; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class CreateCommand implements CommandInterface { + + private final CommandHandler HANDLER; + + private final String NAME; + private final String DESCRIPTION = "Create NPCs."; + private final String CMD_ARGS = " [skin]"; + + public CreateCommand() { + this.HANDLER = PlayerAI.getInstance().getHandler(); + this.NAME = HANDLER.fetchName(this); + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } + + @Override + public String getUsage() { + return CMD_ARGS; + } + + @Override + public void onCommand(CommandSender sender, Command cmd, String label, String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage(HANDLER.nonPlayerMsg()); + return; + } + + if (args.length == 0) { + sender.sendMessage(HANDLER.usageMsg(this)); + return; + } + + String skin; + + if (args.length > 1) { + skin = args[1]; + } else { + skin = args[0]; + } + + Player player = (Player) sender; + NPC npc = NPC.createNPC(args[0], player.getLocation(), skin); + } +} diff --git a/src/net/nuggetmc/ai/cmd/commands/DebugCommand.java b/src/net/nuggetmc/ai/cmd/commands/DebugCommand.java new file mode 100644 index 0000000..c42265c --- /dev/null +++ b/src/net/nuggetmc/ai/cmd/commands/DebugCommand.java @@ -0,0 +1,52 @@ +package net.nuggetmc.ai.cmd.commands; + +import net.nuggetmc.ai.PlayerAI; +import net.nuggetmc.ai.cmd.CommandHandler; +import net.nuggetmc.ai.cmd.CommandInterface; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.HashSet; +import java.util.Set; + +public class DebugCommand implements CommandInterface { + + private final CommandHandler HANDLER; + + private final String NAME; + private final String DESCRIPTION = "Debug NPC stats."; + private final String CMD_ARGS = ""; + + public DebugCommand() { + this.HANDLER = PlayerAI.getInstance().getHandler(); + this.NAME = HANDLER.fetchName(this); + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } + + @Override + public String getUsage() { + return CMD_ARGS; + } + + private boolean active = false; + + private Set tasks = new HashSet<>(); + + private double round2Dec(double n) { + return Math.round(n * 100) / 100.0; + } + + @Override + public void onCommand(CommandSender sender, Command cmd, String label, String[] args) { + } +} diff --git a/src/net/nuggetmc/ai/cmd/commands/InfoCommand.java b/src/net/nuggetmc/ai/cmd/commands/InfoCommand.java new file mode 100644 index 0000000..053a737 --- /dev/null +++ b/src/net/nuggetmc/ai/cmd/commands/InfoCommand.java @@ -0,0 +1,40 @@ +package net.nuggetmc.ai.cmd.commands; + +import net.nuggetmc.ai.PlayerAI; +import net.nuggetmc.ai.cmd.CommandHandler; +import net.nuggetmc.ai.cmd.CommandInterface; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; + +public class InfoCommand implements CommandInterface { + + private final CommandHandler HANDLER; + + private final String NAME; + private final String DESCRIPTION = "Information about loaded NPCs."; + private final String CMD_ARGS = ""; + + public InfoCommand() { + this.HANDLER = PlayerAI.getInstance().getHandler(); + this.NAME = HANDLER.fetchName(this); + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } + + @Override + public String getUsage() { + return CMD_ARGS; + } + + @Override + public void onCommand(CommandSender sender, Command cmd, String label, String[] args) { + } +} diff --git a/src/net/nuggetmc/ai/cmd/commands/ResetCommand.java b/src/net/nuggetmc/ai/cmd/commands/ResetCommand.java new file mode 100644 index 0000000..a3c86b6 --- /dev/null +++ b/src/net/nuggetmc/ai/cmd/commands/ResetCommand.java @@ -0,0 +1,64 @@ +package net.nuggetmc.ai.cmd.commands; + +import net.md_5.bungee.api.ChatColor; +import net.nuggetmc.ai.PlayerAI; +import net.nuggetmc.ai.cmd.CommandHandler; +import net.nuggetmc.ai.cmd.CommandInterface; +import net.nuggetmc.ai.npc.NPCManager; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; + +import java.text.NumberFormat; +import java.util.Locale; + +public class ResetCommand implements CommandInterface { + + private final PlayerAI INSTANCE; + + private final CommandHandler HANDLER; + private final NPCManager MANAGER; + + private final String NAME; + private final String DESCRIPTION = "Information about loaded NPCs."; + private final String CMD_ARGS = ""; + + public ResetCommand() { + this.INSTANCE = PlayerAI.getInstance(); + this.HANDLER = INSTANCE.getHandler(); + this.MANAGER = INSTANCE.getManager(); + this.NAME = HANDLER.fetchName(this); + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } + + @Override + public String getUsage() { + return CMD_ARGS; + } + + @Override + public void onCommand(CommandSender sender, Command cmd, String label, String[] args) { + sender.sendMessage("Removing every NPC..."); + int size = MANAGER.fetch().size(); + + MANAGER.reset(); + + String en; + if (size == 1) { + en = "y"; + } else { + en = "ies"; + } + + String formatted = NumberFormat.getNumberInstance(Locale.US).format(size); + sender.sendMessage("Removed " + ChatColor.RED + formatted + ChatColor.RESET + " entit" + en + "."); + } +} diff --git a/src/net/nuggetmc/ai/npc/NPC.java b/src/net/nuggetmc/ai/npc/NPC.java new file mode 100644 index 0000000..a70ab89 --- /dev/null +++ b/src/net/nuggetmc/ai/npc/NPC.java @@ -0,0 +1,282 @@ +package net.nuggetmc.ai.npc; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import net.minecraft.server.v1_16_R3.*; +import net.nuggetmc.ai.PlayerAI; +import net.nuggetmc.ai.utils.MojangAPI; +import net.nuggetmc.ai.utils.SteveUUID; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.attribute.Attribute; +import org.bukkit.craftbukkit.v1_16_R3.CraftServer; +import org.bukkit.craftbukkit.v1_16_R3.CraftWorld; +import org.bukkit.craftbukkit.v1_16_R3.entity.CraftEntity; +import org.bukkit.craftbukkit.v1_16_R3.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +import java.util.UUID; + +public class NPC extends EntityPlayer { + + public Vector velocity; + + private byte kbTicks; + + private final double REGEN_AMOUNT = 0.05; + + public NPC(MinecraftServer minecraftServer, WorldServer worldServer, GameProfile profile, PlayerInteractManager manager) { + super(minecraftServer, worldServer, profile, manager); + + velocity = new Vector(0, 0, 0); + kbTicks = 0; + } + + public static NPC createNPC(String name, Location loc, String skin) { + MinecraftServer nmsServer = ((CraftServer) Bukkit.getServer()).getServer(); + WorldServer nmsWorld = ((CraftWorld) loc.getWorld()).getHandle(); + + UUID uuid = SteveUUID.generate(); + + GameProfile profile = new GameProfile(uuid, name); + PlayerInteractManager interactManager = new PlayerInteractManager(nmsWorld); + + if (skin != null) { + setSkin(profile, skin); + } + + NPC npc = new NPC(nmsServer, nmsWorld, profile, interactManager); + + npc.playerConnection = new PlayerConnection(nmsServer, new NetworkManager(EnumProtocolDirection.CLIENTBOUND), npc); + npc.setLocation(loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch()); + npc.getBukkitEntity().setNoDamageTicks(0); + nmsWorld.addEntity(npc); + + sendSpawnPackets(npc); + + PlayerAI.getInstance().getManager().add(npc); + + return npc; + } + + private static void setSkin(GameProfile profile, String skin) { + String[] vals = MojangAPI.getSkin(skin); + + if (vals != null) { + profile.getProperties().put("textures", new Property("textures", vals[0], vals[1])); + } + } + + private static void sendSpawnPackets(NPC npc) { + DataWatcher watcher = npc.getDataWatcher(); + watcher.set(new DataWatcherObject<>(16, DataWatcherRegistry.a), (byte) 0xFF); + + for (Player player : Bukkit.getOnlinePlayers()) { + PlayerConnection connection = ((CraftPlayer) player).getHandle().playerConnection; + npc.render(connection, false); + } + } + + public void render(PlayerConnection connection, boolean login) { + connection.sendPacket(new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, this)); + connection.sendPacket(new PacketPlayOutNamedEntitySpawn(this)); + connection.sendPacket(new PacketPlayOutEntityMetadata(this.getId(), this.getDataWatcher(), true)); + connection.sendPacket(new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, this)); + + PacketPlayOutEntityHeadRotation rotationPacket = new PacketPlayOutEntityHeadRotation(this, (byte) ((this.yaw * 256f) / 360f)); + + if (login) { + Bukkit.getScheduler().runTaskLater(PlayerAI.getInstance(), () -> connection.sendPacket(rotationPacket), 10); + } else { + connection.sendPacket(rotationPacket); + } + } + + public void setVelocity(Vector vector) { + this.velocity = vector; + } + + public void addVelocity(Vector vector) { + this.velocity.add(vector); + } + + @Override + public void tick() { + super.tick(); + + if (noDamageTicks > 0) --noDamageTicks; + if (kbTicks > 0) --kbTicks; + + Player playerNPC = this.getBukkitEntity(); + if (playerNPC.isDead()) return; + + double health = playerNPC.getHealth(); + double maxHealth = playerNPC.getAttribute(Attribute.GENERIC_MAX_HEALTH).getDefaultValue(); + double amount; + + if (health < maxHealth - REGEN_AMOUNT) { + amount = health + REGEN_AMOUNT; + } else { + amount = maxHealth; + } + + playerNPC.setHealth(amount); + + updateLocation(); + } + + private void updateLocation() { + velocity.setY(velocity.getY() - 0.1); + + if (predictGround()) { + velocity.setY(0); + addFriction(); + } + + this.move(EnumMoveType.SELF, new Vec3D(velocity.getX(), velocity.getY(), velocity.getZ())); + } + + private final double OFFSET = 0.05; + + public boolean predictGround() { + double vy = velocity.getY(); + + if (vy > 0) { + return false; + } + + double m = vy / 20.0; + + World world = getBukkitEntity().getWorld(); + AxisAlignedBB box = getBoundingBox(); + + double[] xVals = new double[] { + box.minX + OFFSET, + box.maxX - OFFSET + }; + + double[] zVals = new double[] { + box.minZ + OFFSET, + box.maxZ - OFFSET + }; + + for (double x : xVals) { + for (double z : zVals) { + double i = locY(); + + for (int n = 0; n < 20; n++) { + Location test = new Location(world, x, i, z); + + if (test.getBlock().getType().isSolid()) { + return true; + } + + i += m; + } + } + } + + return false; + } + + public void addFriction() { + velocity.setX(velocity.getX() * 0.5); + velocity.setZ(velocity.getZ() * 0.5); + } + + public void despawn() { + getBukkitEntity().remove(); + } + + @Override + public void collide(Entity entity) { + if (!this.isSameVehicle(entity) && !entity.noclip && !this.noclip) { + double d0 = entity.locX() - this.locX(); + double d1 = entity.locZ() - this.locZ(); + double d2 = MathHelper.a(d0, d1); + if (d2 >= 0.009999999776482582D) { + d2 = MathHelper.sqrt(d2); + d0 /= d2; + d1 /= d2; + double d3 = 1.0D / d2; + if (d3 > 1.0D) { + d3 = 1.0D; + } + + d0 *= d3; + d1 *= d3; + d0 *= 0.05000000074505806D; + d1 *= 0.05000000074505806D; + d0 *= 1.0F - this.I; + d1 *= 1.0F - this.I; + + if (!this.isVehicle()) { + velocity.add(new Vector(-d0 * 3, 0.0D, -d1 * 3)); + } + + if (!entity.isVehicle()) { + entity.i(d0, 0.0D, d1); + } + } + } + } + + @Override + public boolean damageEntity(DamageSource damagesource, float f) { + boolean damaged = super.damageEntity(damagesource, f); + + net.minecraft.server.v1_16_R3.Entity attacker = damagesource.getEntity(); + + if (damaged && kbTicks == 0 && attacker != null) { + Player player = getBukkitEntity(); + CraftEntity entity = attacker.getBukkitEntity(); + Location loc1 = player.getLocation(); + Location loc2 = entity.getLocation(); + + kb(player, loc1, loc2); + } + + return damaged; + } + + private void kb(Player playerNPC, Location loc1, Location loc2) { + Vector diff = loc1.toVector().subtract(loc2.toVector()).normalize(); + diff.multiply(0.25); + diff.setY(0.5); + + velocity.add(diff); + kbTicks = 10; + } + + public void faceLocation(Location loc) { + try { + CraftPlayer playerNPC = this.getBukkitEntity(); + Vector dir = loc.toVector().subtract(playerNPC.getLocation().toVector()).normalize(); + Location facing = playerNPC.getLocation().setDirection(dir); + playerNPC.teleport(facing); + + for (Player player : Bukkit.getOnlinePlayers()) { + PlayerConnection connection = ((CraftPlayer) player).getHandle().playerConnection; + connection.sendPacket(new PacketPlayOutEntityHeadRotation(playerNPC.getHandle(), (byte) (facing.getYaw() * 256 / 360))); + } + + } catch (IllegalArgumentException ignored) { } + } + + @Override + public void playerTick() { + if (this.hurtTicks > 0) { + this.hurtTicks -= 1; + } + + entityBaseTick(); + tickPotionEffects(); + + this.aU = (int) this.aT; + this.aL = this.aK; + this.lastYaw = this.yaw; + this.lastPitch = this.pitch; + } +} diff --git a/src/net/nuggetmc/ai/npc/NPCManager.java b/src/net/nuggetmc/ai/npc/NPCManager.java new file mode 100644 index 0000000..023868b --- /dev/null +++ b/src/net/nuggetmc/ai/npc/NPCManager.java @@ -0,0 +1,153 @@ +package net.nuggetmc.ai.npc; + +import io.netty.channel.*; +import net.minecraft.server.v1_16_R3.PacketPlayOutEntityMetadata; +import net.minecraft.server.v1_16_R3.PacketPlayOutNamedEntitySpawn; +import net.minecraft.server.v1_16_R3.PacketPlayOutPlayerInfo; +import net.minecraft.server.v1_16_R3.PlayerConnection; +import net.nuggetmc.ai.PlayerAI; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.v1_16_R3.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class NPCManager implements Listener { + + private PlayerAI plugin; + + private final Set NPCS = new HashSet<>(); + private final Map NPC_CONNECTIONS = new HashMap<>(); + + public Set fetch() { + return NPCS; + } + + public void add(NPC npc) { + NPCS.add(npc); + + Bukkit.getScheduler().runTaskLater(PlayerAI.getInstance(), () -> { + NPC_CONNECTIONS.put(npc.getId(), npc); + }, 10); + } + + public NPCManager(PlayerAI instance) { + plugin = instance; + } + + public void reset() { + for (NPC npc : NPCS) { + npc.despawn(); + } + + NPCS.clear(); + NPC_CONNECTIONS.clear(); + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + PlayerConnection connection = ((CraftPlayer) event.getPlayer()).getHandle().playerConnection; + + for (NPC npc : NPCS) { + npc.render(connection, true); + } + + injectPlayer(event.getPlayer()); + } + + public void connectAll() { + for (Player player : Bukkit.getOnlinePlayers()) { + injectPlayer(player); + } + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + removePlayer(event.getPlayer()); + } + + public void disconnectAll() { + for (Player player : Bukkit.getOnlinePlayers()) { + removePlayer(player); + } + } + + private void injectPlayer(Player player) { + ChannelDuplexHandler channelDuplexHandler = new ChannelDuplexHandler() { + + @Override + public void channelRead(ChannelHandlerContext channelHandlerContext, Object packet) throws Exception { + super.channelRead(channelHandlerContext, packet); + } + + @Override + public void write(ChannelHandlerContext channelHandlerContext, Object packet, ChannelPromise channelPromise) throws Exception { + if (packet instanceof PacketPlayOutNamedEntitySpawn) { + renderNPC(player, (PacketPlayOutNamedEntitySpawn) packet); + } + + super.write(channelHandlerContext, packet, channelPromise); + } + }; + + ChannelPipeline pipeline = ((CraftPlayer) player).getHandle().playerConnection.networkManager.channel.pipeline(); + + try { + pipeline.addBefore("packet_handler", player.getName(), channelDuplexHandler); + } catch (IllegalArgumentException ignore) { } + } + + private void renderNPC(Player player, PacketPlayOutNamedEntitySpawn packet) { + PlayerConnection connection = ((CraftPlayer) player).getHandle().playerConnection; + Field field; + + try { + field = packet.getClass().getDeclaredField("a"); + } catch (NoSuchFieldException e) { + return; + } + + field.setAccessible(true); + + Object obj; + + try { + obj = field.get(packet); + } catch (IllegalAccessException e) { + return; + } + + if (!(obj instanceof Integer)) return; + int n = (int) obj; + + NPC npc = NPC_CONNECTIONS.get(n); + if (npc == null) return; + + connection.sendPacket(new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, npc)); + connection.sendPacket(new PacketPlayOutEntityMetadata(npc.getId(), npc.getDataWatcher(), true)); + + PacketPlayOutPlayerInfo noTabPacket = new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, npc); + + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (!connection.isDisconnected()) { + connection.sendPacket(noTabPacket); + } + }, 5); + } + + public void removePlayer(Player player) { + Channel channel = ((CraftPlayer) player).getHandle().playerConnection.networkManager.channel; + channel.eventLoop().submit(() -> { + channel.pipeline().remove(player.getName()); + return null; + }); + } +} diff --git a/src/net/nuggetmc/ai/utils/MojangAPI.java b/src/net/nuggetmc/ai/utils/MojangAPI.java new file mode 100644 index 0000000..c9764f7 --- /dev/null +++ b/src/net/nuggetmc/ai/utils/MojangAPI.java @@ -0,0 +1,28 @@ +package net.nuggetmc.ai.utils; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.InputStreamReader; +import java.net.URL; + +public class MojangAPI { + + public static String[] getSkin(String name) { + try { + URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + name); + InputStreamReader reader = new InputStreamReader(url.openStream()); + String uuid = new JsonParser().parse(reader).getAsJsonObject().get("id").getAsString(); + + URL url2 = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false"); + InputStreamReader reader2 = new InputStreamReader(url2.openStream()); + JsonObject property = new JsonParser().parse(reader2).getAsJsonObject().get("properties").getAsJsonArray().get(0).getAsJsonObject(); + String texture = property.get("value").getAsString(); + String signature = property.get("signature").getAsString(); + + return new String[] {texture, signature}; + } catch (Exception e) { + return null; + } + } +} diff --git a/src/net/nuggetmc/ai/utils/SteveUUID.java b/src/net/nuggetmc/ai/utils/SteveUUID.java new file mode 100644 index 0000000..303bcd7 --- /dev/null +++ b/src/net/nuggetmc/ai/utils/SteveUUID.java @@ -0,0 +1,16 @@ +package net.nuggetmc.ai.utils; + +import java.util.UUID; + +public class SteveUUID { + + public static UUID generate() { + UUID uuid = UUID.randomUUID(); + + if (uuid.hashCode() % 2 == 0) { + return uuid; + } + + return generate(); + } +} diff --git a/src/plugin.yml b/src/plugin.yml new file mode 100644 index 0000000..cbb6748 --- /dev/null +++ b/src/plugin.yml @@ -0,0 +1,20 @@ +name: PlayerAI +main: net.nuggetmc.ai.PlayerAI +version: 1.0 +api-version: 1.16 +author: HorseNuggets +commands: + playerai: + description: The PlayerAI base command. + permission: playerai.manage + usage: /playerai + aliases: [bot, pai, ai] +permissions: + playerai.*: + description: PlayerAI parent permission. + default: op + children: + playerai.manage: true + playerai.manage: + description: Allows for PlayerAI NPC management. + default: op \ No newline at end of file