diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/patches/api/0002-Leaves-fakeplayer-API.patch b/patches/api/0002-Leaves-fakeplayer-API.patch new file mode 100644 index 0000000..f45f596 --- /dev/null +++ b/patches/api/0002-Leaves-fakeplayer-API.patch @@ -0,0 +1,564 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Mon, 29 Jan 2024 12:44:10 +0000 +Subject: [PATCH] Leaves fakeplayer API + + +diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java +index b7e1c8bd8dd38e1a9e74925740b22dad61a75f49..36863cd765277b9b551ae04a4ecbee502c4d0a4e 100644 +--- a/src/main/java/org/bukkit/Bukkit.java ++++ b/src/main/java/org/bukkit/Bukkit.java +@@ -58,6 +58,7 @@ import org.jetbrains.annotations.Contract; + import org.jetbrains.annotations.NotNull; + import org.jetbrains.annotations.Nullable; + import io.papermc.paper.util.JarManifests; // Paper ++import top.leavesmc.leaves.entity.BotManager; + + /** + * Represents the Bukkit core, for version and Server singleton handling +@@ -2836,6 +2837,18 @@ public final class Bukkit { + } + // Folia end - region threading API + ++ // Leaves start - Bot API ++ /** ++ * Returns a bot manager. ++ * ++ * @return Bot Manager ++ */ ++ public static @NotNull BotManager getBotManager() { ++ return server.getBotManager(); ++ } ++ // Leaves end - Bot API ++ ++ + @NotNull + public static Server.Spigot spigot() { + return server.spigot(); +diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java +index 85b169c04f44431363d4e14d4857140f160ceace..ed89520a9bf17a6f2665a17ded0c334d8f9bf130 100644 +--- a/src/main/java/org/bukkit/Server.java ++++ b/src/main/java/org/bukkit/Server.java +@@ -58,6 +58,7 @@ import org.bukkit.util.CachedServerIcon; + import org.jetbrains.annotations.Contract; + import org.jetbrains.annotations.NotNull; + import org.jetbrains.annotations.Nullable; ++import top.leavesmc.leaves.entity.BotManager; + + /** + * Represents a server implementation. +@@ -2479,4 +2480,14 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi + */ + public boolean isGlobalTickThread(); + // Folia end - region threading API ++ ++ // Leaves start - Bot API ++ /** ++ * Returns a bot manager. ++ * ++ * @return Bot Manager ++ */ ++ @NotNull BotManager getBotManager(); ++ // Leaves end - Bot API ++ + } +diff --git a/src/main/java/top/leavesmc/leaves/entity/Bot.java b/src/main/java/top/leavesmc/leaves/entity/Bot.java +new file mode 100644 +index 0000000000000000000000000000000000000000..02815077dcd6d5c1b155ef3ca5cd6e3a9c45c0b5 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/entity/Bot.java +@@ -0,0 +1,51 @@ ++package top.leavesmc.leaves.entity; ++ ++import org.bukkit.entity.Player; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++import top.leavesmc.leaves.entity.botaction.LeavesBotAction; ++ ++import java.util.UUID; ++ ++/** ++ * Represents a fakeplayer ++ */ ++public interface Bot extends Player { ++ ++ /** ++ * Gets the fakeplayer skin ++ * ++ * @return fakeplayer skin name ++ */ ++ @Nullable ++ public String getSkinName(); ++ ++ /** ++ * Gets the fakeplayer name without prefix and suffix ++ * ++ * @return fakeplayer real name ++ */ ++ @NotNull ++ public String getRealName(); ++ ++ @Nullable ++ public UUID getCreatePlayerUUID(); ++ ++ /** ++ * Sets the fakeplayer action with args. ++ * ++ * @param action action name ++ * @param player player who create this action ++ * @param args passed action arguments ++ */ ++ public boolean setBotAction(@NotNull String action, @NotNull Player player, @NotNull String[] args); ++ ++ /** ++ * Sets the fakeplayer action with args. ++ * ++ * @param action leaves bot action ++ * @param player player who create this action ++ * @param args passed action arguments ++ */ ++ public boolean setBotAction(@NotNull LeavesBotAction action, @NotNull Player player, @NotNull String[] args); ++} +diff --git a/src/main/java/top/leavesmc/leaves/entity/BotManager.java b/src/main/java/top/leavesmc/leaves/entity/BotManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f2258027b2ed4bfcf7feda2e9075b1a1a05a85b6 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/entity/BotManager.java +@@ -0,0 +1,107 @@ ++package top.leavesmc.leaves.entity; ++ ++import org.bukkit.Location; ++import org.bukkit.util.Consumer; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++import top.leavesmc.leaves.entity.botaction.CustomBotAction; ++ ++import java.util.Collection; ++import java.util.UUID; ++ ++/** ++ * Simple fakeplayer manager ++ */ ++public interface BotManager { ++ ++ /** ++ * Gets a fakeplayer object by the given uuid. ++ * ++ * @param uuid the uuid to look up ++ * @return a fakeplayer if one was found, null otherwise ++ */ ++ @Nullable ++ public Bot getBot(@NotNull UUID uuid); ++ ++ /** ++ * Gets a fakeplayer object by the given name. ++ * ++ * @param name the name to look up ++ * @return a fakeplayer if one was found, null otherwise ++ */ ++ @Nullable ++ public Bot getBot(@NotNull String name); ++ ++ /** ++ * Creates a fakeplayer with given param. ++ *

++ * prefix and suffix will not be added. ++ * ++ * @param name fakeplayer name ++ * @param realName fakeplayer real name ++ * @param skin fakeplayer skin arr ++ * @param skinName fakeplayer skin name ++ * @param location a location will create fakeplayer ++ * @return a fakeplayer if success, null otherwise ++ */ ++ @Nullable ++ public Bot createBot(@NotNull String name, @NotNull String realName, @Nullable String[] skin, @Nullable String skinName, @NotNull Location location); ++ ++ /** ++ * Creates a fakeplayer with given param. ++ * ++ * @param name fakeplayer name ++ * @param skinName fakeplayer skin name ++ * @param location a location will create fakeplayer ++ * @param consumer a consumer after create fakeplayer success ++ */ ++ public void createBot(@NotNull String name, @Nullable String skinName, @NotNull Location location, @Nullable Consumer consumer); ++ ++ /** ++ * Removes a fakeplayer object by the given name. ++ * ++ * @param name the name to look up ++ */ ++ public void removeBot(@NotNull String name); ++ ++ /** ++ * Removes a fakeplayer object by the given uuid. ++ * ++ * @param uuid the uuid to look up ++ */ ++ public void removeBot(@NotNull UUID uuid); ++ ++ /** ++ * Removes all fakeplayers. ++ */ ++ public void removeAllBots(); ++ ++ /** ++ * Save fakeplayers data if resident-fakeplayer is true, or remove all fakeplayer. ++ */ ++ public void saveOrRemoveAllBots(); ++ ++ /** ++ * Gets a view of all currently logged in fakeplayers. This view is a reused object, making some operations like Collection.size() zero-allocation. ++ * ++ * @return a view of fakeplayers. ++ */ ++ public Collection getBots(); ++ ++ /** ++ * Register a custom bot action. ++ * ++ * @param name action name ++ * @param action action executor ++ * @return true if success, or false ++ */ ++ public boolean registerCustomBotAction(String name, CustomBotAction action); ++ ++ /** ++ * Unregister a custom bot action. ++ * ++ * @param name action name ++ * @return true if success, or false ++ */ ++ public boolean unregisterCustomBotAction(String name); ++} +diff --git a/src/main/java/top/leavesmc/leaves/entity/botaction/CustomBotAction.java b/src/main/java/top/leavesmc/leaves/entity/botaction/CustomBotAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7abf4eef22e40468929e724ebc07a97b0b2a05f3 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/entity/botaction/CustomBotAction.java +@@ -0,0 +1,52 @@ ++package top.leavesmc.leaves.entity.botaction; ++ ++import org.bukkit.entity.Player; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++import top.leavesmc.leaves.entity.Bot; ++ ++import java.util.List; ++ ++/** ++ * Represents a class which contains methods for a custom bot action ++ */ ++public interface CustomBotAction { ++ ++ /** ++ * Executes the action, returning its success. ++ * ++ * @param bot bot of the action ++ * @return true if once action finish, otherwise false ++ */ ++ public boolean doTick(Bot bot); ++ ++ /** ++ * Created a new action instance. ++ * ++ * @param player player who create this action ++ * @param args passed action arguments ++ * @return a new action instance with given args ++ */ ++ public @Nullable CustomBotAction getNew(Player player, String[] args); ++ ++ /** ++ * Requests a list of possible completions for a action argument. ++ * ++ * @return A List of a List of possible completions for the argument. ++ */ ++ public @NotNull List> getTabComplete(); ++ ++ /** ++ * Return a ticks to wait between {@link CustomBotAction#doTick(Bot)} ++ * ++ * @return the ticks to wait between runs ++ */ ++ public int getTickDelay(); ++ ++ /** ++ * Return a number of times {@link CustomBotAction#doTick(Bot)} can return true ++ * ++ * @return the number of times an action can be executed ++ */ ++ public int getNumber(); ++} +diff --git a/src/main/java/top/leavesmc/leaves/entity/botaction/LeavesBotAction.java b/src/main/java/top/leavesmc/leaves/entity/botaction/LeavesBotAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e298722319ff0cfd52e531693ea3767e5f9a3d52 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/entity/botaction/LeavesBotAction.java +@@ -0,0 +1,32 @@ ++package top.leavesmc.leaves.entity.botaction; ++ ++/** ++ * A Leaves bot action enum ++ */ ++public enum LeavesBotAction { ++ ATTACK("attack"), ++ ATTACK_SELF("attack_self"), ++ BREAK("break"), ++ DROP("drop"), ++ FISH("fish"), ++ JUMP("jump"), ++ LAY("lay"), ++ LOOK("look"), ++ ROTATE("rotate"), ++ SNEAK("sneak"), ++ STOP("stop"), ++ SWIM("swim"), ++ USE("use"), ++ USE_ON("use_on"), ++ USE_TO("use_to"); ++ ++ private final String name; ++ ++ private LeavesBotAction(String name) { ++ this.name = name; ++ } ++ ++ public String getName() { ++ return name; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/event/bot/BotCreateEvent.java b/src/main/java/top/leavesmc/leaves/event/bot/BotCreateEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7cf1eb4eb3d2fe9310f9272ec53208632b87b49b +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/event/bot/BotCreateEvent.java +@@ -0,0 +1,106 @@ ++package top.leavesmc.leaves.event.bot; ++ ++import org.bukkit.Location; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.Event; ++import org.bukkit.event.HandlerList; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++/** ++ * Call when a fakeplayer creates a server ++ */ ++public class BotCreateEvent extends Event implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ ++ private final String bot; ++ private final String skin; ++ private String joinMessage; ++ private Location createLocation; ++ private boolean cancel = false; ++ ++ public BotCreateEvent(@NotNull final String who, @NotNull final String skin, @NotNull final Location createLocation, @Nullable final String joinMessage) { ++ this.bot = who; ++ this.skin = skin; ++ this.joinMessage = joinMessage; ++ this.createLocation = createLocation; ++ } ++ ++ /** ++ * Gets the fakeplayer name ++ * ++ * @return fakeplayer name ++ */ ++ public String getBot() { ++ return bot; ++ } ++ ++ /** ++ * Gets the join message to send to all online players ++ * ++ * @return string join message. Can be null ++ */ ++ @Nullable ++ public String getJoinMessage() { ++ return joinMessage; ++ } ++ ++ /** ++ * Sets the join message to send to all online players ++ * ++ * @param joinMessage join message. If null, no message will be sent ++ */ ++ public void setJoinMessage(@Nullable String joinMessage) { ++ this.joinMessage = joinMessage; ++ } ++ ++ /** ++ * Gets the location to create the fakeplayer ++ * ++ * @return Location to create the fakeplayer ++ */ ++ @NotNull ++ public Location getCreateLocation() { ++ return createLocation; ++ } ++ ++ /** ++ * Sets the location to create the fakeplayer ++ * ++ * @param createLocation location to create the fakeplayer ++ */ ++ public void setCreateLocation(@NotNull Location createLocation) { ++ this.createLocation = createLocation; ++ } ++ ++ /** ++ * Gets the fakeplayer skin ++ * ++ * @return fakeplayer skin name ++ */ ++ @Nullable ++ public String getSkin() { ++ return skin; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return cancel; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancel = cancel; ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/event/bot/BotEvent.java b/src/main/java/top/leavesmc/leaves/event/bot/BotEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4a4fe07ce965d4a97e0d8105a91310dac7d1343c +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/event/bot/BotEvent.java +@@ -0,0 +1,31 @@ ++package top.leavesmc.leaves.event.bot; ++ ++import org.bukkit.event.Event; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.entity.Bot; ++ ++/** ++ * Represents a fakeplayer related event ++ */ ++public abstract class BotEvent extends Event { ++ protected Bot bot; ++ ++ public BotEvent(@NotNull final Bot who) { ++ bot = who; ++ } ++ ++ public BotEvent(@NotNull final Bot who, boolean async) { // Paper - public ++ super(async); ++ bot = who; ++ } ++ ++ /** ++ * Returns the fakeplayer involved in this event ++ * ++ * @return Fakeplayer who is involved in this event ++ */ ++ @NotNull ++ public final Bot getBot() { ++ return bot; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/event/bot/BotInventoryOpenEvent.java b/src/main/java/top/leavesmc/leaves/event/bot/BotInventoryOpenEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e889f7e98ec9cb6a3b95d8ea865e25fffea662a1 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/event/bot/BotInventoryOpenEvent.java +@@ -0,0 +1,46 @@ ++package top.leavesmc.leaves.event.bot; ++ ++import org.bukkit.entity.Player; ++import org.bukkit.event.Cancellable; ++import org.bukkit.event.HandlerList; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++import top.leavesmc.leaves.entity.Bot; ++ ++public class BotInventoryOpenEvent extends BotEvent implements Cancellable { ++ private static final HandlerList handlers = new HandlerList(); ++ ++ private final Player player; ++ private boolean cancel = false; ++ ++ public BotInventoryOpenEvent(@NotNull Bot who, @Nullable Player player) { ++ super(who); ++ this.player = player; ++ } ++ ++ @Override ++ public boolean isCancelled() { ++ return cancel; ++ } ++ ++ @Override ++ public void setCancelled(boolean cancel) { ++ this.cancel = cancel; ++ } ++ ++ @Nullable ++ public Player getOpenPlayer() { ++ return player; ++ } ++ ++ ++ @Override ++ public @NotNull HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/event/bot/BotJoinEvent.java b/src/main/java/top/leavesmc/leaves/event/bot/BotJoinEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..10afa5c7fd4ee8a4e72d64f8ca9bf8731ec2ad61 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/event/bot/BotJoinEvent.java +@@ -0,0 +1,27 @@ ++package top.leavesmc.leaves.event.bot; ++ ++import org.bukkit.event.HandlerList; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.entity.Bot; ++ ++/** ++ * Called when a fakeplayer joins a server ++ */ ++public class BotJoinEvent extends BotEvent { ++ private static final HandlerList handlers = new HandlerList(); ++ ++ public BotJoinEvent(@NotNull Bot who) { ++ super(who); ++ } ++ ++ @Override ++ @NotNull ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} diff --git a/patches/server/0030-Pufferfish-Dynamic-Activation-of-Brain.patch b/patches/server/0030-Pufferfish-Dynamic-Activation-of-Brain.patch index 2a6a7eb..5517e33 100644 --- a/patches/server/0030-Pufferfish-Dynamic-Activation-of-Brain.patch +++ b/patches/server/0030-Pufferfish-Dynamic-Activation-of-Brain.patch @@ -46,7 +46,7 @@ index 11c1a367fbc25cb63738a00ad93fb0b0b3500e7d..4f6af1fa55047e7be9e57c1dd1c60e9d } + private static void initDAB(){ -+ dearEnabled = get("optimizations.dab.enabled", false); ++ dearEnabled = get("optimizations.dab.enabled", true); + startDistance = get("optimizations.dab.start-distance", 12, + "This value determines how far away an entity has to be\n"+ + "from the player to start being effected by DEAR."); diff --git a/patches/server/0060-Leaves-Command-Utilities.patch b/patches/server/0060-Leaves-Command-Utilities.patch new file mode 100644 index 0000000..69217cc --- /dev/null +++ b/patches/server/0060-Leaves-Command-Utilities.patch @@ -0,0 +1,166 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Mon, 29 Jan 2024 12:47:41 +0000 +Subject: [PATCH] Leaves Command Utilities + + +diff --git a/src/main/java/top/leavesmc/leaves/command/CommandArgument.java b/src/main/java/top/leavesmc/leaves/command/CommandArgument.java +new file mode 100644 +index 0000000000000000000000000000000000000000..eadc6d168fb13299348b0c275ae352ee2f1e1ea2 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/command/CommandArgument.java +@@ -0,0 +1,43 @@ ++package top.leavesmc.leaves.command; ++ ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.List; ++ ++public class CommandArgument { ++ ++ private final List> argumentTypes; ++ private final List> tabComplete; ++ ++ public CommandArgument(CommandArgumentType... argumentTypes) { ++ this.argumentTypes = List.of(argumentTypes); ++ this.tabComplete = new ArrayList<>(); ++ for (int i = 0; i < argumentTypes.length; i++) { ++ tabComplete.add(new ArrayList<>()); ++ } ++ } ++ ++ public List tabComplete(int n) { ++ if (tabComplete.size() > n) { ++ return tabComplete.get(n); ++ } else { ++ return List.of(); ++ } ++ } ++ ++ public CommandArgument setTabComplete(int index, List list) { ++ tabComplete.set(index, list); ++ return this; ++ } ++ ++ public CommandArgumentResult parse(int index, String @NotNull [] args) { ++ Object[] result = new Object[argumentTypes.size()]; ++ Arrays.fill(result, null); ++ for (int i = index, j = 0; i < args.length && j < result.length; i++, j++) { ++ result[j] = argumentTypes.get(j).pasre(args[i]); ++ } ++ return new CommandArgumentResult(new ArrayList<>(Arrays.asList(result))); ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/command/CommandArgumentResult.java b/src/main/java/top/leavesmc/leaves/command/CommandArgumentResult.java +new file mode 100644 +index 0000000000000000000000000000000000000000..340eaca64c96180b895a075ce9e44402cd104eed +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/command/CommandArgumentResult.java +@@ -0,0 +1,62 @@ ++package top.leavesmc.leaves.command; ++ ++import net.minecraft.core.BlockPos; ++import org.bukkit.util.Vector; ++ ++import java.util.List; ++import java.util.Objects; ++ ++public class CommandArgumentResult { ++ ++ private final List result; ++ ++ public CommandArgumentResult(List result) { ++ this.result = result; ++ } ++ ++ public Integer readInt(int def) { ++ return Objects.requireNonNullElse(read(Integer.class), def); ++ } ++ ++ public Double readDouble(double def) { ++ return Objects.requireNonNullElse(read(Double.class), def); ++ } ++ ++ public String readString(String def) { ++ return Objects.requireNonNullElse(read(String.class), def); ++ } ++ ++ public BlockPos readPos() { ++ Integer[] pos = {read(Integer.class), read(Integer.class), read(Integer.class)}; ++ for (Integer po : pos) { ++ if (po == null) { ++ return null; ++ } ++ } ++ return new BlockPos(pos[0], pos[1], pos[2]); ++ } ++ ++ public Vector readVector() { ++ Double[] pos = {read(Double.class), read(Double.class), read(Double.class)}; ++ for (Double po : pos) { ++ if (po == null) { ++ return null; ++ } ++ } ++ return new Vector(pos[0], pos[1], pos[2]); ++ } ++ ++ public T read(Class tClass) { ++ if (result.isEmpty()) { ++ return null; ++ } ++ ++ Object obj = result.remove(0); ++ if (tClass.isInstance(obj)) { ++ return tClass.cast(obj); ++ } else { ++ return null; ++ } ++ } ++ ++} +diff --git a/src/main/java/top/leavesmc/leaves/command/CommandArgumentType.java b/src/main/java/top/leavesmc/leaves/command/CommandArgumentType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..edf12195c7224ca2fb5d3c2ac3fcf485d3049d07 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/command/CommandArgumentType.java +@@ -0,0 +1,37 @@ ++package top.leavesmc.leaves.command; ++ ++import org.jetbrains.annotations.NotNull; ++ ++public abstract class CommandArgumentType { ++ ++ public static final CommandArgumentType INTEGER = new CommandArgumentType<>() { ++ @Override ++ public Integer pasre(@NotNull String arg) { ++ try { ++ return Integer.parseInt(arg); ++ } catch (NumberFormatException e) { ++ return null; ++ } ++ } ++ }; ++ ++ public static final CommandArgumentType DOUBLE = new CommandArgumentType<>() { ++ @Override ++ public Double pasre(@NotNull String arg) { ++ try { ++ return Double.parseDouble(arg); ++ } catch (NumberFormatException e) { ++ return null; ++ } ++ } ++ }; ++ ++ public static final CommandArgumentType STRING = new CommandArgumentType<>() { ++ @Override ++ public String pasre(@NotNull String arg) { ++ return arg; ++ } ++ }; ++ ++ public abstract E pasre(@NotNull String arg); ++} diff --git a/patches/server/0061-Leaves-fakeplayer-support.patch b/patches/server/0061-Leaves-fakeplayer-support.patch new file mode 100644 index 0000000..5fc2e9b --- /dev/null +++ b/patches/server/0061-Leaves-fakeplayer-support.patch @@ -0,0 +1,3368 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Mon, 29 Jan 2024 13:22:53 +0000 +Subject: [PATCH] Leaves fakeplayer support + + +diff --git a/src/main/java/me/earthme/luminol/LuminolConfig.java b/src/main/java/me/earthme/luminol/LuminolConfig.java +index e531453e3d25700e38ff41548c7e32e278a6304d..f924a238f211c22a00b8df67a209dcfea751039c 100644 +--- a/src/main/java/me/earthme/luminol/LuminolConfig.java ++++ b/src/main/java/me/earthme/luminol/LuminolConfig.java +@@ -78,7 +78,17 @@ public class LuminolConfig { + public static boolean syncmaticaProtocol = false; + public static boolean syncmaticaQuota = false; + public static int syncmaticaQuotaLimit = 32767; +- ++ public static boolean fakeplayerSupport = false; ++ public static boolean fakeplayerSkipSleep = false; ++ public static boolean fakeplayerSpawnPhantom = true; ++ public static boolean alwaysSendFakeplayerData = true; ++ public static int fakeplayerLimit = 20; ++ public static List unableFakeplayerNames = List.of("player-name"); ++ public static String fakeplayerPrefix = ""; ++ public static String fakeplayerSuffix = ""; ++ public static boolean fakeplayerResident = false; ++ public static boolean openFakeplayerInventory = false; ++ public static double fakeplayerRegenAmount = 0.010; + + + public static void init() throws IOException { +@@ -95,6 +105,23 @@ public class LuminolConfig { + MAIN_CONFIG.save(); + } + ++ public static void initFakePlayer(){ ++ fakeplayerSupport = get("gameplay.fakeplayer.enabled",fakeplayerSupport); ++ fakeplayerLimit = get("gameplay.fakeplayer.max_count",fakeplayerLimit); ++ fakeplayerPrefix = get("gameplay.fakeplayer.name_prefix",fakeplayerPrefix); ++ fakeplayerSuffix = get("gameplay.fakeplayer.name_suffix",fakeplayerSuffix); ++ fakeplayerSkipSleep = get("gameplay.fakeplayer.fakeplayer_skip_sleep",fakeplayerSkipSleep); ++ fakeplayerResident = get("gameplay.fakeplayer.resident",fakeplayerResident); ++ fakeplayerSpawnPhantom = get("gameplay.fakeplayer.spawn_phantom_for_fakeplayer",fakeplayerSpawnPhantom); ++ fakeplayerRegenAmount = get("gameplay.fakeplayer.regen_amount",fakeplayerRegenAmount); ++ unableFakeplayerNames = get("gameplay.fakeplayer.name_black_list",unableFakeplayerNames); ++ ++ if (fakeplayerSupport){ ++ MinecraftServer.getServer().server.getCommandMap().register("bot", "Leaves", new top.leavesmc.leaves.bot.BotCommand("bot")); ++ top.leavesmc.leaves.bot.agent.Actions.registerAll(); ++ } ++ } ++ + public static void initTpsbar(){ + if (tpsbarEnabled){ + GlobalServerTpsBar.init(); +@@ -222,6 +249,7 @@ public class LuminolConfig { + syncmaticaProtocol = get("gameplay.syncmatica_protocol",syncmaticaProtocol); + syncmaticaQuota = get("gameplay.syncmatica_protocol_quota",syncmaticaQuota); + syncmaticaQuotaLimit = get("gameplay.syncmatica_protocol_quota_limit",syncmaticaQuotaLimit); ++ initFakePlayer(); + } + + public static T get(String key,T def){ +diff --git a/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java b/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java +index f0367a9cce13ef576fbb7023c0aba6eb48963606..7b156d147c409ded1b0385583e0b34d3bd5c48de 100644 +--- a/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java ++++ b/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java +@@ -51,6 +51,7 @@ public abstract class SimpleCriterionTrigger predicate) { ++ if (player instanceof top.leavesmc.leaves.bot.ServerBot) return; // Leaves - bot skip + PlayerAdvancements playerAdvancements = player.getAdvancements(); + Set> set = (Set) playerAdvancements.criterionData.get(this); // Paper - fix AdvancementDataPlayer leak + if (set != null && !set.isEmpty()) { +diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java +index b84e9d5ae5efe1a8257a5f1f78a0ab66113a698e..832f6851a3f72cefe95c96757645374ac592f33b 100644 +--- a/src/main/java/net/minecraft/network/Connection.java ++++ b/src/main/java/net/minecraft/network/Connection.java +@@ -365,6 +365,14 @@ public class Connection extends SimpleChannelInboundHandler> { + } + } + ++ // Leaves start - fakeplayer ++ public void setListenerForce(PacketListener packetListener) { ++ Validate.notNull(packetListener, "packetListener"); ++ this.packetListener = packetListener; ++ this.disconnectListener = null; ++ } ++ // Leaves end - fakeplayer ++ + public void setListenerForServerboundHandshake(PacketListener packetListener) { + if (this.packetListener != null) { + throw new IllegalStateException("Listener already set"); +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 675bbf1f69011f7f95fabc050c9877d6e70070b2..926b80aa09b0a593f4074efc86601ac176692c72 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -669,6 +669,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop loot = new java.util.ArrayList(this.getInventory().getContainerSize()); + boolean keepInventory = this.level().getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY) || this.isSpectator(); + +- if (!keepInventory) { ++ if (!keepInventory || this instanceof ServerBot) { // Leaves - skip bot + for (ItemStack item : this.getInventory().getContents()) { + if (!item.isEmpty() && !EnchantmentHelper.hasVanishingCurse(item)) { + loot.add(CraftItemStack.asCraftMirror(item)); +@@ -1730,6 +1735,13 @@ public class ServerPlayer extends Player { + this.lastSentHealth = -1.0F; + this.lastSentFood = -1; + ++ // Leaves start - bot support ++ if (me.earthme.luminol.LuminolConfig.fakeplayerSupport) { ++ ServerBot.getBots().forEach(bot1 -> ++ bot1.sendFakeDataIfNeed(this, true)); // Leaves - render bot ++ } ++ // Leaves end - bot support ++ + // CraftBukkit start + PlayerChangedWorldEvent changeEvent = new PlayerChangedWorldEvent(this.getBukkitEntity(), worldserver1.getWorld()); + this.level().getCraftServer().getPluginManager().callEvent(changeEvent); +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index c4652f250e22f33e8505183b4050e08aedf9245c..18745ee33843d668613fd9e7f15d6a79f118280f 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -300,7 +300,13 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.lastSeenMessages = new LastSeenMessagesValidator(20); + this.messageSignatureCache = MessageSignatureCache.createDefault(); + this.chunkSender = new PlayerChunkSender(connection.isMemoryConnection()); +- connection.setListener(this); ++ // Leaves start - fakeplayer ++ if (player instanceof top.leavesmc.leaves.bot.ServerBot) { ++ connection.setListenerForce(this); ++ } else { ++ connection.setListener(this); ++ } ++ // Leaves end - fakeplayer + this.player = player; + player.connection = this; + player.getTextFilter().join(); +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 3f366ad035ada0ac8170d2925bbfe86b80bd84cb..f2329d24a4e3f4637b78d4894b0e6c99a0e00faa 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -123,6 +123,8 @@ import org.bukkit.event.player.PlayerRespawnEvent.RespawnReason; + import org.bukkit.event.player.PlayerSpawnChangeEvent; + // CraftBukkit end + ++import top.leavesmc.leaves.bot.ServerBot; ++ + public abstract class PlayerList { + + public static final File USERBANLIST_FILE = new File("banned-players.json"); +@@ -437,6 +439,21 @@ public abstract class PlayerList { + + top.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol + ++ // Leaves start - bot support ++ if (me.earthme.luminol.LuminolConfig.fakeplayerSupport) { ++ ServerBot bot = ServerBot.getBot(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); ++ if (bot != null) { ++ bot.die(bot.damageSources().fellOutOfWorld()); // Leaves - remove bot with the same name ++ this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); ++ this.playersByUUID.put(player.getUUID(), player); ++ } ++ ServerBot.getBots().forEach(bot1 -> { ++ bot1.sendPlayerInfo(player); ++ bot1.sendFakeDataIfNeed(player, true); ++ }); // Leaves - render bot ++ } ++ // Leaves end - bot support ++ + final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); + + if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure +@@ -1092,6 +1109,13 @@ public abstract class PlayerList { + } + // Paper end + ++ // Leaves start - bot support ++ if (me.earthme.luminol.LuminolConfig.fakeplayerSupport) { ++ ServerBot.getBots().forEach(bot1 -> ++ bot1.sendFakeDataIfNeed(entityplayer1, true)); // Leaves - render bot ++ } ++ // Leaves end - bot support ++ + // CraftBukkit end + return entityplayer1; + } +@@ -1201,12 +1225,17 @@ public abstract class PlayerList { + + public String[] getPlayerNamesArray() { + List players = new java.util.ArrayList<>(this.players); // Folia start - region threading +- String[] astring = new String[players.size()]; ++ String[] astring = new String[players.size() + ServerBot.getBots().size()]; + + for (int i = 0; i < players.size(); ++i) { + astring[i] = ((ServerPlayer) players.get(i)).getGameProfile().getName(); + // Folia end - region threading + } ++ // Leaves start - fakeplayer support ++ for (int i = this.players.size(); i < astring.length; ++i) { ++ astring[i] = ((ServerPlayer) ServerBot.getBots().get(i - this.players.size())).getGameProfile().getName(); ++ } ++ // Leaves end - fakeplayer support + + return astring; + } +@@ -1715,4 +1744,17 @@ public abstract class PlayerList { + public boolean isAllowCheatsForAllPlayers() { + return this.allowCheatsForAllPlayers; + } ++ ++ ++ // Leaves start - fakeplayer support ++ public void addNewBot(ServerBot bot) { ++ playersByName.put(bot.getScoreboardName().toLowerCase(java.util.Locale.ROOT), bot); ++ playersByUUID.put(bot.getUUID(), bot); ++ } ++ ++ public void removeBot(ServerBot bot) { ++ playersByName.remove(bot.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); ++ playersByUUID.remove(bot.getUUID()); ++ } ++ // Leaves end - fakeplayer support + } +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index ec255db477e6308cc0fbe336b7bebeac75b97b2a..e24b55dc96c2e3904e3c03d6bfa769aedf767b3b 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -1498,7 +1498,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return offsetFactor; + } + +- private Vec3 collide(Vec3 movement) { ++ public Vec3 collide(Vec3 movement) { // Leaves - private -> public + // Paper start - optimise collisions + final boolean xZero = movement.x == 0.0; + final boolean yZero = movement.y == 0.0; +diff --git a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java +index 762376c2ab61200681fd5e5732337530285e03fb..4112edf568b2d9836d3ecba54318d76dd4a6c94c 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java +@@ -61,7 +61,7 @@ public class FishingHook extends Projectile { + public static final EntityDataAccessor DATA_HOOKED_ENTITY = SynchedEntityData.defineId(FishingHook.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_BITING = SynchedEntityData.defineId(FishingHook.class, EntityDataSerializers.BOOLEAN); + private int life; +- private int nibble; ++ public int nibble; // Leaves - private -> public + public int timeUntilLured; + private int timeUntilHooked; + private float fishAngle; +diff --git a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java +index f664da5a8413bb13cc95d2cf1604f11a5d285dae..2d5f6a69e0c2dc96375ec24d41522ae4e77c6744 100644 +--- a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java +@@ -406,6 +406,8 @@ public abstract class AbstractContainerMenu { + ItemStack itemstack1; + int l; + ++ if (!doClickCheck(slotIndex, button, actionType, player)) return; // Leaves - doClick check ++ + if (actionType == ClickType.QUICK_CRAFT) { + int i1 = this.quickcraftStatus; + +@@ -676,6 +678,23 @@ public abstract class AbstractContainerMenu { + + } + ++ // Leaves start - doClick check ++ private boolean doClickCheck(int slotIndex, int button, ClickType actionType, Player player) { ++ if (slotIndex < 0) { ++ return true; ++ } ++ ++ Slot slot = getSlot(slotIndex); ++ ItemStack itemStack = slot.getItem(); ++ if (itemStack.getTag() != null) { ++ if (itemStack.getTag().get("Leaves.Gui.Placeholder") != null) { ++ return !itemStack.getTag().getBoolean("Leaves.Gui.Placeholder"); ++ } ++ } ++ return true; ++ } ++ // Leaves end - doClick check ++ + private boolean tryItemClickBehaviourOverride(Player player, ClickAction clickType, Slot slot, ItemStack stack, ItemStack cursorStack) { + FeatureFlagSet featureflagset = player.level().enabledFeatures(); + +diff --git a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +index a127b6cbaab211d324d42b3bddcd6ebd84c250e3..2649177134985505a341c40473dfdbb210d0ae87 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +@@ -70,6 +70,11 @@ public class PhantomSpawner implements CustomSpawner { + ServerStatsCounter serverstatisticmanager = entityplayer.getStats(); + int j = Mth.clamp(serverstatisticmanager.getValue(Stats.CUSTOM.get(Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE); + boolean flag2 = true; ++ // Leaves start - fakeplayer spawn ++ if (entityplayer instanceof top.leavesmc.leaves.bot.ServerBot bot && bot.spawnPhantom) { ++ j = bot.notSleepTicks; ++ } ++ // Leaves end - fakeplayer spawn + + if (randomsource.nextInt(j) >= world.paperConfig().entities.behavior.playerInsomniaStartTicks) { // Paper + BlockPos blockposition1 = blockposition.above(20 + randomsource.nextInt(15)).east(-10 + randomsource.nextInt(21)).south(-10 + randomsource.nextInt(21)); +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index ee66241a422acb8920395fd5e41578a966746f5c..913ea63d7103d6a54990b55683d4cf0341d4b722 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -264,6 +264,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; + import org.yaml.snakeyaml.error.MarkedYAMLException; + + import net.md_5.bungee.api.chat.BaseComponent; // Spigot ++import top.leavesmc.leaves.entity.CraftBotManager; + + import javax.annotation.Nullable; // Paper + import javax.annotation.Nonnull; // Paper +@@ -309,6 +310,7 @@ public final class CraftServer implements Server { + public static Exception excessiveVelEx; // Paper - Velocity warnings + private final io.papermc.paper.logging.SysoutCatcher sysoutCatcher = new io.papermc.paper.logging.SysoutCatcher(); // Paper + private final CraftPotionBrewer potionBrewer = new CraftPotionBrewer(); // Paper ++ private final CraftBotManager botManager = new CraftBotManager(); + + // Paper start - Folia region threading API + private final io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler(); // Folia - region threading +@@ -3241,4 +3243,12 @@ public final class CraftServer implements Server { + } + + // Paper end ++ ++ ++ //Leaves start - Bot API ++ @Override ++ public CraftBotManager getBotManager() { ++ return botManager; ++ } ++ // Leaves end - Bot API + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index 9f070a9f0894d88688ff50efc3f4dba8188c3885..5699adccb5c2e5e7e6612940cf42fcf9c7a38dba 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -194,6 +194,8 @@ import org.bukkit.plugin.Plugin; + import org.bukkit.util.BoundingBox; + import org.bukkit.util.NumberConversions; + import org.bukkit.util.Vector; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.entity.CraftBot; + + import net.md_5.bungee.api.chat.BaseComponent; // Spigot + +@@ -231,7 +233,12 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + if (entity instanceof LivingEntity) { + // Players + if (entity instanceof net.minecraft.world.entity.player.Player) { +- if (entity instanceof ServerPlayer) { return new CraftPlayer(server, (ServerPlayer) entity); } ++ // Leaves start - add CraftBot ++ if (entity instanceof ServerPlayer) { ++ if (entity instanceof ServerBot) { return new CraftBot(server, (ServerBot) entity); } ++ else { return new CraftPlayer(server, (ServerPlayer) entity); } ++ } ++ // Leaves end - add CraftBot + else { return new CraftHumanEntity(server, (net.minecraft.world.entity.player.Player) entity); } + } + // Water Animals +diff --git a/src/main/java/top/leavesmc/leaves/bot/BotCommand.java b/src/main/java/top/leavesmc/leaves/bot/BotCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4563284facbfd01733d6a0efdfe7d0cabebdc1e7 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/BotCommand.java +@@ -0,0 +1,330 @@ ++package top.leavesmc.leaves.bot; ++ ++import org.bukkit.Bukkit; ++import org.bukkit.ChatColor; ++import org.bukkit.Location; ++import org.bukkit.World; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.command.ConsoleCommandSender; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.bukkit.entity.Player; ++import org.bukkit.generator.WorldInfo; ++import org.bukkit.permissions.Permission; ++import org.bukkit.permissions.PermissionDefault; ++import org.bukkit.plugin.PluginManager; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.agent.Actions; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.bot.agent.actions.CraftCustomBotAction; ++import top.leavesmc.leaves.entity.Bot; ++ ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++ ++public class BotCommand extends Command { ++ ++ public BotCommand(String name) { ++ super(name); ++ this.description = "FakePlayer Command"; ++ this.usageMessage = "/bot [create | remove | action | list]"; ++ this.setPermission("bukkit.command.bot"); ++ final PluginManager pluginManager = Bukkit.getServer().getPluginManager(); ++ if (pluginManager.getPermission("bukkit.command.bot") == null) { ++ pluginManager.addPermission(new Permission("bukkit.command.bot", PermissionDefault.OP)); ++ } ++ } ++ ++ @Override ++ public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, String @NotNull [] args, Location location) throws IllegalArgumentException { ++ var list = new ArrayList(); ++ ++ if (args.length <= 1) { ++ list.add("create"); ++ list.add("remove"); ++ list.add("action"); ++ list.add("config"); ++ list.add("list"); ++ } ++ ++ if (args.length == 2) { ++ switch (args[0]) { ++ case "create" -> list.add(""); ++ case "remove", "action", "config" -> ++ list.addAll(ServerBot.getBots().stream().map(e -> e.getName().getString()).toList()); ++ case "list" -> list.addAll(Bukkit.getWorlds().stream().map(WorldInfo::getName).toList()); ++ } ++ } ++ ++ if (args.length == 3) { ++ switch (args[0]) { ++ case "action" -> { ++ list.add("list"); ++ list.addAll(Actions.getNames()); ++ } ++ case "create" -> list.add(""); ++ case "config" -> list.addAll(acceptConfig); ++ } ++ } ++ ++ if (args.length == 4) { ++ switch (args[0]) { ++ case "config" -> { ++ list.add("true"); ++ list.add("false"); ++ } ++ } ++ } ++ ++ if (args.length >= 4 && args[0].equals("action")) { ++ BotAction action = Actions.getForName(args[2]); ++ if (action != null) { ++ list.addAll(action.getArgument().tabComplete(args.length - 4)); ++ } ++ } ++ ++ return list; ++ } ++ ++ @Override ++ public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, String[] args) { ++ if (!testPermission(sender) || !me.earthme.luminol.LuminolConfig.fakeplayerSupport) return true; ++ ++ if (args.length == 0) { ++ sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); ++ return false; ++ } ++ ++ switch (args[0]) { ++ case "create" -> this.onCreate(sender, args); ++ ++ case "remove" -> this.onRemove(sender, args); ++ ++ case "action" -> this.onAction(sender, args); ++ ++ case "config" -> this.onConfig(sender, args); ++ ++ case "list" -> this.onList(sender, args); ++ ++ default -> { ++ sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); ++ return false; ++ } ++ } ++ ++ return true; ++ } ++ ++ private void onCreate(CommandSender sender, String @NotNull [] args) { ++ if (args.length < 2) { ++ sender.sendMessage(ChatColor.RED + "Use /bot create [skin_name] to create a fakeplayer"); ++ return; ++ } ++ ++ if (canCreate(sender, args[1])) { ++ if (sender instanceof Player player) { ++ new ServerBot.BotCreateState(player.getLocation(), args[1], args.length < 3 ? args[1] : args[2]).createAsync(bot -> bot.createPlayer = player.getUniqueId()); ++ } else if (sender instanceof ConsoleCommandSender) { ++ if (args.length < 6) { ++ sender.sendMessage(ChatColor.RED + "Use /bot create to create a fakeplayer"); ++ return; ++ } ++ ++ try { ++ World world = Bukkit.getWorld(args[3]); ++ double x = Double.parseDouble(args[4]); ++ double y = Double.parseDouble(args[5]); ++ double z = Double.parseDouble(args[6]); ++ ++ if (world != null) { ++ new ServerBot.BotCreateState(new Location(world, x, y, z), args[1], args[2]).createAsync(null); ++ } ++ } catch (Exception e) { ++ e.printStackTrace(); ++ } ++ } ++ } ++ } ++ ++ private boolean canCreate(CommandSender sender, @NotNull String name) { ++ if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) { ++ sender.sendMessage(ChatColor.RED + "This name is illegal"); ++ return false; ++ } ++ ++ if (Bukkit.getPlayer(name) != null || ServerBot.getBot(name) != null) { ++ sender.sendMessage(ChatColor.RED + "This player is in server"); ++ return false; ++ } ++ ++ if (me.earthme.luminol.LuminolConfig.unableFakeplayerNames.contains(name)) { ++ sender.sendMessage(ChatColor.RED + "This name is not allowed"); ++ return false; ++ } ++ ++ if (ServerBot.getBots().size() >= me.earthme.luminol.LuminolConfig.fakeplayerLimit) { ++ sender.sendMessage(ChatColor.RED + "Fakeplayer limit is full"); ++ return false; ++ } ++ ++ return true; ++ } ++ ++ private void onRemove(CommandSender sender, String @NotNull [] args) { ++ if (args.length < 2) { ++ sender.sendMessage(ChatColor.RED + "Use /bot remove to remove a fakeplayer"); ++ return; ++ } ++ ++ ServerBot bot = ServerBot.getBot(args[1]); ++ ++ if (bot == null) { ++ sender.sendMessage(ChatColor.RED + "This fakeplayer is not in server"); ++ return; ++ } ++ ++ bot.die(bot.damageSources().fellOutOfWorld()); ++ } ++ ++ private void onAction(CommandSender sender, String @NotNull [] args) { ++ if (args.length < 3) { ++ sender.sendMessage(ChatColor.RED + "Use /bot action to make fakeplayer do action"); ++ return; ++ } ++ ++ ServerBot bot = ServerBot.getBot(args[1]); ++ if (bot == null) { ++ sender.sendMessage(ChatColor.RED + "This fakeplayer is not in server"); ++ return; ++ } ++ ++ if (args[2].equals("list")) { ++ sender.sendMessage(bot.getScoreboardName() + "'s action list:"); ++ for (BotAction action : bot.getBotActions()) { ++ sender.sendMessage(action.getName()); ++ } ++ return; ++ } ++ ++ BotAction action = Actions.getForName(args[2]); ++ if (action == null) { ++ sender.sendMessage(ChatColor.RED + "Invalid action"); ++ return; ++ } ++ ++ CraftPlayer player; ++ if (sender instanceof CraftPlayer) { ++ player = (CraftPlayer) sender; ++ } else { ++ player = bot.getBukkitEntity(); ++ } ++ ++ BotAction newAction; ++ if (action instanceof CraftCustomBotAction customBotAction) { ++ String[] realArgs = new String[args.length - 3]; ++ if (realArgs.length != 0) { ++ System.arraycopy(args, 3, realArgs, 0, realArgs.length); ++ } ++ newAction = customBotAction.getNew(player, realArgs); ++ } else { ++ newAction = action.getNew(player.getHandle(), action.getArgument().parse(3, args)); ++ } ++ ++ if (newAction == null) { ++ sender.sendMessage(ChatColor.RED + "Action create error, please check your arguments"); ++ return; ++ } ++ ++ bot.setBotAction(newAction); ++ sender.sendMessage("Action " + action.getName() + " has been issued to " + bot.getName().getString()); ++ } ++ ++ private static final List acceptConfig = List.of("skip_sleep", "spawn_phantom", "always_send_data"); ++ ++ private void onConfig(CommandSender sender, String @NotNull [] args) { ++ if (args.length < 3) { ++ sender.sendMessage(ChatColor.RED + "Use /bot config to modify fakeplayer's config"); ++ return; ++ } ++ ++ ServerBot bot = ServerBot.getBot(args[1]); ++ if (bot == null) { ++ sender.sendMessage(ChatColor.RED + "This fakeplayer is not in server"); ++ return; ++ } ++ ++ if (!acceptConfig.contains(args[2])) { ++ sender.sendMessage(ChatColor.RED + "This config is not accept"); ++ return; ++ } ++ ++ if (args.length < 4) { ++ switch (args[2]) { ++ case "skip_sleep" -> sender.sendMessage(bot.getScoreboardName() + "'s skip_sleep: " + bot.fauxSleeping); ++ case "spawn_phantom" -> { ++ sender.sendMessage(bot.getScoreboardName() + "'s spawn_phantom: " + bot.spawnPhantom); ++ if (bot.spawnPhantom) { ++ sender.sendMessage(bot.getScoreboardName() + "'s not_sleeping_ticks: " + bot.notSleepTicks); ++ } ++ } ++ case "always_send_data" -> ++ sender.sendMessage(bot.getScoreboardName() + "'s always_send_data: " + bot.alwaysSendData); ++ } ++ } else { ++ boolean value = args[3].equals("true"); ++ switch (args[2]) { ++ case "skip_sleep" -> bot.fauxSleeping = value; ++ case "spawn_phantom" -> bot.spawnPhantom = value; ++ case "always_send_data" -> bot.alwaysSendData = value; ++ } ++ sender.sendMessage(bot.getScoreboardName() + "'s " + args[2] + " changed: " + value); ++ } ++ } ++ ++ private void onList(CommandSender sender, String @NotNull [] args) { ++ if (args.length < 2) { ++ Map> botMap = new HashMap<>(); ++ for (World world : Bukkit.getWorlds()) { ++ botMap.put(world, new ArrayList<>()); ++ } ++ ++ for (ServerBot bot : ServerBot.getBots()) { ++ Bot bukkitBot = bot.getBukkitPlayer(); ++ botMap.get(bukkitBot.getWorld()).add(bukkitBot.getName()); ++ } ++ ++ sender.sendMessage("Total number: (" + ServerBot.getBots().size() + "/" + me.earthme.luminol.LuminolConfig.fakeplayerLimit + ")"); ++ for (World world : botMap.keySet()) { ++ sender.sendMessage(world.getName() + "(" + botMap.get(world).size() + "): " + formatPlayerNameList(botMap.get(world))); ++ } ++ } else { ++ World world = Bukkit.getWorld(args[2]); ++ ++ if (world == null) { ++ sender.sendMessage(ChatColor.RED + "Unknown world"); ++ return; ++ } ++ ++ List botList = new ArrayList<>(); ++ for (ServerBot bot : ServerBot.getBots()) { ++ Bot bukkitBot = bot.getBukkitPlayer(); ++ if (bukkitBot.getWorld() == world) { ++ botList.add(bukkitBot.getName()); ++ } ++ } ++ ++ sender.sendMessage(world.getName() + "(" + botList.size() + "): " + formatPlayerNameList(botList)); ++ } ++ } ++ ++ @NotNull ++ private static String formatPlayerNameList(@NotNull List list) { ++ if (list.isEmpty()) { ++ return ""; ++ } ++ String string = list.toString(); ++ return string.substring(1, string.length() - 1); ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/BotInventoryContainer.java b/src/main/java/top/leavesmc/leaves/bot/BotInventoryContainer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b83ad5bf2a338d589eb200d3f3b3153571ba2cad +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/BotInventoryContainer.java +@@ -0,0 +1,180 @@ ++package top.leavesmc.leaves.bot; ++ ++import com.google.common.collect.ImmutableList; ++import com.mojang.datafixers.util.Pair; ++import net.minecraft.core.NonNullList; ++import net.minecraft.network.chat.Component; ++import net.minecraft.world.ContainerHelper; ++import net.minecraft.world.SimpleContainer; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.item.Items; ++ ++import javax.annotation.Nonnull; ++import java.util.List; ++ ++// Power by gugle-carpet-addition(https://github.com/Gu-ZT/gugle-carpet-addition) ++public class BotInventoryContainer extends SimpleContainer { ++ ++ public final NonNullList items; ++ public final NonNullList armor; ++ public final NonNullList offhand; ++ private final List> compartments; ++ private final NonNullList buttons = NonNullList.withSize(13, ItemStack.EMPTY); ++ private final ServerBot player; ++ ++ public BotInventoryContainer(ServerBot player) { ++ this.player = player; ++ this.items = this.player.getInventory().items; ++ this.armor = this.player.getInventory().armor; ++ this.offhand = this.player.getInventory().offhand; ++ this.compartments = ImmutableList.of(this.items, this.armor, this.offhand, this.buttons); ++ createButton(); ++ } ++ ++ @Override ++ public int getContainerSize() { ++ return this.items.size() + this.armor.size() + this.offhand.size() + this.buttons.size(); ++ } ++ ++ @Override ++ public boolean isEmpty() { ++ for (ItemStack itemStack : this.items) { ++ if (itemStack.isEmpty()) { ++ continue; ++ } ++ return false; ++ } ++ for (ItemStack itemStack : this.armor) { ++ if (itemStack.isEmpty()) { ++ continue; ++ } ++ return false; ++ } ++ for (ItemStack itemStack : this.offhand) { ++ if (itemStack.isEmpty()) { ++ continue; ++ } ++ return false; ++ } ++ return true; ++ } ++ ++ @Override ++ @Nonnull ++ public ItemStack getItem(int slot) { ++ Pair, Integer> pair = getItemSlot(slot); ++ if (pair != null) { ++ return pair.getFirst().get(pair.getSecond()); ++ } else { ++ return ItemStack.EMPTY; ++ } ++ } ++ ++ public Pair, Integer> getItemSlot(int slot) { ++ switch (slot) { ++ case 0 -> { ++ return new Pair<>(buttons, 0); ++ } ++ case 1, 2, 3, 4 -> { ++ return new Pair<>(armor, 4 - slot); ++ } ++ case 5, 6 -> { ++ return new Pair<>(buttons, slot - 4); ++ } ++ case 7 -> { ++ return new Pair<>(offhand, 0); ++ } ++ case 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 -> { ++ return new Pair<>(buttons, slot - 5); ++ } ++ case 18, 19, 20, 21, 22, 23, 24, 25, 26, ++ 27, 28, 29, 30, 31, 32, 33, 34, 35, ++ 36, 37, 38, 39, 40, 41, 42, 43, 44 -> { ++ return new Pair<>(items, slot - 9); ++ } ++ case 45, 46, 47, 48, 49, 50, 51, 52, 53 -> { ++ return new Pair<>(items, slot - 45); ++ } ++ default -> { ++ return null; ++ } ++ } ++ } ++ ++ @Override ++ @Nonnull ++ public ItemStack removeItem(int slot, int amount) { ++ Pair, Integer> pair = getItemSlot(slot); ++ NonNullList list = null; ++ ItemStack itemStack = ItemStack.EMPTY; ++ if (pair != null) { ++ list = pair.getFirst(); ++ slot = pair.getSecond(); ++ } ++ if (list != null && !list.get(slot).isEmpty()) { ++ itemStack = ContainerHelper.removeItem(list, slot, amount); ++ player.detectEquipmentUpdatesPublic(); ++ } ++ return itemStack; ++ } ++ ++ @Override ++ @Nonnull ++ public ItemStack removeItemNoUpdate(int slot) { ++ Pair, Integer> pair = getItemSlot(slot); ++ NonNullList list = null; ++ if (pair != null) { ++ list = pair.getFirst(); ++ slot = pair.getSecond(); ++ } ++ if (list != null && !list.get(slot).isEmpty()) { ++ ItemStack itemStack = list.get(slot); ++ list.set(slot, ItemStack.EMPTY); ++ return itemStack; ++ } ++ return ItemStack.EMPTY; ++ } ++ ++ @Override ++ public void setItem(int slot, @Nonnull ItemStack stack) { ++ Pair, Integer> pair = getItemSlot(slot); ++ NonNullList list = null; ++ if (pair != null) { ++ list = pair.getFirst(); ++ slot = pair.getSecond(); ++ } ++ if (list != null) { ++ list.set(slot, stack); ++ player.detectEquipmentUpdatesPublic(); ++ } ++ } ++ ++ @Override ++ public void setChanged() { ++ } ++ ++ @Override ++ public boolean stillValid(@Nonnull Player player) { ++ if (this.player.isRemoved()) { ++ return false; ++ } ++ return !(player.distanceToSqr(this.player) > 64.0); ++ } ++ ++ @Override ++ public void clearContent() { ++ for (List list : this.compartments) { ++ list.clear(); ++ } ++ } ++ ++ private void createButton() { ++ for (int i = 0; i < 13; i++) { ++ ItemStack button = new ItemStack(Items.STRUCTURE_VOID); ++ button.setHoverName(Component.empty()); ++ button.getOrCreateTag().putBoolean("Leaves.Gui.Placeholder", true); ++ buttons.set(i, button); ++ } ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/BotStatsCounter.java b/src/main/java/top/leavesmc/leaves/bot/BotStatsCounter.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c3f114fba0759221b5fea0ccc4862f0579260cef +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/BotStatsCounter.java +@@ -0,0 +1,38 @@ ++package top.leavesmc.leaves.bot; ++ ++import com.mojang.datafixers.DataFixer; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.stats.ServerStatsCounter; ++import net.minecraft.stats.Stat; ++import net.minecraft.world.entity.player.Player; ++ ++import java.io.File; ++ ++public class BotStatsCounter extends ServerStatsCounter { ++ ++ private static final File UNKOWN_FILE = new File("BOT_STATS_REMOVE_THIS"); ++ ++ public BotStatsCounter(MinecraftServer server) { ++ super(server, UNKOWN_FILE); ++ } ++ ++ @Override ++ public void save() { ++ ++ } ++ ++ @Override ++ public void setValue(Player player, Stat stat, int value) { ++ ++ } ++ ++ @Override ++ public void parseLocal(DataFixer dataFixer, String json) { ++ ++ } ++ ++ @Override ++ public int getValue(Stat stat) { ++ return 0; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/BotUtil.java b/src/main/java/top/leavesmc/leaves/bot/BotUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8e8edd0a9fb53449fa3d1bee81629d667d0aa572 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/BotUtil.java +@@ -0,0 +1,183 @@ ++package top.leavesmc.leaves.bot; ++ ++import com.google.common.base.Charsets; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonObject; ++import net.minecraft.core.NonNullList; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.ListTag; ++import net.minecraft.nbt.NbtAccounter; ++import net.minecraft.nbt.NbtIo; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.world.entity.EquipmentSlot; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.storage.LevelResource; ++import org.bukkit.Bukkit; ++import org.bukkit.Location; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.agent.Actions; ++import top.leavesmc.leaves.bot.agent.BotAction; ++ ++import java.io.File; ++import java.io.IOException; ++import java.util.Collection; ++import java.util.Map; ++import java.util.UUID; ++ ++public class BotUtil { ++ ++ public static void replenishment(@NotNull ItemStack itemStack, NonNullList itemStackList) { ++ int count = itemStack.getMaxStackSize() / 2; ++ if (itemStack.getCount() <= 8 && count > 8) { ++ for (ItemStack itemStack1 : itemStackList) { ++ if (itemStack1 == ItemStack.EMPTY || itemStack1 == itemStack) { ++ continue; ++ } ++ ++ if (ItemStack.isSameItemSameTags(itemStack1, itemStack)) { ++ if (itemStack1.getCount() > count) { ++ itemStack.setCount(itemStack.getCount() + count); ++ itemStack1.setCount(itemStack1.getCount() - count); ++ } else { ++ itemStack.setCount(itemStack.getCount() + itemStack1.getCount()); ++ itemStack1.setCount(0); ++ } ++ break; ++ } ++ } ++ } ++ } ++ ++ public static void replaceTool(@NotNull EquipmentSlot slot, @NotNull ServerBot bot) { ++ ItemStack itemStack = bot.getItemBySlot(slot); ++ for (int i = 0; i < 36; i++) { ++ ItemStack itemStack1 = bot.getInventory().getItem(i); ++ if (itemStack1 == ItemStack.EMPTY || itemStack1 == itemStack) { ++ continue; ++ } ++ ++ if (itemStack1.getItem().getClass() == itemStack.getItem().getClass() && !isDamage(itemStack1, 10)) { ++ ItemStack itemStack2 = itemStack1.copy(); ++ bot.getInventory().setItem(i, itemStack); ++ bot.setItemSlot(slot, itemStack2); ++ return; ++ } ++ } ++ ++ for (int i = 0; i < 36; i++) { ++ ItemStack itemStack1 = bot.getInventory().getItem(i); ++ if (itemStack1 == ItemStack.EMPTY && itemStack1 != itemStack) { ++ bot.getInventory().setItem(i, itemStack); ++ bot.setItemSlot(slot, ItemStack.EMPTY); ++ return; ++ } ++ } ++ } ++ ++ public static boolean isDamage(@NotNull ItemStack item, int minDamage) { ++ return item.isDamageableItem() && (item.getMaxDamage() - item.getDamageValue()) <= minDamage; ++ } ++ ++ @NotNull ++ public static JsonObject saveBot(@NotNull ServerBot bot) { ++ double pos_x = bot.getX(); ++ double pos_y = bot.getY(); ++ double pos_z = bot.getZ(); ++ float yaw = bot.getYRot(); ++ float pitch = bot.getXRot(); ++ String dimension = bot.getLocation().getWorld().getName(); ++ String skin = bot.createState.skinName; ++ ++ JsonObject fakePlayer = new JsonObject(); ++ fakePlayer.addProperty("pos_x", pos_x); ++ fakePlayer.addProperty("pos_y", pos_y); ++ fakePlayer.addProperty("pos_z", pos_z); ++ fakePlayer.addProperty("yaw", yaw); ++ fakePlayer.addProperty("pitch", pitch); ++ fakePlayer.addProperty("dimension", dimension); ++ fakePlayer.addProperty("skin", skin); ++ ++ Collection actions = bot.getBotActions(); ++ JsonArray botActions = new JsonArray(); ++ for (BotAction action : actions) { ++ JsonObject actionObj = new JsonObject(); ++ actionObj.addProperty("name", action.getName()); ++ actionObj.addProperty("number", String.valueOf(action.getNumber())); ++ actionObj.addProperty("delay", String.valueOf(action.getTickDelay())); ++ botActions.add(actionObj); ++ } ++ fakePlayer.add("actions", botActions); ++ ++ CompoundTag invnbt = new CompoundTag(); ++ invnbt.put("Inventory", bot.getInventory().save(new ListTag())); ++ ++ File file = MinecraftServer.getServer().getWorldPath(LevelResource.ROOT).resolve("fakeplayer/" + bot.getStringUUID() + ".dat").toFile(); ++ File parent = file.getParentFile(); ++ try { ++ if (!parent.exists() || !parent.isDirectory()) { ++ parent.mkdirs(); ++ } ++ if (file.exists() && file.isFile()) { ++ file.delete(); ++ } ++ file.createNewFile(); ++ NbtIo.writeCompressed(invnbt, file); ++ } catch (IOException e) { ++ e.printStackTrace(); ++ } ++ ++ return fakePlayer; ++ } ++ ++ public static void loadBot(Map.@NotNull Entry entry) { ++ String username = entry.getKey(); ++ JsonObject fakePlayer = entry.getValue().getAsJsonObject(); ++ double pos_x = fakePlayer.get("pos_x").getAsDouble(); ++ double pos_y = fakePlayer.get("pos_y").getAsDouble(); ++ double pos_z = fakePlayer.get("pos_z").getAsDouble(); ++ float yaw = fakePlayer.get("yaw").getAsFloat(); ++ float pitch = fakePlayer.get("pitch").getAsFloat(); ++ String dimension = fakePlayer.get("dimension").getAsString(); ++ String skin = fakePlayer.get("skin").getAsString(); ++ ++ Location location = new Location(Bukkit.getWorld(dimension), pos_x, pos_y, pos_z, yaw, pitch); ++ ServerBot.BotCreateState state = new ServerBot.BotCreateState(location, username, skin); ++ ++ ListTag inv = null; ++ File file = MinecraftServer.getServer().getWorldPath(LevelResource.ROOT).resolve("fakeplayer/" + getBotUUID(state) + ".dat").toFile(); ++ if (file.exists()) { ++ try { ++ CompoundTag nbt = NbtIo.readCompressed(file); ++ inv = nbt.getList("Inventory", 10); ++ } catch (IOException e) { ++ e.printStackTrace(); ++ } ++ file.delete(); ++ } ++ ++ final JsonArray finalActions = fakePlayer.get("actions").getAsJsonArray(); ++ final ListTag finalInv = inv; ++ state.createAsync(serverBot -> { ++ if (finalInv != null) { ++ serverBot.getInventory().load(finalInv); ++ } ++ ++ for (JsonElement element : finalActions) { ++ JsonObject actionObj = element.getAsJsonObject(); ++ BotAction action = Actions.getForName(actionObj.get("name").getAsString()); ++ if (action != null) { ++ BotAction newAction = action.getNew(serverBot, ++ action.getArgument().parse(0, new String[]{actionObj.get("delay").getAsString(), actionObj.get("number").getAsString()}) ++ ); ++ serverBot.setBotAction(newAction); ++ } ++ } ++ }); ++ } ++ ++ @NotNull ++ public static UUID getBotUUID(ServerBot.@NotNull BotCreateState state) { ++ return UUID.nameUUIDFromBytes(("Fakeplayer:" + state.getRealName()).getBytes(Charsets.UTF_8)); ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/MojangAPI.java b/src/main/java/top/leavesmc/leaves/bot/MojangAPI.java +new file mode 100644 +index 0000000000000000000000000000000000000000..daaece30b2a3983f1cc9ee9a851e8f373974d5ec +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/MojangAPI.java +@@ -0,0 +1,41 @@ ++package top.leavesmc.leaves.bot; ++ ++import com.google.gson.JsonObject; ++import com.google.gson.JsonParser; ++ ++import java.io.IOException; ++import java.io.InputStreamReader; ++import java.net.URL; ++import java.util.HashMap; ++import java.util.Map; ++ ++public class MojangAPI { ++ ++ private static final boolean CACHE_ENABLED = false; ++ ++ private static final Map CACHE = new HashMap<>(); ++ ++ public static String[] getSkin(String name) { ++ if (CACHE_ENABLED && CACHE.containsKey(name)) { ++ return CACHE.get(name); ++ } ++ ++ String[] values = pullFromAPI(name); ++ CACHE.put(name, values); ++ return values; ++ } ++ ++ // Laggggggggggggggggggggggggggggggggggggggggg ++ public static String[] pullFromAPI(String name) { ++ try { ++ String uuid = new JsonParser().parse(new InputStreamReader(new URL("https://api.mojang.com/users/profiles/minecraft/" + name) ++ .openStream())).getAsJsonObject().get("id").getAsString(); ++ JsonObject property = new JsonParser() ++ .parse(new InputStreamReader(new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false") ++ .openStream())).getAsJsonObject().get("properties").getAsJsonArray().get(0).getAsJsonObject(); ++ return new String[] {property.get("value").getAsString(), property.get("signature").getAsString()}; ++ } catch (IOException | IllegalStateException e) { ++ return null; ++ } ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/ServerBot.java b/src/main/java/top/leavesmc/leaves/bot/ServerBot.java +new file mode 100644 +index 0000000000000000000000000000000000000000..30391d5dc0e1bdaeaf9ee190e74c3da0440eeef8 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/ServerBot.java +@@ -0,0 +1,760 @@ ++package top.leavesmc.leaves.bot; ++ ++import com.google.common.collect.Lists; ++import com.google.gson.Gson; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonObject; ++import com.mojang.authlib.GameProfile; ++import com.mojang.authlib.properties.Property; ++import io.papermc.paper.threadedregions.RegionizedServer; ++import io.papermc.paper.threadedregions.ThreadedRegionizer; ++import io.papermc.paper.threadedregions.TickRegionScheduler; ++import io.papermc.paper.util.TickThread; ++import net.minecraft.Util; ++import net.minecraft.core.BlockPos; ++import net.minecraft.network.Connection; ++import net.minecraft.network.PacketSendListener; ++import net.minecraft.network.protocol.Packet; ++import net.minecraft.network.protocol.PacketFlow; ++import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; ++import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; ++import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; ++import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; ++import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; ++import net.minecraft.network.syncher.EntityDataAccessor; ++import net.minecraft.network.syncher.EntityDataSerializers; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ClientInformation; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.network.CommonListenerCookie; ++import net.minecraft.server.network.ServerGamePacketListenerImpl; ++import net.minecraft.server.network.ServerPlayerConnection; ++import net.minecraft.stats.ServerStatsCounter; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.InteractionResult; ++import net.minecraft.world.SimpleMenuProvider; ++import net.minecraft.world.damagesource.DamageSource; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.EquipmentSlot; ++import net.minecraft.world.entity.ai.attributes.Attributes; ++import net.minecraft.world.entity.item.ItemEntity; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.inventory.ChestMenu; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.gameevent.GameEvent; ++import net.minecraft.world.level.storage.LevelResource; ++import net.minecraft.world.phys.AABB; ++import net.minecraft.world.phys.Vec3; ++import org.bukkit.Bukkit; ++import org.bukkit.ChatColor; ++import org.bukkit.Location; ++import org.bukkit.Material; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.craftbukkit.scheduler.CraftScheduler; ++import org.bukkit.event.entity.CreatureSpawnEvent; ++import org.bukkit.util.Vector; ++import org.jetbrains.annotations.NotNull; ++import me.earthme.luminol.LuminolConfig; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.bot.agent.actions.StopAction; ++import top.leavesmc.leaves.entity.Bot; ++import top.leavesmc.leaves.entity.CraftBot; ++import top.leavesmc.leaves.event.bot.BotCreateEvent; ++import top.leavesmc.leaves.event.bot.BotInventoryOpenEvent; ++import top.leavesmc.leaves.event.bot.BotJoinEvent; ++import top.leavesmc.leaves.util.MathUtils; ++ ++import javax.annotation.Nullable; ++import java.io.BufferedReader; ++import java.io.BufferedWriter; ++import java.io.File; ++import java.io.IOException; ++import java.nio.charset.StandardCharsets; ++import java.nio.file.Files; ++import java.util.Collection; ++import java.util.EnumSet; ++import java.util.HashMap; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import java.util.UUID; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.CopyOnWriteArrayList; ++import java.util.function.Consumer; ++import java.util.function.Supplier; ++ ++// TODO remake all ++public class ServerBot extends ServerPlayer { ++ ++ private final Map actions; ++ private final boolean removeOnDeath; ++ private final int tracingRange; ++ ++ private Vec3 velocity; ++ private int fireTicks; ++ private int jumpTicks; ++ private int noFallTicks; ++ public boolean waterSwim; ++ private Vec3 knockback; ++ public BotCreateState createState; ++ public UUID createPlayer; ++ ++ private final ServerStatsCounter stats; ++ private final BotInventoryContainer container; ++ ++ private static final List bots = new CopyOnWriteArrayList<>(); ++ ++ public boolean spawnPhantom; ++ public int notSleepTicks; ++ public boolean alwaysSendData; ++ ++ private ServerBot(MinecraftServer server, ServerLevel world, GameProfile profile) { ++ super(server, world, profile, ClientInformation.createDefault()); ++ this.entityData.set(new EntityDataAccessor<>(16, EntityDataSerializers.INT), 0xFF); ++ this.entityData.set(Player.DATA_PLAYER_MODE_CUSTOMISATION, (byte) -2); ++ ++ this.velocity = new Vec3(this.xxa, this.yya, this.zza); ++ this.noFallTicks = 60; ++ this.fireTicks = 0; ++ this.actions = new HashMap<>(); ++ this.removeOnDeath = true; ++ this.stats = new BotStatsCounter(server); ++ this.container = new BotInventoryContainer(this); ++ this.waterSwim = true; ++ this.knockback = Vec3.ZERO; ++ this.tracingRange = world.spigotConfig.playerTrackingRange * world.spigotConfig.playerTrackingRange; ++ this.notSleepTicks = 0; ++ ++ this.fauxSleeping = LuminolConfig.fakeplayerSkipSleep; ++ this.spawnPhantom = LuminolConfig.fakeplayerSpawnPhantom; ++ this.alwaysSendData = LuminolConfig.alwaysSendFakeplayerData; ++ } ++ ++ public static ServerBot createBot(@NotNull BotCreateState state) { ++ if (!isCreateLegal(state.name)) { ++ return null; ++ } ++ ++ MinecraftServer server = MinecraftServer.getServer(); ++ ++ Supplier creator = () -> { ++ BotCreateEvent event = new BotCreateEvent(state.name, state.skinName, state.loc, ChatColor.YELLOW + state.name + " joined the game"); ++ server.server.getPluginManager().callEvent(event); ++ ++ if (event.isCancelled()) { ++ return null; ++ } ++ ++ Location location = event.getCreateLocation(); ++ ++ ServerLevel world = ((CraftWorld) location.getWorld()).getHandle(); ++ CustomGameProfile profile = new CustomGameProfile(BotUtil.getBotUUID(state), state.name, state.skin); ++ ++ ServerBot bot = new ServerBot(server, world, profile); ++ ++ bot.connection = new ServerGamePacketListenerImpl(server, new Connection(PacketFlow.SERVERBOUND) { // ? ++ @Override ++ public void send(@NotNull Packet packet) { ++ } ++ ++ @Override ++ public void send(@NotNull Packet packet, @Nullable PacketSendListener packetsendlistener) { ++ } ++ ++ @Override ++ public void send(@NotNull Packet packet, @Nullable PacketSendListener callbacks, boolean flush) { ++ } ++ }, bot, CommonListenerCookie.createInitial(profile)); ++ bot.isRealPlayer = true; ++ bot.createState = state; ++ ++ if (event.getJoinMessage() != null) { ++ Bukkit.broadcastMessage(event.getJoinMessage()); ++ } ++ ++ bot.teleportTo(location.getX(), location.getY(), location.getZ()); ++ bot.setRot(location.getYaw(), location.getPitch()); ++ bot.getBukkitEntity().setRotation(location.getYaw(), location.getPitch()); ++ world.addFreshEntity(bot, CreatureSpawnEvent.SpawnReason.COMMAND); ++ ++ bot.renderAll(); ++ server.getPlayerList().addNewBot(bot); ++ bots.add(bot); ++ ++ BotJoinEvent event1 = new BotJoinEvent(bot.getBukkitPlayer()); ++ server.server.getPluginManager().callEvent(event1); ++ ++ return bot; ++ }; ++ ++ if (TickThread.isTickThreadFor(((CraftWorld) state.loc.getWorld()).getHandle(),state.loc.getBlockX() >> 4,state.loc.getBlockZ() >> 4)){ ++ return creator.get(); ++ } ++ ++ return CompletableFuture.supplyAsync( ++ creator, ++ task -> ++ RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ ((CraftWorld) state.loc.getWorld()).getHandle(), ++ state.loc.getBlockX() >> 4, ++ state.loc.getBlockZ() >> 4, ++ task ++ ) ++ ).join(); ++ } ++ ++ public static boolean isCreateLegal(@NotNull String name) { ++ if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) { ++ return false; ++ } ++ ++ if (Bukkit.getPlayer(name) != null || ServerBot.getBot(name) != null) { ++ return false; ++ } ++ ++ if (me.earthme.luminol.LuminolConfig.unableFakeplayerNames.contains(name)) { ++ return false; ++ } ++ ++ return ServerBot.getBots().size() < me.earthme.luminol.LuminolConfig.fakeplayerLimit; ++ } ++ ++ public void renderAll() { ++ MinecraftServer.getServer().getPlayerList().getPlayers().forEach( ++ player -> { ++ this.sendPlayerInfo(player); ++ this.sendFakeData(player.connection, false); ++ } ++ ); ++ } ++ ++ public void sendPlayerInfo(ServerPlayer player) { ++ player.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED, ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME), List.of(this))); ++ } ++ ++ public boolean needSendFakeData(ServerPlayer player) { ++ return alwaysSendData && (player.level() == this.level() && player.position().distanceToSqr(this.position()) > this.tracingRange); ++ } ++ ++ public void sendFakeDataIfNeed(ServerPlayer player, boolean login) { ++ if (needSendFakeData(player)) { ++ this.sendFakeData(player.connection, login); ++ } ++ } ++ ++ public void sendFakeData(ServerPlayerConnection playerConnection, boolean login) { ++ playerConnection.send(new ClientboundAddEntityPacket(this)); ++ if (login) { ++ Bukkit.getScheduler().runTaskLater(CraftScheduler.MINECRAFT, () -> { ++ connection.send(new ClientboundRotateHeadPacket(this, (byte) ((getYRot() * 256f) / 360f))); ++ }, 10); ++ } else { ++ connection.send(new ClientboundRotateHeadPacket(this, (byte) ((getYRot() * 256f) / 360f))); ++ } ++ } ++ ++ private void sendPacket(Packet packet) { ++ MinecraftServer.getServer().getPlayerList().getPlayers().forEach( ++ player -> player.connection.send(packet) ++ ); ++ } ++ ++ // die check start ++ @Override ++ public void die(@NotNull DamageSource damageSource) { ++ super.die(damageSource); ++ this.dieCheck(); ++ } ++ ++ private void dieCheck() { ++ if (removeOnDeath) { ++ bots.remove(this); ++ server.getPlayerList().removeBot(this); ++ remove(RemovalReason.KILLED); ++ this.setDead(); ++ this.removeTab(); ++ Bukkit.broadcastMessage(ChatColor.YELLOW + this.getName().getString() + " left the game"); // TODO i18n ++ } ++ } ++ ++ private void removeTab() { ++ sendPacket(new ClientboundPlayerInfoRemovePacket(List.of(this.getUUID()))); ++ } ++ ++ private void setDead() { ++ sendPacket(new ClientboundRemoveEntitiesPacket(getId())); ++ this.dead = true; ++ this.inventoryMenu.removed(this); ++ this.containerMenu.removed(this); ++ } ++ ++ // die check end ++ ++ @Nullable ++ @Override ++ public Entity changeDimension(@NotNull ServerLevel destination) { ++ return null; // disable dimension change ++ } ++ ++ public Bot getBukkitPlayer() { ++ return getBukkitEntity(); ++ } ++ ++ @Override ++ @NotNull ++ public CraftBot getBukkitEntity() { ++ return (CraftBot) super.getBukkitEntity(); ++ } ++ ++ @Override ++ public boolean isInWater() { ++ Location loc = getLocation(); ++ for (int i = 0; i <= 2; i++) { ++ Material type = loc.getBlock().getType(); ++ if (type == Material.WATER || type == Material.LAVA) { ++ return true; ++ } ++ loc.add(0, 0.9, 0); ++ } ++ return false; ++ } ++ ++ @Override ++ public void tick() { ++ super.tick(); ++ this.doTick(); ++ ++ if (!isAlive()) { ++ return; ++ } ++ ++ if (spawnPhantom) { ++ notSleepTicks++; ++ } ++ ++ if (fireTicks > 0) { ++ --fireTicks; ++ } ++ if (jumpTicks > 0) { ++ --jumpTicks; ++ } ++ if (noFallTicks > 0) { ++ --noFallTicks; ++ } ++ if (takeXpDelay > 0) { ++ --takeXpDelay; ++ } ++ ++ this.updateLocation(); ++ this.updatePlayerPose(); ++ ++ if (TickRegionScheduler.getCurrentRegion().getData().getCurrentTick() % 20 == 0) { ++ float health = getHealth(); ++ float maxHealth = getMaxHealth(); ++ float regenAmount = (float) (me.earthme.luminol.LuminolConfig.fakeplayerRegenAmount * 20); ++ float amount; ++ ++ if (health < maxHealth - regenAmount) { ++ amount = health + regenAmount; ++ } else { ++ amount = maxHealth; ++ } ++ ++ this.setHealth(amount); ++ } ++ ++ BlockPos blockposition = this.getOnPosLegacy(); ++ BlockState iblockdata = this.level().getBlockState(blockposition); ++ Vec3 vec3d1 = this.collide(velocity); ++ this.checkFallDamage(vec3d1.y, this.onGround(), iblockdata, blockposition); ++ ++ ++this.attackStrengthTicker; ++ ++ if (this.getHealth() > 0.0F) { ++ AABB axisalignedbb; ++ ++ if (this.isPassenger() && !this.getVehicle().isRemoved()) { ++ axisalignedbb = this.getBoundingBox().minmax(this.getVehicle().getBoundingBox()).inflate(1.0D, 0.0D, 1.0D); ++ } else { ++ axisalignedbb = this.getBoundingBox().inflate(1.0D, 0.5D, 1.0D); ++ } ++ ++ List list = this.level().getEntities(this, axisalignedbb); ++ List list1 = Lists.newArrayList(); ++ ++ for (Entity entity : list) { ++ if (entity.getType() == EntityType.EXPERIENCE_ORB) { ++ list1.add(entity); ++ } else if (!entity.isRemoved()) { ++ this.touch(entity); ++ } ++ } ++ ++ if (!list1.isEmpty()) { ++ this.touch(Util.getRandom(list1, this.random)); ++ } ++ } ++ ++ Iterator> iterator = actions.entrySet().iterator(); ++ while (iterator.hasNext()) { ++ Map.Entry entry = iterator.next(); ++ if (entry.getValue().isCancel()) { ++ iterator.remove(); ++ } else { ++ entry.getValue().tryTick(this); ++ } ++ } ++ } ++ ++ private void touch(@NotNull Entity entity) { ++ entity.playerTouch(this); ++ } ++ ++ @Override ++ public void onItemPickup(@NotNull ItemEntity item) { ++ super.onItemPickup(item); ++ this.updateItemInMainHand(); ++ } ++ ++ public void updateItemInMainHand() { ++ tryReplenishOrReplaceInMainHand(); ++ detectEquipmentUpdatesPublic(); ++ } ++ ++ public void updateItemInOffHand() { ++ tryReplenishOrReplaceInOffHand(); ++ detectEquipmentUpdatesPublic(); ++ } ++ ++ public void tryReplenishOrReplaceInOffHand() { ++ net.minecraft.world.item.ItemStack offhand = getOffhandItem(); ++ ++ if (!offhand.isEmpty()) { ++ BotUtil.replenishment(offhand, getInventory().items); ++ if (BotUtil.isDamage(offhand, 10)) { ++ BotUtil.replaceTool(EquipmentSlot.OFFHAND, this); ++ } ++ } ++ } ++ ++ public void tryReplenishOrReplaceInMainHand() { ++ net.minecraft.world.item.ItemStack mainHand = getMainHandItem(); ++ ++ if (!mainHand.isEmpty()) { ++ BotUtil.replenishment(mainHand, getInventory().items); ++ if (BotUtil.isDamage(mainHand, 10)) { ++ BotUtil.replaceTool(EquipmentSlot.MAINHAND, this); ++ } ++ } ++ } ++ ++ @Override ++ public void checkFallDamage(double heightDifference, boolean onGround, @NotNull BlockState state, @NotNull BlockPos landedPosition) { ++ if (onGround) { ++ if (this.fallDistance > 0.0F) { ++ state.getBlock().fallOn(this.level(), state, landedPosition, this, this.fallDistance); ++ this.level().gameEvent(GameEvent.HIT_GROUND, this.position(), GameEvent.Context.of(this, this.mainSupportingBlockPos.map((blockposition1) -> { ++ return this.level().getBlockState(blockposition1); ++ }).orElse(state))); ++ } ++ ++ this.resetFallDistance(); ++ } else if (heightDifference < 0.0D) { ++ this.fallDistance -= (float) heightDifference; ++ } ++ } ++ ++ @Override ++ public void doTick() { ++ if (this.hurtTime > 0) { ++ this.hurtTime -= 1; ++ } ++ ++ baseTick(); ++ ++ this.lerpSteps = (int) this.zza; ++ this.animStep = this.run; ++ this.yRotO = this.getYRot(); ++ this.xRotO = this.getXRot(); ++ } ++ ++ public Location getLocation() { ++ return getBukkitPlayer().getLocation(); ++ } ++ ++ @Override ++ public void knockback(double strength, double x, double z, @NotNull Entity knockingBackEntity) { ++ strength *= 1.0D - this.getAttributeValue(Attributes.KNOCKBACK_RESISTANCE); ++ if (strength > 0.0D) { ++ this.hasImpulse = true; ++ Vec3 vec3d = this.getDeltaMovement(); ++ Vec3 vec3d1 = (new Vec3(x, 0.0D, z)).normalize().scale(strength); ++ knockback = new Vec3(vec3d.x / 2.0D - vec3d1.x, this.onGround() ? Math.min(0.4D, vec3d.y / 2.0D + strength) : vec3d.y, vec3d.z / 2.0D - vec3d1.z); ++ } ++ } ++ ++ private void updateLocation() { ++ this.velocity = new Vec3(this.xxa, this.yya, this.zza); ++ ++ if (waterSwim && isInWater()) { ++ this.addDeltaMovement(new Vec3(0, 0.05, 0)); ++ } ++ this.addDeltaMovement(knockback); ++ knockback = Vec3.ZERO; ++ ++ this.travel(this.velocity); ++ } ++ ++ public void faceLocation(@NotNull Location loc) { ++ look(loc.toVector().subtract(getLocation().toVector()), false); ++ } ++ ++ public void look(Vector dir, boolean keepYaw) { ++ float yaw, pitch; ++ ++ if (keepYaw) { ++ yaw = this.getYHeadRot(); ++ pitch = MathUtils.fetchPitch(dir); ++ } else { ++ float[] vals = MathUtils.fetchYawPitch(dir); ++ yaw = vals[0]; ++ pitch = vals[1]; ++ ++ sendPacket(new ClientboundRotateHeadPacket(this, (byte) (yaw * 256 / 360f))); ++ } ++ ++ setRot(yaw, pitch); ++ this.getBukkitEntity().setRotation(yaw, pitch); ++ } ++ ++ public void punch() { ++ swing(InteractionHand.MAIN_HAND); ++ } ++ ++ public void attack(@NotNull Entity target) { ++ super.attack(target); ++ punch(); ++ } ++ ++ public void jumpFromGround() { ++ double jumpPower = (double) this.getJumpPower() + this.getJumpBoostPower(); ++ this.addDeltaMovement(new Vec3(0, jumpPower, 0)); ++ } ++ ++ public void dropAll() { ++ getInventory().dropAll(); ++ detectEquipmentUpdatesPublic(); ++ } ++ ++ public void setBotAction(BotAction action) { ++ if (action instanceof StopAction) { ++ this.actions.clear(); ++ } ++ action.init(); ++ this.actions.put(action.getName(), action); ++ } ++ ++ public Collection getBotActions() { ++ return actions.values(); ++ } ++ ++ public BotAction getBotAction(String name) { ++ return actions.get(name); ++ } ++ ++ @Deprecated ++ public BotAction getBotAction() { ++ return null; ++ } ++ ++ @Override ++ public @NotNull ServerStatsCounter getStats() { ++ return stats; ++ } ++ ++ public BotInventoryContainer getContainer() { ++ return container; ++ } ++ ++ @Override ++ public @NotNull InteractionResult interact(@NotNull Player player, @NotNull InteractionHand hand) { ++ if (me.earthme.luminol.LuminolConfig.openFakeplayerInventory) { ++ if (player instanceof ServerPlayer player1 && player.getMainHandItem().isEmpty()) { ++ BotInventoryOpenEvent event = new BotInventoryOpenEvent(this.getBukkitEntity(), player1.getBukkitEntity()); ++ server.server.getPluginManager().callEvent(event); ++ if (!event.isCancelled()) { ++ player.openMenu(new SimpleMenuProvider((i, inventory, p) -> ChestMenu.sixRows(i, inventory, container), getDisplayName())); ++ return InteractionResult.SUCCESS; ++ } ++ } ++ } ++ return super.interact(player, hand); ++ } ++ ++ public static ServerBot getBot(ServerPlayer player) { ++ ServerBot bot = null; ++ for (ServerBot b : bots) { ++ if (b.getId() == player.getId()) { ++ bot = b; ++ break; ++ } ++ } ++ return bot; ++ } ++ ++ public static ServerBot getBot(String name) { ++ ServerBot bot = null; ++ for (ServerBot b : bots) { ++ if (b.getName().getString().equals(name)) { ++ bot = b; ++ break; ++ } ++ } ++ return bot; ++ } ++ ++ public static ServerBot getBot(UUID uuid) { ++ ServerBot bot = null; ++ for (ServerBot b : bots) { ++ if (b.uuid == uuid) { ++ bot = b; ++ break; ++ } ++ } ++ return bot; ++ } ++ ++ public static void saveOrRemoveAllBot() { ++ if (me.earthme.luminol.LuminolConfig.fakeplayerSupport && me.earthme.luminol.LuminolConfig.fakeplayerResident) { ++ JsonObject fakePlayerList = new JsonObject(); ++ bots.forEach(bot -> fakePlayerList.add(bot.createState.realName, BotUtil.saveBot(bot))); ++ File file = MinecraftServer.getServer().getWorldPath(LevelResource.ROOT).resolve("fake_player.leaves.json").toFile(); ++ if (!file.isFile()) { ++ try { ++ file.createNewFile(); ++ } catch (IOException e) { ++ e.printStackTrace(); ++ } ++ } ++ try (BufferedWriter bfw = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { ++ bfw.write(new Gson().toJson(fakePlayerList)); ++ } catch (IOException e) { ++ e.printStackTrace(); ++ } ++ } else { ++ removeAllBot(); ++ } ++ } ++ ++ public static void loadAllBot() { ++ if (me.earthme.luminol.LuminolConfig.fakeplayerSupport && me.earthme.luminol.LuminolConfig.fakeplayerResident) { ++ JsonObject fakePlayerList = new JsonObject(); ++ File file = MinecraftServer.getServer().getWorldPath(LevelResource.ROOT).resolve("fake_player.leaves.json").toFile(); ++ if (!file.isFile()) { ++ return; ++ } ++ try (BufferedReader bfr = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { ++ fakePlayerList = new Gson().fromJson(bfr, JsonObject.class); ++ } catch (IOException e) { ++ e.printStackTrace(); ++ } ++ for (Map.Entry entry : fakePlayerList.entrySet()) { ++ BotUtil.loadBot(entry); ++ } ++ file.delete(); ++ } ++ } ++ ++ public static boolean removeAllBot() { ++ Iterator iterator = bots.iterator(); ++ while (iterator.hasNext()) { ++ ServerBot bot = iterator.next(); ++ bot.die(bot.damageSources().fellOutOfWorld()); ++ } ++ return true; ++ } ++ ++ public static List getBots() { ++ return bots; ++ } ++ ++ public static class CustomGameProfile extends GameProfile { ++ ++ public CustomGameProfile(UUID uuid, String name, String[] skin) { ++ super(uuid, name); ++ setSkin(skin); ++ } ++ ++ public void setSkin(String[] skin) { ++ if (skin != null) { ++ getProperties().put("textures", new Property("textures", skin[0], skin[1])); ++ } ++ } ++ } ++ ++ public static class BotCreateState { ++ ++ public Location loc; ++ ++ public String[] skin; ++ public String skinName; ++ ++ private String realName; ++ private String name; ++ ++ public BotCreateState(Location loc, String realName, String skinName) { ++ this.loc = loc; ++ this.skinName = skinName; ++ this.setRealName(realName); ++ } ++ ++ public BotCreateState(Location loc, String name, String realName, String skinName, String[] skin) { ++ this.loc = loc; ++ this.skinName = skinName; ++ this.skin = skin; ++ this.realName = realName; ++ this.name = name; ++ } ++ ++ public ServerBot createSync() { ++ return createBot(this); ++ } ++ ++ public void createAsync(Consumer consumer) { ++ Bukkit.getAsyncScheduler().runNow(CraftScheduler.MINECRAFT,scheduledTask -> { ++ try { ++ if (skinName != null) { ++ this.skin = MojangAPI.getSkin(skinName); ++ } ++ ++ ++ ServerBot bot = createBot(this); ++ if (bot != null && consumer != null) { ++ consumer.accept(bot); ++ } ++ }catch (Exception e){ ++ MinecraftServer.LOGGER.error("Exception in processing async task for fakeplayer!",e); ++ } ++ }); ++ } ++ ++ public void setName(String name) { ++ this.name = name; ++ } ++ ++ public void setRealName(String realName) { ++ this.realName = realName; ++ this.name = me.earthme.luminol.LuminolConfig.fakeplayerPrefix + realName + me.earthme.luminol.LuminolConfig.fakeplayerSuffix; ++ } ++ ++ public String getName() { ++ return name; ++ } ++ ++ public String getRealName() { ++ return realName; ++ } ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/Actions.java b/src/main/java/top/leavesmc/leaves/bot/agent/Actions.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a7e98848e400641077ac567d9ea9a58c32123c98 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/Actions.java +@@ -0,0 +1,66 @@ ++package top.leavesmc.leaves.bot.agent; ++ ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.agent.actions.*; ++ ++import java.util.Collection; ++import java.util.HashMap; ++import java.util.Map; ++import java.util.Set; ++ ++public class Actions { ++ ++ private static final Map actions = new HashMap<>(); ++ ++ public static void registerAll() { ++ register(new AttackAction()); ++ register(new BreakBlockAction()); ++ register(new DropAction()); ++ register(new JumpAction()); ++ register(new RotateAction()); ++ register(new SneakAction()); ++ register(new StopAction()); ++ register(new UseItemAction()); ++ register(new UseItemOnAction()); ++ register(new UseItemToAction()); ++ register(new LookAction()); ++ register(new FishAction()); ++ register(new AttackSelfAction()); ++ register(new SwimAction()); ++ register(new UseItemOffHandAction()); ++ register(new UseItemOnOffhandAction()); ++ register(new UseItemToOffhandAction()); ++ } ++ ++ public static boolean register(@NotNull BotAction action) { ++ if (!actions.containsKey(action.getName())) { ++ actions.put(action.getName(), action); ++ return true; ++ } ++ return false; ++ } ++ ++ public static boolean unregister(@NotNull String name) { ++ if (actions.containsKey(name)) { ++ actions.remove(name); ++ return true; ++ } ++ return false; ++ } ++ ++ @NotNull ++ @Contract(pure = true) ++ public static Collection getAll() { ++ return actions.values(); ++ } ++ ++ @NotNull ++ public static Set getNames() { ++ return actions.keySet(); ++ } ++ ++ public static BotAction getForName(String name) { ++ return actions.get(name); ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/BotAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/BotAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..20ba4904219d015903cd3a5b2e1693eaba6088d8 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/BotAction.java +@@ -0,0 +1,93 @@ ++package top.leavesmc.leaves.bot.agent; ++ ++import net.minecraft.server.level.ServerPlayer; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++ ++import java.util.List; ++ ++public abstract class BotAction { ++ ++ private final String name; ++ private final CommandArgument argument; ++ ++ private boolean cancel; ++ private int tickDelay; ++ private int number; ++ ++ private int needWaitTick; ++ private int canDoNumber; ++ ++ public BotAction(String name, CommandArgument argument) { ++ this.name = name; ++ this.argument = argument; ++ ++ this.cancel = false; ++ this.tickDelay = 20; ++ this.number = -1; ++ } ++ ++ public String getName() { ++ return name; ++ } ++ ++ public int getTickDelay() { ++ return tickDelay; ++ } ++ ++ public int getNumber() { ++ return number; ++ } ++ ++ public boolean isCancel() { ++ return cancel; ++ } ++ ++ public BotAction setTickDelay(int tickDelay) { ++ this.tickDelay = Math.max(0, tickDelay); ++ return this; ++ } ++ ++ public BotAction setTabComplete(int index, List list) { ++ argument.setTabComplete(index, list); ++ return this; ++ } ++ ++ public BotAction setNumber(int number) { ++ this.number = Math.max(-1, number); ++ return this; ++ } ++ ++ public void setCancel(boolean cancel) { ++ this.cancel = cancel; ++ } ++ ++ public void init() { ++ this.needWaitTick = 0; ++ this.canDoNumber = this.getNumber(); ++ this.setCancel(false); ++ } ++ ++ public void tryTick(ServerBot bot) { ++ if (canDoNumber == 0) { ++ this.setCancel(true); ++ return; ++ } ++ if (needWaitTick-- <= 0) { ++ if (this.doTick(bot)) { ++ canDoNumber--; ++ needWaitTick = this.getTickDelay(); ++ } ++ } ++ } ++ ++ public CommandArgument getArgument() { ++ return argument; ++ } ++ ++ public abstract BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result); ++ ++ public abstract boolean doTick(@NotNull ServerBot bot); ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/AttackAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/AttackAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..609605b21cfe5af8876f76ea4922e379c5dd166e +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/AttackAction.java +@@ -0,0 +1,36 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.phys.EntityHitResult; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++import top.leavesmc.leaves.command.CommandArgumentType; ++ ++import java.util.List; ++ ++public class AttackAction extends BotAction { ++ ++ public AttackAction() { ++ super("attack", new CommandArgument(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER)); ++ setTabComplete(0, List.of("[TickDelay]")); ++ setTabComplete(1, List.of("[DoNumber]")); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return new AttackAction().setTickDelay(result.readInt(20)).setNumber(result.readInt(-1)); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ EntityHitResult result = bot.getTargetEntity(3); ++ if (result != null) { ++ bot.attack(result.getEntity()); ++ return true; ++ } ++ return false; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/AttackSelfAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/AttackSelfAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5a0e5626751d0b6ea12a6074b5626937b6668608 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/AttackSelfAction.java +@@ -0,0 +1,42 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import com.google.common.base.Predicates; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.entity.Entity; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++import top.leavesmc.leaves.command.CommandArgumentType; ++ ++import java.util.List; ++ ++public class AttackSelfAction extends BotAction { ++ ++ public AttackSelfAction() { ++ super("attack_self", new CommandArgument(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER)); ++ setTabComplete(0, List.of("[TickDelay]")); ++ setTabComplete(1, List.of("[DoNumber]")); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return new AttackSelfAction().setTickDelay(result.readInt(20)).setNumber(result.readInt(-1)); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ List entities = bot.level().getEntities((Entity) null, bot.getBoundingBox(), Predicates.alwaysTrue()); ++ if (!entities.isEmpty()) { ++ for (int i = 0; i < entities.size(); i++) { ++ Entity entity = entities.get(i); ++ if (entity != bot) { ++ bot.attack(entity); ++ return true; ++ } ++ } ++ } ++ return false; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2a3ca671b43fec658bf5cd8a6eb08b476a766c29 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java +@@ -0,0 +1,104 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.level.block.state.BlockState; ++import org.bukkit.block.Block; ++import org.bukkit.craftbukkit.block.CraftBlock; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++import top.leavesmc.leaves.command.CommandArgumentType; ++ ++import java.util.List; ++ ++public class BreakBlockAction extends BotAction { ++ ++ public BreakBlockAction() { ++ super("break", new CommandArgument(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER)); ++ setTabComplete(0, List.of("[TickDelay]")); ++ setTabComplete(1, List.of("[DoNumber]")); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return new BreakBlockAction().setTickDelay(result.readInt(20)).setNumber(result.readInt(-1)); ++ } ++ ++ @Override ++ public BotAction setTickDelay(int tickDelay) { ++ super.setTickDelay(0); ++ this.delay = tickDelay; ++ return this; ++ } ++ ++ private int delay = 0; ++ private int nowDelay = 0; ++ ++ private BlockPos lastPos = null; ++ private int destroyProgressTime = 0; ++ private int lastSentState = -1; ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ if (nowDelay > 0) { ++ nowDelay--; ++ return false; ++ } ++ ++ Block block = bot.getBukkitEntity().getTargetBlockExact(5); ++ if (block != null) { ++ BlockPos pos = ((CraftBlock) block).getPosition(); ++ ++ if (lastPos == null || !lastPos.equals(pos)) { ++ lastPos = pos; ++ destroyProgressTime = 0; ++ lastSentState = -1; ++ } ++ ++ BlockState iblockdata = bot.level().getBlockState(pos); ++ if (!iblockdata.isAir()) { ++ bot.punch(); ++ ++ if (iblockdata.getDestroyProgress(bot, bot.level(), pos) >= 1.0F) { ++ bot.gameMode.destroyAndAck(pos, 0, "insta mine"); ++ bot.level().destroyBlockProgress(bot.getId(), pos, -1); ++ bot.updateItemInMainHand(); ++ finalBreak(); ++ return true; ++ } ++ ++ float damage = this.incrementDestroyProgress(bot, iblockdata, pos); ++ if (damage >= 1.0F) { ++ bot.gameMode.destroyAndAck(pos, 0, "destroyed"); ++ bot.level().destroyBlockProgress(bot.getId(), pos, -1); ++ bot.updateItemInMainHand(); ++ finalBreak(); ++ return true; ++ } ++ } ++ } ++ return false; ++ } ++ ++ private void finalBreak() { ++ lastPos = null; ++ destroyProgressTime = 0; ++ lastSentState = -1; ++ nowDelay = delay; ++ } ++ ++ private float incrementDestroyProgress(ServerBot bot, @NotNull BlockState state, BlockPos pos) { ++ float f = state.getDestroyProgress(bot, bot.level(), pos) * (float) (++destroyProgressTime); ++ int k = (int) (f * 10.0F); ++ ++ if (k != lastSentState) { ++ bot.level().destroyBlockProgress(bot.getId(), pos, k); ++ lastSentState = k; ++ } ++ ++ return f; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..89a361249179d7a0a84768e715ced05aafc13272 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java +@@ -0,0 +1,48 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import org.bukkit.entity.Player; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++import top.leavesmc.leaves.entity.botaction.CustomBotAction; ++ ++public class CraftCustomBotAction extends BotAction { ++ ++ private final CustomBotAction realAction; ++ ++ public CraftCustomBotAction(String name, @NotNull CustomBotAction realAction) { ++ super(name, new CommandArgument().setAllTabComplete(realAction.getTabComplete())); ++ this.realAction = realAction; ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ ++ public BotAction getNew(@NotNull Player player, String[] args) { ++ CustomBotAction newRealAction = realAction.getNew(player, args); ++ if (newRealAction != null) { ++ return new CraftCustomBotAction(getName(), newRealAction); ++ } ++ return null; ++ } ++ ++ @Override ++ public int getNumber() { ++ return realAction.getNumber(); ++ } ++ ++ @Override ++ public int getTickDelay() { ++ return realAction.getTickDelay(); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ return realAction.doTick(bot.getBukkitPlayer()); ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/DropAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/DropAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cc72960b8490a72aca5db3e834c71f97e3742f7d +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/DropAction.java +@@ -0,0 +1,26 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++ ++public class DropAction extends BotAction { ++ ++ public DropAction() { ++ super("drop", new CommandArgument()); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return this.setTickDelay(0).setNumber(1); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ bot.dropAll(); ++ return true; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/FishAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/FishAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d92ea54770bce73c2f10f1ebcb0dff5b9532e0e9 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/FishAction.java +@@ -0,0 +1,70 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.entity.projectile.FishingHook; ++import net.minecraft.world.item.FishingRodItem; ++import net.minecraft.world.item.ItemStack; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++import top.leavesmc.leaves.command.CommandArgumentType; ++ ++import java.util.List; ++ ++public class FishAction extends BotAction { ++ ++ public FishAction() { ++ super("fish", new CommandArgument(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER)); ++ setTabComplete(0, List.of("[TickDelay]")); ++ setTabComplete(1, List.of("[DoNumber]")); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return new FishAction().setTickDelay(result.readInt(20)).setNumber(result.readInt(-1)); ++ } ++ ++ @Override ++ public BotAction setTickDelay(int tickDelay) { ++ super.setTickDelay(0); ++ this.delay = tickDelay; ++ return this; ++ } ++ ++ private int delay = 0; ++ private int nowDelay = 0; ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ if (nowDelay > 0) { ++ nowDelay--; ++ return false; ++ } ++ ++ ItemStack mainHand = bot.getMainHandItem(); ++ if (mainHand == ItemStack.EMPTY || mainHand.getItem().getClass() != FishingRodItem.class) { ++ return false; ++ } ++ ++ FishingHook fishingHook = bot.fishing; ++ if (fishingHook != null) { ++ if (fishingHook.currentState == FishingHook.FishHookState.HOOKED_IN_ENTITY) { ++ mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND); ++ nowDelay = 20; ++ return false; ++ } ++ if (fishingHook.nibble > 0) { ++ mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND); ++ nowDelay = delay; ++ return true; ++ } ++ } else { ++ mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND); ++ } ++ ++ return false; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/JumpAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/JumpAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d99f667992e45e85c0fe0bd74682d563fe1315eb +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/JumpAction.java +@@ -0,0 +1,35 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++import top.leavesmc.leaves.command.CommandArgumentType; ++ ++import java.util.List; ++ ++public class JumpAction extends BotAction { ++ ++ public JumpAction() { ++ super("jump", new CommandArgument(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER)); ++ setTabComplete(0, List.of("[TickDelay]")); ++ setTabComplete(1, List.of("[DoNumber]")); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return new JumpAction().setTickDelay(result.readInt(20)).setNumber(result.readInt(-1)); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ if (bot.onGround()) { ++ bot.jumpFromGround(); ++ return true; ++ } else { ++ return false; ++ } ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/LookAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/LookAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5432e61c156a1a6d49dcf4b24e3bcfcc6c1aa7bb +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/LookAction.java +@@ -0,0 +1,49 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import org.bukkit.util.Vector; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++import top.leavesmc.leaves.command.CommandArgumentType; ++ ++import java.util.List; ++ ++public class LookAction extends BotAction { ++ ++ public LookAction() { ++ super("look", new CommandArgument(CommandArgumentType.DOUBLE, CommandArgumentType.DOUBLE, CommandArgumentType.DOUBLE)); ++ setTabComplete(0, List.of("")); ++ setTabComplete(1, List.of("")); ++ setTabComplete(2, List.of("")); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ Vector pos = result.readVector(); ++ if (pos != null) { ++ return new LookAction().setPos(pos).setTickDelay(0).setNumber(1); ++ } else { ++ return null; ++ } ++ } ++ ++ private Vector pos; ++ ++ public LookAction setPos(Vector pos) { ++ if (pos != null) { ++ this.pos = pos; ++ return this; ++ } else { ++ return null; ++ } ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ bot.look(pos.subtract(bot.getLocation().toVector()), false); ++ return true; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/RotateAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/RotateAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1e5be54b33467591924cb2400639fb593dc50ec6 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/RotateAction.java +@@ -0,0 +1,33 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++ ++public class RotateAction extends BotAction { ++ ++ public RotateAction() { ++ super("rotate", new CommandArgument()); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return new RotateAction().setPlayer(player).setTickDelay(0).setNumber(1); ++ } ++ ++ private ServerPlayer player; ++ ++ public RotateAction setPlayer(ServerPlayer player) { ++ this.player = player; ++ return this; ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ bot.faceLocation(player.getBukkitEntity().getLocation()); ++ return true; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/SneakAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/SneakAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..18e276c0252fa867b2cdc9770e3a7ed0b9cc63de +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/SneakAction.java +@@ -0,0 +1,27 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.entity.Pose; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++ ++public class SneakAction extends BotAction { ++ ++ public SneakAction() { ++ super("sneak", new CommandArgument()); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return this.setTickDelay(0).setNumber(1); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ bot.setShiftKeyDown(!bot.isShiftKeyDown()); ++ return true; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/StopAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/StopAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..36d199269afc46783b0815e3887842cd82b6e813 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/StopAction.java +@@ -0,0 +1,26 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++ ++public class StopAction extends BotAction { ++ ++ public StopAction() { ++ super("stop", new CommandArgument()); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return this.setTickDelay(0).setNumber(0); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ this.setCancel(true); ++ return true; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/SwimAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/SwimAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..58c815bd0ebfd455fcf4903ee5ced6b81be00982 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/SwimAction.java +@@ -0,0 +1,26 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++ ++public class SwimAction extends BotAction { ++ ++ public SwimAction() { ++ super("swim", new CommandArgument()); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return this.setTickDelay(0).setNumber(1); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ bot.waterSwim = !bot.waterSwim; ++ return true; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5dc3fbf8e62ccffc8291962c835a568efd65d7af +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemAction.java +@@ -0,0 +1,33 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.InteractionHand; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++import top.leavesmc.leaves.command.CommandArgumentType; ++ ++import java.util.List; ++ ++public class UseItemAction extends BotAction { ++ ++ public UseItemAction() { ++ super("use", new CommandArgument(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER)); ++ setTabComplete(0, List.of("[TickDelay]")); ++ setTabComplete(1, List.of("[DoNumber]")); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return new UseItemAction().setTickDelay(result.readInt(20)).setNumber(result.readInt(-1)); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ bot.punch(); ++ bot.updateItemInMainHand(); ++ return bot.getInventory().getSelected().use(bot.level(), bot, InteractionHand.MAIN_HAND).getResult().consumesAction(); ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..345932e779f5187355ca722c2bb9b05f384660a1 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java +@@ -0,0 +1,33 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.InteractionHand; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++import top.leavesmc.leaves.command.CommandArgumentType; ++ ++import java.util.List; ++ ++public class UseItemOffHandAction extends BotAction { ++ ++ public UseItemOffHandAction() { ++ super("use_offhand", new CommandArgument(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER)); ++ setTabComplete(0, List.of("[TickDelay]")); ++ setTabComplete(1, List.of("[DoNumber]")); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return new UseItemOffHandAction().setTickDelay(result.readInt(20)).setNumber(result.readInt(-1)); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ bot.punch(); ++ bot.updateItemInOffHand(); ++ return bot.getInventory().getSelected().use(bot.level(), bot, InteractionHand.OFF_HAND).getResult().consumesAction(); ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f4837f60909763df89ea7474f70dd0236360e657 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java +@@ -0,0 +1,56 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.level.ClipContext; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.entity.BlockEntity; ++import net.minecraft.world.level.block.entity.TrappedChestBlockEntity; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.phys.BlockHitResult; ++import net.minecraft.world.phys.HitResult; ++import org.bukkit.Bukkit; ++import org.bukkit.craftbukkit.scheduler.CraftScheduler; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++import top.leavesmc.leaves.command.CommandArgumentType; ++ ++import java.util.List; ++ ++public class UseItemOnAction extends BotAction { ++ ++ public UseItemOnAction() { ++ super("use_on", new CommandArgument(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER)); ++ setTabComplete(0, List.of("[TickDelay]")); ++ setTabComplete(1, List.of("[DoNumber]")); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return new UseItemOnAction().setTickDelay(result.readInt(20)).setNumber(result.readInt(-1)); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ HitResult result = bot.getRayTrace(5, ClipContext.Fluid.NONE); ++ if (result instanceof BlockHitResult blockHitResult) { ++ BlockState state = bot.serverLevel().getBlockState(blockHitResult.getBlockPos()); ++ bot.punch(); ++ if (state.getBlock() == Blocks.TRAPPED_CHEST) { ++ BlockEntity entity = bot.serverLevel().getBlockEntity(blockHitResult.getBlockPos()); ++ if (entity instanceof TrappedChestBlockEntity chestBlockEntity) { ++ chestBlockEntity.startOpen(bot); ++ Bukkit.getScheduler().runTaskLater(CraftScheduler.MINECRAFT, () -> chestBlockEntity.stopOpen(bot), 1); ++ return true; ++ } ++ } else { ++ bot.updateItemInMainHand(); ++ return bot.gameMode.useItemOn(bot, bot.level(), bot.getItemInHand(InteractionHand.MAIN_HAND), InteractionHand.MAIN_HAND, (BlockHitResult) result).consumesAction(); ++ } ++ } ++ return false; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4ab84fba3624a8e8c4d345c03fe678a012a5c367 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java +@@ -0,0 +1,56 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.level.ClipContext; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.entity.BlockEntity; ++import net.minecraft.world.level.block.entity.TrappedChestBlockEntity; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.phys.BlockHitResult; ++import net.minecraft.world.phys.HitResult; ++import org.bukkit.Bukkit; ++import org.bukkit.craftbukkit.scheduler.CraftScheduler; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++import top.leavesmc.leaves.command.CommandArgumentType; ++ ++import java.util.List; ++ ++public class UseItemOnOffhandAction extends BotAction { ++ ++ public UseItemOnOffhandAction() { ++ super("use_on_offhand", new CommandArgument(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER)); ++ setTabComplete(0, List.of("[TickDelay]")); ++ setTabComplete(1, List.of("[DoNumber]")); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return new UseItemOnOffhandAction().setTickDelay(result.readInt(20)).setNumber(result.readInt(-1)); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ HitResult result = bot.getRayTrace(5, ClipContext.Fluid.NONE); ++ if (result instanceof BlockHitResult blockHitResult) { ++ BlockState state = bot.serverLevel().getBlockState(blockHitResult.getBlockPos()); ++ bot.punch(); ++ if (state.getBlock() == Blocks.TRAPPED_CHEST) { ++ BlockEntity entity = bot.serverLevel().getBlockEntity(blockHitResult.getBlockPos()); ++ if (entity instanceof TrappedChestBlockEntity chestBlockEntity) { ++ chestBlockEntity.startOpen(bot); ++ Bukkit.getScheduler().runTaskLater(CraftScheduler.MINECRAFT, () -> chestBlockEntity.stopOpen(bot), 1); ++ return true; ++ } ++ } else { ++ bot.updateItemInMainHand(); ++ return bot.gameMode.useItemOn(bot, bot.level(), bot.getItemInHand(InteractionHand.OFF_HAND), InteractionHand.OFF_HAND, (BlockHitResult) result).consumesAction(); ++ } ++ } ++ return false; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemToAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemToAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cc8689ee726144f220e4ccc5cd418b79a29b79ab +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemToAction.java +@@ -0,0 +1,38 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.phys.EntityHitResult; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++import top.leavesmc.leaves.command.CommandArgumentType; ++ ++import java.util.List; ++ ++public class UseItemToAction extends BotAction { ++ ++ public UseItemToAction() { ++ super("use_to", new CommandArgument(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER)); ++ setTabComplete(0, List.of("[TickDelay]")); ++ setTabComplete(1, List.of("[DoNumber]")); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return new UseItemToAction().setTickDelay(result.readInt(20)).setNumber(result.readInt(-1)); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ EntityHitResult result = bot.getTargetEntity(3); ++ if (result != null) { ++ bot.punch(); ++ bot.updateItemInMainHand(); ++ return result.getEntity().interact(bot, InteractionHand.MAIN_HAND).consumesAction(); ++ } ++ return false; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java b/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1fde496c993ec0c961a63b32a8088479da88c91d +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java +@@ -0,0 +1,38 @@ ++package top.leavesmc.leaves.bot.agent.actions; ++ ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.phys.EntityHitResult; ++import org.jetbrains.annotations.NotNull; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.command.CommandArgument; ++import top.leavesmc.leaves.command.CommandArgumentResult; ++import top.leavesmc.leaves.command.CommandArgumentType; ++ ++import java.util.List; ++ ++public class UseItemToOffhandAction extends BotAction { ++ ++ public UseItemToOffhandAction() { ++ super("use_to_offhand", new CommandArgument(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER)); ++ setTabComplete(0, List.of("[TickDelay]")); ++ setTabComplete(1, List.of("[DoNumber]")); ++ } ++ ++ @Override ++ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) { ++ return new UseItemToOffhandAction().setTickDelay(result.readInt(20)).setNumber(result.readInt(-1)); ++ } ++ ++ @Override ++ public boolean doTick(@NotNull ServerBot bot) { ++ EntityHitResult result = bot.getTargetEntity(3); ++ if (result != null) { ++ bot.punch(); ++ bot.updateItemInOffHand(); ++ return result.getEntity().interact(bot, InteractionHand.OFF_HAND).consumesAction(); ++ } ++ return false; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/command/CommandArgument.java b/src/main/java/top/leavesmc/leaves/command/CommandArgument.java +index eadc6d168fb13299348b0c275ae352ee2f1e1ea2..134c6d26acc612bf6142ae6b6885a0ee53d2a196 100644 +--- a/src/main/java/top/leavesmc/leaves/command/CommandArgument.java ++++ b/src/main/java/top/leavesmc/leaves/command/CommandArgument.java +@@ -32,6 +32,12 @@ public class CommandArgument { + return this; + } + ++ public CommandArgument setAllTabComplete(List> tabComplete) { ++ this.tabComplete.clear(); ++ this.tabComplete.addAll(tabComplete); ++ return this; ++ } ++ + public CommandArgumentResult parse(int index, String @NotNull [] args) { + Object[] result = new Object[argumentTypes.size()]; + Arrays.fill(result, null); +diff --git a/src/main/java/top/leavesmc/leaves/command/CommandArgumentResult.java b/src/main/java/top/leavesmc/leaves/command/CommandArgumentResult.java +index 340eaca64c96180b895a075ce9e44402cd104eed..39e90dcff0de259373d7955021c29397c2cc15d5 100644 +--- a/src/main/java/top/leavesmc/leaves/command/CommandArgumentResult.java ++++ b/src/main/java/top/leavesmc/leaves/command/CommandArgumentResult.java +@@ -58,5 +58,4 @@ public class CommandArgumentResult { + return null; + } + } +- + } +diff --git a/src/main/java/top/leavesmc/leaves/entity/CraftBot.java b/src/main/java/top/leavesmc/leaves/entity/CraftBot.java +new file mode 100644 +index 0000000000000000000000000000000000000000..713240da3ba37915b455d952a45ae7f68b8294ee +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/entity/CraftBot.java +@@ -0,0 +1,67 @@ ++package top.leavesmc.leaves.entity; ++ ++import org.bukkit.craftbukkit.CraftServer; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.bukkit.entity.Player; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.Actions; ++import top.leavesmc.leaves.bot.agent.BotAction; ++import top.leavesmc.leaves.entity.botaction.LeavesBotAction; ++ ++import java.util.UUID; ++ ++public class CraftBot extends CraftPlayer implements Bot { ++ ++ public CraftBot(CraftServer server, ServerBot entity) { ++ super(server, entity); ++ } ++ ++ @Override ++ public String getSkinName() { ++ return getHandle().createState.skinName; ++ } ++ ++ @Override ++ public @NotNull String getRealName() { ++ return getHandle().createState.getRealName(); ++ } ++ ++ @Override ++ public @Nullable UUID getCreatePlayerUUID() { ++ return getHandle().createPlayer; ++ } ++ ++ @Override ++ public boolean setBotAction(@NotNull String action, @NotNull Player player, @NotNull String[] args) { ++ BotAction botAction = Actions.getForName(action); ++ if (botAction != null) { ++ BotAction newAction = botAction.getNew(((CraftPlayer) player).getHandle(), botAction.getArgument().parse(0, args)); ++ if (newAction != null) { ++ getHandle().setBotAction(newAction); ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ @Override ++ public boolean setBotAction(@NotNull LeavesBotAction action, @NotNull Player player, @NotNull String[] args) { ++ return setBotAction(action.getName(), player, args); ++ } ++ ++ @Override ++ public ServerBot getHandle() { ++ return (ServerBot) entity; ++ } ++ ++ public void setHandle(final ServerBot entity) { ++ super.setHandle(entity); ++ } ++ ++ @Override ++ public String toString() { ++ return "CraftBot{" + "name=" + getName() + '}'; ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/entity/CraftBotManager.java b/src/main/java/top/leavesmc/leaves/entity/CraftBotManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..df5796bfa333a287ccd486be9a9cdae4ca5dc757 +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/entity/CraftBotManager.java +@@ -0,0 +1,93 @@ ++package top.leavesmc.leaves.entity; ++ ++import com.google.common.base.Function; ++import com.google.common.collect.Lists; ++import org.bukkit.Location; ++import org.bukkit.util.Consumer; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++import top.leavesmc.leaves.bot.ServerBot; ++import top.leavesmc.leaves.bot.agent.Actions; ++import top.leavesmc.leaves.bot.agent.actions.CraftCustomBotAction; ++import top.leavesmc.leaves.entity.botaction.CustomBotAction; ++ ++import java.util.Collection; ++import java.util.Collections; ++import java.util.UUID; ++ ++public class CraftBotManager implements BotManager { ++ ++ private final Collection botViews = Collections.unmodifiableList(Lists.transform(ServerBot.getBots(), new Function() { ++ @Override ++ public CraftBot apply(ServerBot bot) { ++ return bot.getBukkitEntity(); ++ } ++ })); ++ ++ @Override ++ public @Nullable Bot getBot(@NotNull UUID uuid) { ++ return ServerBot.getBot(uuid).getBukkitPlayer(); ++ } ++ ++ @Override ++ public @Nullable Bot getBot(@NotNull String name) { ++ return ServerBot.getBot(name).getBukkitPlayer(); ++ } ++ ++ @Override ++ public @Nullable Bot createBot(@NotNull String name, @NotNull String realName, @Nullable String[] skin, @Nullable String skinName, @NotNull Location location) { ++ ServerBot bot = new ServerBot.BotCreateState(location, name, realName, skinName, skin).createSync(); ++ if (bot != null) { ++ return bot.getBukkitPlayer(); ++ } ++ return null; ++ } ++ ++ @Override ++ public void createBot(@NotNull String name, @Nullable String skinName, @NotNull Location location, Consumer consumer) { ++ new ServerBot.BotCreateState(location, name, skinName).createAsync((serverBot -> { ++ consumer.accept(serverBot.getBukkitPlayer()); ++ })); ++ } ++ ++ @Override ++ public void removeBot(@NotNull String name) { ++ ServerBot bot = ServerBot.getBot(name); ++ if (bot != null) { ++ bot.die(bot.damageSources().fellOutOfWorld()); ++ } ++ } ++ ++ @Override ++ public void removeBot(@NotNull UUID uuid) { ++ ServerBot bot = ServerBot.getBot(uuid); ++ if (bot != null) { ++ bot.die(bot.damageSources().fellOutOfWorld()); ++ } ++ } ++ ++ @Override ++ public void removeAllBots() { ++ ServerBot.removeAllBot(); ++ } ++ ++ @Override ++ public void saveOrRemoveAllBots() { ++ ServerBot.saveOrRemoveAllBot(); ++ } ++ ++ @Override ++ public Collection getBots() { ++ return botViews; ++ } ++ ++ @Override ++ public boolean registerCustomBotAction(String name, CustomBotAction action) { ++ return Actions.register(new CraftCustomBotAction(name, action)); ++ } ++ ++ @Override ++ public boolean unregisterCustomBotAction(String name) { ++ return Actions.unregister(name); ++ } ++} +diff --git a/src/main/java/top/leavesmc/leaves/util/MathUtils.java b/src/main/java/top/leavesmc/leaves/util/MathUtils.java +new file mode 100644 +index 0000000000000000000000000000000000000000..349cd0c0d2d9dc2c9c745ef3469e548a798931ba +--- /dev/null ++++ b/src/main/java/top/leavesmc/leaves/util/MathUtils.java +@@ -0,0 +1,78 @@ ++package top.leavesmc.leaves.util; ++ ++import org.bukkit.util.NumberConversions; ++import org.bukkit.util.Vector; ++ ++import java.util.regex.Pattern; ++ ++public class MathUtils { ++ // Lag ? ++ public static void clean(Vector vector) { ++ if (!NumberConversions.isFinite(vector.getX())) vector.setX(0); ++ if (!NumberConversions.isFinite(vector.getY())) vector.setY(0); ++ if (!NumberConversions.isFinite(vector.getZ())) vector.setZ(0); ++ } ++ ++ private static final Pattern numericPattern = Pattern.compile("^-?[1-9]\\d*$|^0$"); ++ public static boolean isNumeric(String str){ ++ return numericPattern.matcher(str).matches(); ++ } ++ ++ public static float[] fetchYawPitch(Vector dir) { ++ double x = dir.getX(); ++ double z = dir.getZ(); ++ ++ float[] out = new float[2]; ++ ++ if (x == 0.0D && z == 0.0D) { ++ out[1] = (float) (dir.getY() > 0.0D ? -90 : 90); ++ } ++ ++ else { ++ double theta = Math.atan2(-x, z); ++ out[0] = (float) Math.toDegrees((theta + 6.283185307179586D) % 6.283185307179586D); ++ ++ double x2 = NumberConversions.square(x); ++ double z2 = NumberConversions.square(z); ++ double xz = Math.sqrt(x2 + z2); ++ out[1] = (float) Math.toDegrees(Math.atan(-dir.getY() / xz)); ++ } ++ ++ return out; ++ } ++ ++ public static float fetchPitch(Vector dir) { ++ double x = dir.getX(); ++ double z = dir.getZ(); ++ ++ float result; ++ ++ if (x == 0.0D && z == 0.0D) { ++ result = (float) (dir.getY() > 0.0D ? -90 : 90); ++ } ++ ++ else { ++ double x2 = NumberConversions.square(x); ++ double z2 = NumberConversions.square(z); ++ double xz = Math.sqrt(x2 + z2); ++ result = (float) Math.toDegrees(Math.atan(-dir.getY() / xz)); ++ } ++ ++ return result; ++ } ++ ++ public static Vector getDirection(double rotX, double rotY) { ++ Vector vector = new Vector(); ++ ++ rotX = Math.toRadians(rotX); ++ rotY = Math.toRadians(rotY); ++ ++ double xz = Math.abs(Math.cos(rotY)); ++ ++ vector.setX(-Math.sin(rotX) * xz); ++ vector.setZ(Math.cos(rotX) * xz); ++ vector.setY(-Math.sin(rotY)); ++ ++ return vector; ++ } ++}