diff --git a/patches/api/0003-Add-fakeplayer-api.patch b/patches/api/0003-Add-fakeplayer-api.patch
index a843538e..62003ac4 100644
--- a/patches/api/0003-Add-fakeplayer-api.patch
+++ b/patches/api/0003-Add-fakeplayer-api.patch
@@ -63,10 +63,10 @@ index 594deedd08c3b3255fe6838471d945759f09a182..6fa638198f75458177af795f00250ce9
}
diff --git a/src/main/java/org/leavesmc/leaves/entity/Bot.java b/src/main/java/org/leavesmc/leaves/entity/Bot.java
new file mode 100644
-index 0000000000000000000000000000000000000000..922ca5b27bc0dd443d635646f37f879559cc0252
+index 0000000000000000000000000000000000000000..7a1ee45d571687317883b896f3ec0a837a8ef450
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/entity/Bot.java
-@@ -0,0 +1,51 @@
+@@ -0,0 +1,80 @@
+package org.leavesmc.leaves.entity;
+
+import org.bukkit.entity.Player;
@@ -97,33 +97,120 @@ index 0000000000000000000000000000000000000000..922ca5b27bc0dd443d635646f37f8795
+ @NotNull
+ public String getRealName();
+
++ /**
++ * Gets the creator's UUID of the fakeplayer
++ *
++ * @return creator's UUID
++ */
+ @Nullable
+ public UUID getCreatePlayerUUID();
+
+ /**
-+ * Sets the fakeplayer action with args.
++ * Add an action to the fakeplayer
+ *
-+ * @param action action name
-+ * @param player player who create this action
-+ * @param args passed action arguments
++ * @param action bot action
+ */
-+ public boolean setBotAction(@NotNull String action, @NotNull Player player, @NotNull String[] args);
++ public void addAction(@NotNull LeavesBotAction action);
+
+ /**
-+ * Sets the fakeplayer action with args.
++ * Get the copy action in giving index
+ *
-+ * @param action leaves bot action
-+ * @param player player who create this action
-+ * @param args passed action arguments
++ * @param index index of actions
++ * @return Action of that index
++ */
++ public LeavesBotAction getAction(int index);
++
++ /**
++ * Get action size
++ *
++ * @return size
++ */
++ public int getActionSize();
++
++ /**
++ * Stop the action in giving index
++ *
++ * @param index index of actions
++ */
++ public void stopAction(int index);
++
++ /**
++ * Stop all the actions of the fakeplayer
++ */
++ public void stopAllActions();
++
++ /**
++ * Remove the fakeplayer
++ *
++ * @param save should save
++ * @return success
++ */
++ public boolean remove(boolean save);
++}
+diff --git a/src/main/java/org/leavesmc/leaves/entity/BotCreator.java b/src/main/java/org/leavesmc/leaves/entity/BotCreator.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..98c7e87854eae9760a6f4427c6b052b192df2b45
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/entity/BotCreator.java
+@@ -0,0 +1,52 @@
++package org.leavesmc.leaves.entity;
++
++import org.bukkit.Bukkit;
++import org.bukkit.Location;
++import org.bukkit.command.CommandSender;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++import java.util.function.Consumer;
++
++public interface BotCreator {
++
++ default BotCreator of(String realName, Location location) {
++ return Bukkit.getBotManager().botCreator(realName, location);
++ }
++
++ public BotCreator name(String name);
++
++ public BotCreator skinName(String skinName);
++
++ public BotCreator skin(String[] skin);
++
++ /**
++ * Sets the skin of the bot using the Mojang API based on the provided skin name.
++ *
++ * Need Async.
++ *
++ * @return BotCreator
+ */
-+ public boolean setBotAction(@NotNull LeavesBotAction action, @NotNull Player player, @NotNull String[] args);
++ public BotCreator mojangAPISkin();
++
++ public BotCreator location(@NotNull Location location);
++
++ public BotCreator creator(@Nullable CommandSender creator);
++
++ /**
++ * Create a bot directly
++ *
++ * @return a bot, null spawn fail
++ */
++ @Nullable
++ public Bot spawn();
++
++ /**
++ * Create a bot and apply skin
++ *
++ * you can not get the bot instance instantly because get skin in on async thread
++ *
++ * @param consumer Consumer
++ */
++ public void spawnWithSkin(Consumer consumer);
+}
diff --git a/src/main/java/org/leavesmc/leaves/entity/BotManager.java b/src/main/java/org/leavesmc/leaves/entity/BotManager.java
new file mode 100644
-index 0000000000000000000000000000000000000000..ee6848b8990c516aa5c5490546dd16ae5c909740
+index 0000000000000000000000000000000000000000..105fc912c2cba717b6533771bfd85eee83954fef
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/entity/BotManager.java
-@@ -0,0 +1,124 @@
+@@ -0,0 +1,59 @@
+package org.leavesmc.leaves.entity;
+
+import org.bukkit.Location;
@@ -133,7 +220,6 @@ index 0000000000000000000000000000000000000000..ee6848b8990c516aa5c5490546dd16ae
+
+import java.util.Collection;
+import java.util.UUID;
-+import java.util.function.Consumer;
+
+/**
+ * Simple fakeplayer manager
@@ -159,72 +245,6 @@ index 0000000000000000000000000000000000000000..ee6848b8990c516aa5c5490546dd16ae
+ 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
-+ */
-+ @Deprecated(since = "1.21")
-+ @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.
-+ *
-+ * 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
-+ * @param consumer a consumer after create fakeplayer success
-+ * @return a fakeplayer if you support skin arr and the creation is success, null otherwise
-+ */
-+ @Nullable
-+ public Bot createBot(@NotNull String name, @NotNull String realName, @NotNull String[] skin, @Nullable String skinName, @NotNull Location location, @Nullable Consumer consumer);
-+
-+ /**
-+ * 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.
@@ -234,7 +254,7 @@ index 0000000000000000000000000000000000000000..ee6848b8990c516aa5c5490546dd16ae
+ /**
+ * Register a custom bot action.
+ *
-+ * @param name action name
++ * @param name action name
+ * @param action action executor
+ * @return true if success, or false
+ */
@@ -247,13 +267,55 @@ index 0000000000000000000000000000000000000000..ee6848b8990c516aa5c5490546dd16ae
+ * @return true if success, or false
+ */
+ public boolean unregisterCustomBotAction(String name);
++
++ public BotCreator botCreator(@NotNull String realName, @NotNull Location location);
++}
+diff --git a/src/main/java/org/leavesmc/leaves/entity/botaction/BotActionType.java b/src/main/java/org/leavesmc/leaves/entity/botaction/BotActionType.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..601a40b28211027db66063681130c0f25fd62152
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/entity/botaction/BotActionType.java
+@@ -0,0 +1,34 @@
++package org.leavesmc.leaves.entity.botaction;
++
++/**
++ * A Leaves bot action enum
++ */
++public enum BotActionType {
++ ATTACK("attack"),
++ BREAK("break"),
++ DROP("drop"),
++ FISH("fish"),
++ JUMP("jump"),
++ LOOK("look"),
++ ROTATE("rotate"),
++ ROTATION("rotation"),
++ SNEAK("sneak"),
++ STOP("stop"),
++ SWIM("swim"),
++ USE("use"),
++ USE_ON("use_on"),
++ USE_TO("use_to"),
++ USE_OFFHAND("use_offhand"),
++ USE_ON_OFFHAND("use_on_offhand"),
++ USE_TO_OFFHAND("use_to_offhand");
++
++ private final String name;
++
++ private BotActionType(String name) {
++ this.name = name;
++ }
++
++ public String getName() {
++ return name;
++ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/entity/botaction/CustomBotAction.java b/src/main/java/org/leavesmc/leaves/entity/botaction/CustomBotAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..0b1648013d5f03d064c0719c231981082ab563be
+index 0000000000000000000000000000000000000000..c952d7ddaf457f79d66baf42557e75600a471f81
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/entity/botaction/CustomBotAction.java
-@@ -0,0 +1,52 @@
+@@ -0,0 +1,54 @@
+package org.leavesmc.leaves.entity.botaction;
+
+import org.bukkit.entity.Player;
@@ -283,14 +345,16 @@ index 0000000000000000000000000000000000000000..0b1648013d5f03d064c0719c23198108
+ * @param args passed action arguments
+ * @return a new action instance with given args
+ */
-+ public @Nullable CustomBotAction getNew(Player player, String[] args);
++ @Nullable
++ public CustomBotAction getNew(@Nullable 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();
++ @NotNull
++ public List> getTabComplete();
+
+ /**
+ * Return a ticks to wait between {@link CustomBotAction#doTick(Bot)}
@@ -308,75 +372,105 @@ index 0000000000000000000000000000000000000000..0b1648013d5f03d064c0719c23198108
+}
diff --git a/src/main/java/org/leavesmc/leaves/entity/botaction/LeavesBotAction.java b/src/main/java/org/leavesmc/leaves/entity/botaction/LeavesBotAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..a6fdf8c77318172621494af94a0efb2bd34ca651
+index 0000000000000000000000000000000000000000..8a73f5cc673a95c434677ad7578abfb5402118e3
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/entity/botaction/LeavesBotAction.java
-@@ -0,0 +1,36 @@
+@@ -0,0 +1,73 @@
+package org.leavesmc.leaves.entity.botaction;
+
-+/**
-+ * A Leaves bot action enum
-+ */
-+public enum LeavesBotAction {
-+ ATTACK("attack"),
-+ @Deprecated(since = "1.21.1", forRemoval = true)
-+ 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"),
-+ USE_OFFHAND("use_offhand"),
-+ USE_ON_OFFHAND("use_on_offhand"),
-+ USE_TO_OFFHAND("use_to_offhand");
++import org.bukkit.entity.Player;
++import org.jetbrains.annotations.Nullable;
+
-+ private final String name;
++import java.util.UUID;
+
-+ private LeavesBotAction(String name) {
-+ this.name = name;
++public class LeavesBotAction {
++
++ private final String actionName;
++ private int tickToExecute;
++ private int executeInterval;
++ private int remainingExecuteTime;
++ private final UUID uuid;
++ private Player actionPlayer;
++
++ public LeavesBotAction(BotActionType type, int executeInterval, int remainingExecuteTime) {
++ this(type.getName(), executeInterval, remainingExecuteTime, UUID.randomUUID());
+ }
+
-+ public String getName() {
-+ return name;
++ public LeavesBotAction(String name, int executeInterval, int remainingExecuteTime) {
++ this(name, executeInterval, remainingExecuteTime, UUID.randomUUID());
++ }
++
++ protected LeavesBotAction(String name, int executeInterval, int remainingExecuteTime, UUID actionUUID) {
++ this.actionName = name;
++ this.remainingExecuteTime = remainingExecuteTime;
++ this.executeInterval = executeInterval;
++ this.uuid = actionUUID;
++ this.tickToExecute = executeInterval;
++ }
++
++ public void setTickToExecute(int tickToExecute) {
++ this.tickToExecute = tickToExecute;
++ }
++
++ public int getTickToExecute() {
++ return tickToExecute;
++ }
++
++ public void setExecuteInterval(int executeInterval) {
++ this.executeInterval = executeInterval;
++ }
++
++ public int getExecuteInterval() {
++ return executeInterval;
++ }
++
++ public void setRemainingExecuteTime(int remainingExecuteTime) {
++ this.remainingExecuteTime = remainingExecuteTime;
++ }
++
++ public int getRemainingExecuteTime() {
++ return remainingExecuteTime;
++ }
++
++ public String getActionName() {
++ return actionName;
++ }
++
++ public void setActionPlayer(@Nullable Player actionPlayer) {
++ this.actionPlayer = actionPlayer;
++ }
++
++ @Nullable
++ public Player getActionPlayer() {
++ return actionPlayer;
++ }
++
++ public UUID getUuid() {
++ return uuid;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/event/bot/BotActionEvent.java b/src/main/java/org/leavesmc/leaves/event/bot/BotActionEvent.java
new file mode 100644
-index 0000000000000000000000000000000000000000..91ea5540387b7d7e1be5b6368a2f02b3b784614a
+index 0000000000000000000000000000000000000000..1818aa77f8b051a00b4dbc3da0907cf3462ffcbb
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/event/bot/BotActionEvent.java
-@@ -0,0 +1,49 @@
+@@ -0,0 +1,27 @@
+package org.leavesmc.leaves.event.bot;
+
-+import org.bukkit.event.Cancellable;
-+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.entity.Bot;
+
-+public class BotActionEvent extends BotEvent implements Cancellable {
-+ private static final HandlerList handlers = new HandlerList();
++import java.util.UUID;
++
++public abstract class BotActionEvent extends BotEvent {
+
+ private final String actionName;
-+ private final String[] actionArgs;
-+ private boolean cancel = false;
++ private final UUID actionUUID;
+
-+ public BotActionEvent(@NotNull Bot who, String actionName, String[] actionArgs) {
++ public BotActionEvent(@NotNull Bot who, String actionName, UUID actionUUID) {
+ super(who);
-+ this.actionArgs = actionArgs;
+ this.actionName = actionName;
-+ }
-+
-+ @NotNull
-+ public String[] getActionArgs() {
-+ return actionArgs;
++ this.actionUUID = actionUUID;
+ }
+
+ @NotNull
@@ -384,11 +478,93 @@ index 0000000000000000000000000000000000000000..91ea5540387b7d7e1be5b6368a2f02b3
+ return actionName;
+ }
+
++ public UUID getActionUUID() {
++ return actionUUID;
++ }
++}
+diff --git a/src/main/java/org/leavesmc/leaves/event/bot/BotActionExecuteEvent.java b/src/main/java/org/leavesmc/leaves/event/bot/BotActionExecuteEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..69a99679d407f974ef0e414945d3bcc7a1a710ea
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/event/bot/BotActionExecuteEvent.java
+@@ -0,0 +1,52 @@
++package org.leavesmc.leaves.event.bot;
++
++import org.bukkit.event.Cancellable;
++import org.bukkit.event.HandlerList;
++import org.jetbrains.annotations.NotNull;
++import org.leavesmc.leaves.entity.Bot;
++
++import java.util.UUID;
++
++public class BotActionExecuteEvent extends BotActionEvent implements Cancellable {
++
++ private static final HandlerList handlers = new HandlerList();
++
++ public enum Result {
++ PASS, SOFT_CANCEL, HARD_CANCEL;
++
++ }
++
++ private Result result = Result.PASS;
++
++ public BotActionExecuteEvent(@NotNull Bot who, String actionName, UUID actionUUID) {
++ super(who, actionName, actionUUID);
++ }
++
++ @Override
++ public boolean isCancelled() {
++ return result != Result.PASS;
++ }
++
++ @Override
++ public void setCancelled(boolean cancel) {
++ this.result = cancel ? Result.SOFT_CANCEL : Result.PASS;
++ }
++
++ public void hardCancel() {
++ this.result = Result.HARD_CANCEL;
++ }
++
++ public Result getResult() {
++ return this.result;
++ }
++
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return handlers;
+ }
+
++ public static HandlerList getHandlerList() {
++ return handlers;
++ }
++}
++
+diff --git a/src/main/java/org/leavesmc/leaves/event/bot/BotActionScheduleEvent.java b/src/main/java/org/leavesmc/leaves/event/bot/BotActionScheduleEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7189649e608d41511d4213c1c3938996361290df
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/event/bot/BotActionScheduleEvent.java
+@@ -0,0 +1,39 @@
++package org.leavesmc.leaves.event.bot;
++
++import org.bukkit.event.Cancellable;
++import org.bukkit.event.HandlerList;
++import org.jetbrains.annotations.NotNull;
++import org.leavesmc.leaves.entity.Bot;
++
++import java.util.UUID;
++
++public class BotActionScheduleEvent extends BotActionEvent implements Cancellable {
++
++ private static final HandlerList handlers = new HandlerList();
++
++ private boolean cancel = false;
++
++ public BotActionScheduleEvent(@NotNull Bot who, String actionName, UUID actionUUID) {
++ super(who, actionName, actionUUID);
++ }
++
+ @Override
+ public boolean isCancelled() {
+ return cancel;
@@ -399,16 +575,64 @@ index 0000000000000000000000000000000000000000..91ea5540387b7d7e1be5b6368a2f02b3
+ this.cancel = cancel;
+ }
+
++ @Override
++ @NotNull
++ public HandlerList getHandlers() {
++ return handlers;
++ }
++
++ public static HandlerList getHandlerList() {
++ return handlers;
++ }
++}
+diff --git a/src/main/java/org/leavesmc/leaves/event/bot/BotActionStopEvent.java b/src/main/java/org/leavesmc/leaves/event/bot/BotActionStopEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..e8de7b19d72b3dfd6e4423096573b3a7ef737803
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/event/bot/BotActionStopEvent.java
+@@ -0,0 +1,36 @@
++package org.leavesmc.leaves.event.bot;
++
++import org.bukkit.event.HandlerList;
++import org.jetbrains.annotations.NotNull;
++import org.leavesmc.leaves.entity.Bot;
++
++import java.util.UUID;
++
++public class BotActionStopEvent extends BotActionEvent {
++
++ private static final HandlerList handlers = new HandlerList();
++
++ public enum Reason {
++ DONE, COMMAND, PLUGIN, INTERNAL
++ }
++
++ private final Reason reason;
++
++ public BotActionStopEvent(@NotNull Bot who, String actionName, UUID actionUUID, Reason stopReason) {
++ super(who, actionName, actionUUID);
++ this.reason = stopReason;
++ }
++
++ public Reason getReason() {
++ return reason;
++ }
++
++ @Override
++ public @NotNull HandlerList getHandlers() {
++ return handlers;
++ }
++
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/event/bot/BotConfigModifyEvent.java b/src/main/java/org/leavesmc/leaves/event/bot/BotConfigModifyEvent.java
new file mode 100644
-index 0000000000000000000000000000000000000000..5e55759fd3d7891e8e1d5d6a306dc8144d366469
+index 0000000000000000000000000000000000000000..053be37cb250d77b1c9f4c1bbd83a49da93027a7
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/event/bot/BotConfigModifyEvent.java
-@@ -0,0 +1,49 @@
+@@ -0,0 +1,50 @@
+package org.leavesmc.leaves.event.bot;
+
+import org.bukkit.event.Cancellable;
@@ -417,13 +641,14 @@ index 0000000000000000000000000000000000000000..5e55759fd3d7891e8e1d5d6a306dc814
+import org.leavesmc.leaves.entity.Bot;
+
+public class BotConfigModifyEvent extends BotEvent implements Cancellable {
++
+ private static final HandlerList handlers = new HandlerList();
+
+ private final String configName;
-+ private final String configValue;
++ private final String[] configValue;
+ private boolean cancel;
+
-+ public BotConfigModifyEvent(@NotNull Bot who, String configName, String configValue) {
++ public BotConfigModifyEvent(@NotNull Bot who, String configName, String[] configValue) {
+ super(who);
+ this.configName = configName;
+ this.configValue = configValue;
@@ -435,16 +660,11 @@ index 0000000000000000000000000000000000000000..5e55759fd3d7891e8e1d5d6a306dc814
+ }
+
+ @NotNull
-+ public String getConfigValue() {
++ public String[] getConfigValue() {
+ return configValue;
+ }
+
+ @Override
-+ public @NotNull HandlerList getHandlers() {
-+ return handlers;
-+ }
-+
-+ @Override
+ public boolean isCancelled() {
+ return cancel;
+ }
@@ -454,16 +674,21 @@ index 0000000000000000000000000000000000000000..5e55759fd3d7891e8e1d5d6a306dc814
+ this.cancel = cancel;
+ }
+
++ @Override
++ public @NotNull HandlerList getHandlers() {
++ return handlers;
++ }
++
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/event/bot/BotCreateEvent.java b/src/main/java/org/leavesmc/leaves/event/bot/BotCreateEvent.java
new file mode 100644
-index 0000000000000000000000000000000000000000..be510d565c5942efea3423190b06c01873a7abd2
+index 0000000000000000000000000000000000000000..08e382120feec65c2a842134a1643f236a120bbd
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/event/bot/BotCreateEvent.java
-@@ -0,0 +1,118 @@
+@@ -0,0 +1,119 @@
+package org.leavesmc.leaves.event.bot;
+
+import org.bukkit.Location;
@@ -474,23 +699,24 @@ index 0000000000000000000000000000000000000000..be510d565c5942efea3423190b06c018
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
-+import java.util.Optional;
-+
+/**
+ * Call when a fakeplayer creates a server
+ */
+public class BotCreateEvent extends Event implements Cancellable {
++
++ private static final HandlerList handlers = new HandlerList();
++
+ public enum CreateReason {
+ COMMAND,
+ PLUGIN,
-+ INTERNAL
++ INTERNAL,
++ UNKNOWN,
+ }
-+ private static final HandlerList handlers = new HandlerList();
+
+ private final String bot;
+ private final String skin;
+ private final CreateReason reason;
-+ private final Optional creator;
++ private final CommandSender creator;
+ private Location createLocation;
+ private boolean cancel = false;
+
@@ -499,7 +725,7 @@ index 0000000000000000000000000000000000000000..be510d565c5942efea3423190b06c018
+ this.skin = skin;
+ this.createLocation = createLocation;
+ this.reason = reason;
-+ this.creator = Optional.ofNullable(creator);
++ this.creator = creator;
+ }
+
+ /**
@@ -552,12 +778,12 @@ index 0000000000000000000000000000000000000000..be510d565c5942efea3423190b06c018
+
+ /**
+ * Gets the creator of the bot
-+ * if the create reason is not COMMAND, the creator might be Optional.empty()
++ * if the create reason is not COMMAND, the creator might be null
+ *
+ * @return An optional of creator
+ */
-+ @NotNull
-+ public Optional getCreator() {
++ @Nullable
++ public CommandSender getCreator() {
+ return creator;
+ }
+
@@ -582,12 +808,87 @@ index 0000000000000000000000000000000000000000..be510d565c5942efea3423190b06c018
+ return handlers;
+ }
+}
+diff --git a/src/main/java/org/leavesmc/leaves/event/bot/BotDeathEvent.java b/src/main/java/org/leavesmc/leaves/event/bot/BotDeathEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3366b50cf1835129a027b5342e4d8cf070cecf4a
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/event/bot/BotDeathEvent.java
+@@ -0,0 +1,69 @@
++package org.leavesmc.leaves.event.bot;
++
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
++import org.bukkit.event.Cancellable;
++import org.bukkit.event.HandlerList;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++import org.leavesmc.leaves.entity.Bot;
++
++public class BotDeathEvent extends BotEvent implements Cancellable {
++
++ private static final HandlerList handlers = new HandlerList();
++
++ private boolean cancel = false;
++ private boolean sendDeathMessage;
++ private Component deathMessage;
++
++ public BotDeathEvent(@NotNull Bot who, @Nullable Component deathMessage, boolean sendDeathMessage) {
++ super(who);
++ this.deathMessage = deathMessage;
++ this.sendDeathMessage = sendDeathMessage;
++ }
++
++ @Override
++ public @NotNull HandlerList getHandlers() {
++ return handlers;
++ }
++
++ @NotNull
++ public static HandlerList getHandlerList() {
++ return handlers;
++ }
++
++ public Component deathMessage() {
++ return deathMessage;
++ }
++
++ public void deathMessage(Component deathMessage) {
++ this.deathMessage = deathMessage;
++ }
++
++ @Nullable
++ public String getDeathMessage() {
++ return this.deathMessage == null ? null : LegacyComponentSerializer.legacySection().serialize(this.deathMessage);
++ }
++
++ public void setDeathMessage(@Nullable String deathMessage) {
++ this.deathMessage = deathMessage != null ? LegacyComponentSerializer.legacySection().deserialize(deathMessage) : null;
++ }
++
++ public boolean isSendDeathMessage() {
++ return sendDeathMessage;
++ }
++
++ public void setSendDeathMessage(boolean sendDeathMessage) {
++ this.sendDeathMessage = sendDeathMessage;
++ }
++
++ @Override
++ public boolean isCancelled() {
++ return cancel;
++ }
++
++ @Override
++ public void setCancelled(boolean cancel) {
++ this.cancel = cancel;
++ }
++}
diff --git a/src/main/java/org/leavesmc/leaves/event/bot/BotEvent.java b/src/main/java/org/leavesmc/leaves/event/bot/BotEvent.java
new file mode 100644
-index 0000000000000000000000000000000000000000..ad358081f1e1da4075243d7ca0a01c1f7b00631b
+index 0000000000000000000000000000000000000000..ed9f954da5d381368977eae3ed19a334a3bc3e5a
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/event/bot/BotEvent.java
-@@ -0,0 +1,31 @@
+@@ -0,0 +1,32 @@
+package org.leavesmc.leaves.event.bot;
+
+import org.bukkit.event.Event;
@@ -598,13 +899,14 @@ index 0000000000000000000000000000000000000000..ad358081f1e1da4075243d7ca0a01c1f
+ * 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
++ public BotEvent(@NotNull final Bot who, boolean async) {
+ super(async);
+ bot = who;
+ }
@@ -621,7 +923,7 @@ index 0000000000000000000000000000000000000000..ad358081f1e1da4075243d7ca0a01c1f
+}
diff --git a/src/main/java/org/leavesmc/leaves/event/bot/BotInventoryOpenEvent.java b/src/main/java/org/leavesmc/leaves/event/bot/BotInventoryOpenEvent.java
new file mode 100644
-index 0000000000000000000000000000000000000000..a369b468d4793b36dd0944a1368a70e07b9fc10f
+index 0000000000000000000000000000000000000000..8191c0e1302234981212d2fa015425e25825ce61
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/event/bot/BotInventoryOpenEvent.java
@@ -0,0 +1,46 @@
@@ -635,6 +937,7 @@ index 0000000000000000000000000000000000000000..a369b468d4793b36dd0944a1368a70e0
+import org.leavesmc.leaves.entity.Bot;
+
+public class BotInventoryOpenEvent extends BotEvent implements Cancellable {
++
+ private static final HandlerList handlers = new HandlerList();
+
+ private final Player player;
@@ -660,7 +963,6 @@ index 0000000000000000000000000000000000000000..a369b468d4793b36dd0944a1368a70e0
+ return player;
+ }
+
-+
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return handlers;
@@ -673,10 +975,10 @@ index 0000000000000000000000000000000000000000..a369b468d4793b36dd0944a1368a70e0
+}
diff --git a/src/main/java/org/leavesmc/leaves/event/bot/BotJoinEvent.java b/src/main/java/org/leavesmc/leaves/event/bot/BotJoinEvent.java
new file mode 100644
-index 0000000000000000000000000000000000000000..07f6d81c4cd897230bbd6712dac09b8995431104
+index 0000000000000000000000000000000000000000..24e5f4d833897000e0378d4d3c3ff75c08a5bad2
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/event/bot/BotJoinEvent.java
-@@ -0,0 +1,66 @@
+@@ -0,0 +1,67 @@
+package org.leavesmc.leaves.event.bot;
+
+import net.kyori.adventure.text.Component;
@@ -690,6 +992,7 @@ index 0000000000000000000000000000000000000000..07f6d81c4cd897230bbd6712dac09b89
+ * Called when a fakeplayer joins a server
+ */
+public class BotJoinEvent extends BotEvent {
++
+ private static final HandlerList handlers = new HandlerList();
+
+ private Component joinMessage;
@@ -743,12 +1046,77 @@ index 0000000000000000000000000000000000000000..07f6d81c4cd897230bbd6712dac09b89
+ return handlers;
+ }
+}
+diff --git a/src/main/java/org/leavesmc/leaves/event/bot/BotLoadEvent.java b/src/main/java/org/leavesmc/leaves/event/bot/BotLoadEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d4472675af540cdeeebf428144c70b9a5c3f34ce
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/event/bot/BotLoadEvent.java
+@@ -0,0 +1,59 @@
++package org.leavesmc.leaves.event.bot;
++
++import org.bukkit.event.Cancellable;
++import org.bukkit.event.Event;
++import org.bukkit.event.HandlerList;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.UUID;
++
++/**
++ * Call when a fakeplayer loading a server
++ */
++public class BotLoadEvent extends Event implements Cancellable {
++
++ private static final HandlerList handlers = new HandlerList();
++
++ private final String bot;
++ private final UUID botUUID;
++ private boolean cancel = false;
++
++ public BotLoadEvent(@NotNull final String who, @NotNull final UUID uuid) {
++ this.bot = who;
++ this.botUUID = uuid;
++ }
++
++ /**
++ * Gets the fakeplayer name
++ *
++ * @return fakeplayer name
++ */
++ public String getBot() {
++ return bot;
++ }
++
++ public UUID getBotUUID() {
++ return botUUID;
++ }
++
++ @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/org/leavesmc/leaves/event/bot/BotRemoveEvent.java b/src/main/java/org/leavesmc/leaves/event/bot/BotRemoveEvent.java
new file mode 100644
-index 0000000000000000000000000000000000000000..7af990a5ee020dfdbec2efc6aecf6393a4223730
+index 0000000000000000000000000000000000000000..408a7e39ee1923d595fb8ac3f91d60e14a2c446c
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/event/bot/BotRemoveEvent.java
-@@ -0,0 +1,105 @@
+@@ -0,0 +1,106 @@
+package org.leavesmc.leaves.event.bot;
+
+import net.kyori.adventure.text.Component;
@@ -760,12 +1128,13 @@ index 0000000000000000000000000000000000000000..7af990a5ee020dfdbec2efc6aecf6393
+import org.jetbrains.annotations.Nullable;
+import org.leavesmc.leaves.entity.Bot;
+
-+import java.util.Optional;
-+
+/**
+ * Call when a fakeplayer creates a server
+ */
+public class BotRemoveEvent extends BotEvent implements Cancellable {
++
++ private static final HandlerList handlers = new HandlerList();
++
+ public enum RemoveReason {
+ COMMAND,
+ PLUGIN,
@@ -773,26 +1142,18 @@ index 0000000000000000000000000000000000000000..7af990a5ee020dfdbec2efc6aecf6393
+ INTERNAL
+ }
+
-+ private static final HandlerList handlers = new HandlerList();
-+
+ private final RemoveReason reason;
-+ private final Optional remover;
++ private final CommandSender remover;
+ private Component removeMessage;
++ private boolean save;
+ private boolean cancel = false;
+
-+ public BotRemoveEvent(@NotNull final Bot who, @NotNull RemoveReason reason) {
-+ this(who, reason, null);
-+ }
-+
-+ public BotRemoveEvent(@NotNull final Bot who, @NotNull RemoveReason reason, @Nullable CommandSender remover) {
-+ this(who, reason, remover, null);
-+ }
-+
-+ public BotRemoveEvent(@NotNull final Bot who, @NotNull RemoveReason reason, @Nullable CommandSender remover, @Nullable Component removeMessage) {
++ public BotRemoveEvent(@NotNull final Bot who, @NotNull RemoveReason reason, @Nullable CommandSender remover, @Nullable Component removeMessage, boolean save) {
+ super(who);
+ this.reason = reason;
-+ this.remover = Optional.ofNullable(remover);
++ this.remover = remover;
+ this.removeMessage = removeMessage;
++ this.save = save;
+ }
+
+ /**
@@ -807,12 +1168,12 @@ index 0000000000000000000000000000000000000000..7af990a5ee020dfdbec2efc6aecf6393
+
+ /**
+ * Gets the remover of the bot
-+ * if the remove reason is not COMMAND, the creator might be Optional.empty()
++ * if the remove reason is not COMMAND, the creator might be null
+ *
+ * @return An optional of remover
+ */
-+ @NotNull
-+ public Optional getRemover() {
++ @Nullable
++ public CommandSender getRemover() {
+ return remover;
+ }
+
@@ -843,6 +1204,14 @@ index 0000000000000000000000000000000000000000..7af990a5ee020dfdbec2efc6aecf6393
+ this.cancel = cancel;
+ }
+
++ public boolean shouldSave() {
++ return save;
++ }
++
++ public void setSave(boolean save) {
++ this.save = save;
++ }
++
+ @Override
+ @NotNull
+ public HandlerList getHandlers() {
@@ -854,3 +1223,47 @@ index 0000000000000000000000000000000000000000..7af990a5ee020dfdbec2efc6aecf6393
+ return handlers;
+ }
+}
+diff --git a/src/main/java/org/leavesmc/leaves/event/bot/BotSpawnLocationEvent.java b/src/main/java/org/leavesmc/leaves/event/bot/BotSpawnLocationEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..46ab3e9b5e398ec238e129d16fb020b481a88f76
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/event/bot/BotSpawnLocationEvent.java
+@@ -0,0 +1,38 @@
++package org.leavesmc.leaves.event.bot;
++
++import org.bukkit.Location;
++import org.bukkit.event.HandlerList;
++import org.jetbrains.annotations.NotNull;
++import org.leavesmc.leaves.entity.Bot;
++
++public class BotSpawnLocationEvent extends BotEvent {
++
++ private static final HandlerList handlers = new HandlerList();
++
++ private Location spawnLocation;
++
++ public BotSpawnLocationEvent(@NotNull final Bot who, @NotNull Location spawnLocation) {
++ super(who);
++ this.spawnLocation = spawnLocation;
++ }
++
++ @NotNull
++ public Location getSpawnLocation() {
++ return spawnLocation;
++ }
++
++ public void setSpawnLocation(@NotNull Location location) {
++ this.spawnLocation = location;
++ }
++
++ @NotNull
++ @Override
++ public HandlerList getHandlers() {
++ return handlers;
++ }
++
++ @NotNull
++ public static HandlerList getHandlerList() {
++ return handlers;
++ }
++}
diff --git a/patches/server/0006-Leaves-Server-Config-And-Command.patch b/patches/server/0006-Leaves-Server-Config-And-Command.patch
index f6fedf4a..db93bda8 100644
--- a/patches/server/0006-Leaves-Server-Config-And-Command.patch
+++ b/patches/server/0006-Leaves-Server-Config-And-Command.patch
@@ -85,10 +85,10 @@ index d97771ecaf06b92d92b5ca0224ae0866e36703a6..439305bb4f5ce232aa6237276c121d53
.withRequiredArg()
diff --git a/src/main/java/org/leavesmc/leaves/LeavesConfig.java b/src/main/java/org/leavesmc/leaves/LeavesConfig.java
new file mode 100644
-index 0000000000000000000000000000000000000000..59c98fa51afd4fd305852d14509b06f8bef859a1
+index 0000000000000000000000000000000000000000..10cf4d4aed544e0e0dd6698119734eb9e9cc92e2
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/LeavesConfig.java
-@@ -0,0 +1,883 @@
+@@ -0,0 +1,889 @@
+package org.leavesmc.leaves;
+
+import com.destroystokyo.paper.util.SneakyThrow;
@@ -228,13 +228,13 @@ index 0000000000000000000000000000000000000000..59c98fa51afd4fd305852d14509b06f8
+ public static boolean fakeplayerSpawnPhantom = false;
+
+ @GlobalConfig(name = "regen-amount", category = {"modify", "fakeplayer"}, verify = RegenAmountVerify.class)
-+ public static double fakeplayerRegenAmount = 0.010;
++ public static double fakeplayerRegenAmount = 0.0;
+
+ private static class RegenAmountVerify extends ConfigVerifyImpl.DoubleConfigVerify {
+ @Override
+ public void check(Double old, Double value) throws IllegalArgumentException {
-+ if (value <= 0.0) {
-+ throw new IllegalArgumentException("regen-amount need > 0.0f");
++ if (value < 0.0) {
++ throw new IllegalArgumentException("regen-amount need >= 0.0");
+ }
+ }
+ }
@@ -245,6 +245,12 @@ index 0000000000000000000000000000000000000000..59c98fa51afd4fd305852d14509b06f8
+ @GlobalConfig(name = "modify-config", category = {"modify", "fakeplayer"})
+ public static boolean fakeplayerModifyConfig = false;
+
++ @GlobalConfig(name = "manual-save-and-load", category = {"modify", "fakeplayer"})
++ public static boolean fakeplayerManualSaveAndLoad = false;
++
++ @GlobalConfig(name = "cache-skin", category = {"modify", "fakeplayer"}, lock = true)
++ public static boolean fakeplayerCacheSkin = false;
++
+ // Leaves end - modify - fakeplayer
+
+ // Leaves start - modify - minecraft-old
@@ -974,10 +980,10 @@ index 0000000000000000000000000000000000000000..59c98fa51afd4fd305852d14509b06f8
+}
diff --git a/src/main/java/org/leavesmc/leaves/command/CommandArgument.java b/src/main/java/org/leavesmc/leaves/command/CommandArgument.java
new file mode 100644
-index 0000000000000000000000000000000000000000..2f0e6671dd8bfe4f320eab92c5f5bbc10abc3b05
+index 0000000000000000000000000000000000000000..0bccbf7816ef621316f0da4911ec112f4753f88e
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/command/CommandArgument.java
-@@ -0,0 +1,49 @@
+@@ -0,0 +1,55 @@
+package org.leavesmc.leaves.command;
+
+import org.jetbrains.annotations.NotNull;
@@ -988,10 +994,12 @@ index 0000000000000000000000000000000000000000..2f0e6671dd8bfe4f320eab92c5f5bbc1
+
+public class CommandArgument {
+
++ public static final CommandArgument EMPTY = new CommandArgument();
++
+ private final List> argumentTypes;
+ private final List> tabComplete;
+
-+ public CommandArgument(CommandArgumentType>... argumentTypes) {
++ private CommandArgument(CommandArgumentType>... argumentTypes) {
+ this.argumentTypes = List.of(argumentTypes);
+ this.tabComplete = new ArrayList<>();
+ for (int i = 0; i < argumentTypes.length; i++) {
@@ -999,6 +1007,10 @@ index 0000000000000000000000000000000000000000..2f0e6671dd8bfe4f320eab92c5f5bbc1
+ }
+ }
+
++ public static CommandArgument of(CommandArgumentType>... argumentTypes) {
++ return new CommandArgument(argumentTypes);
++ }
++
+ public List tabComplete(int n) {
+ if (tabComplete.size() > n) {
+ return tabComplete.get(n);
@@ -1029,10 +1041,10 @@ index 0000000000000000000000000000000000000000..2f0e6671dd8bfe4f320eab92c5f5bbc1
+}
diff --git a/src/main/java/org/leavesmc/leaves/command/CommandArgumentResult.java b/src/main/java/org/leavesmc/leaves/command/CommandArgumentResult.java
new file mode 100644
-index 0000000000000000000000000000000000000000..3f0707940f22736f703c24f4da25c18fa6e5b309
+index 0000000000000000000000000000000000000000..46aa6eaa75b65aad6bdbe4a5f517b42e87bcca77
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/command/CommandArgumentResult.java
-@@ -0,0 +1,65 @@
+@@ -0,0 +1,69 @@
+package org.leavesmc.leaves.command;
+
+import net.minecraft.core.BlockPos;
@@ -1065,6 +1077,10 @@ index 0000000000000000000000000000000000000000..3f0707940f22736f703c24f4da25c18f
+ return Objects.requireNonNullElse(read(String.class), def);
+ }
+
++ public boolean readBoolean(boolean def) {
++ return Objects.requireNonNullElse(read(Boolean.class), def);
++ }
++
+ public BlockPos readPos() {
+ Integer[] pos = {read(Integer.class), read(Integer.class), read(Integer.class)};
+ for (Integer po : pos) {
@@ -1100,10 +1116,10 @@ index 0000000000000000000000000000000000000000..3f0707940f22736f703c24f4da25c18f
+}
diff --git a/src/main/java/org/leavesmc/leaves/command/CommandArgumentType.java b/src/main/java/org/leavesmc/leaves/command/CommandArgumentType.java
new file mode 100644
-index 0000000000000000000000000000000000000000..0d6d33e66461dda39c8f0e8395bf3f047ef88cbd
+index 0000000000000000000000000000000000000000..4ca3508475bbd9771768704e300fe12b717489d6
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/command/CommandArgumentType.java
-@@ -0,0 +1,48 @@
+@@ -0,0 +1,55 @@
+package org.leavesmc.leaves.command;
+
+import org.jetbrains.annotations.NotNull;
@@ -1150,6 +1166,13 @@ index 0000000000000000000000000000000000000000..0d6d33e66461dda39c8f0e8395bf3f04
+ }
+ };
+
++ public static final CommandArgumentType BOOLEAN = new CommandArgumentType<>() {
++ @Override
++ public Boolean pasre(@NotNull String arg) {
++ return Boolean.parseBoolean(arg);
++ }
++ };
++
+ public abstract E pasre(@NotNull String arg);
+}
diff --git a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java
diff --git a/patches/server/0010-Fakeplayer-support.patch b/patches/server/0010-Fakeplayer-support.patch
index dbb42cb6..b8f0cef7 100644
--- a/patches/server/0010-Fakeplayer-support.patch
+++ b/patches/server/0010-Fakeplayer-support.patch
@@ -33,27 +33,74 @@ index 35772110e9318df46a2729dbc0b5879b290011b7..f26989a44cdda9baabf337d573436c6c
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 3e550f8e7cd4f4e16f499a8a2a4b95420270f07a..46d9c77581b78c427692aa8645d17b3d0c2bb6a6 100644
+--- a/src/main/java/net/minecraft/network/Connection.java
++++ b/src/main/java/net/minecraft/network/Connection.java
+@@ -104,7 +104,7 @@ public class Connection extends SimpleChannelInboundHandler> {
+ @Nullable
+ private volatile PacketListener disconnectListener;
+ @Nullable
+- private volatile PacketListener packetListener;
++ protected volatile PacketListener packetListener; // Leaves - private -> protected
+ @Nullable
+ private DisconnectionDetails disconnectionDetails;
+ private boolean encrypted;
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 7ce0fb36690e12f3f36c9a43e45ac71814be8e69..a625473805fcde57f3987f3d788efb36fa89073e 100644
+index 7ce0fb36690e12f3f36c9a43e45ac71814be8e69..89c568a24df0323b902b5236484644a6edbea7ff 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -746,6 +746,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) {
+ AtomicReference atomicreference = new AtomicReference();
+ Thread thread = new ca.spottedleaf.moonrise.common.util.TickThread(() -> { // Paper - rewrite chunk system
+@@ -746,6 +748,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop loot = new java.util.ArrayList<>(this.getInventory().getContainerSize()); // Paper - Restore vanilla drops behavior
- boolean keepInventory = this.level().getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY) || this.isSpectator();
-
-- if (!keepInventory) {
-+ if (!keepInventory || this instanceof org.leavesmc.leaves.bot.ServerBot) { // Leaves - skip bot
- for (ItemStack item : this.getInventory().getContents()) {
- if (!item.isEmpty() && !EnchantmentHelper.has(item, EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP)) {
- loot.add(new DefaultDrop(item, stack -> this.drop(stack, true, false, false))); // Paper - Restore vanilla drops behavior; drop function taken from Inventory#dropAll (don't fire drop event)
-@@ -1417,6 +1421,13 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple
+@@ -1417,6 +1421,12 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple
this.lastSentHealth = -1.0F;
this.lastSentFood = -1;
+ // Leaves start - bot support
+ if (org.leavesmc.leaves.LeavesConfig.fakeplayerSupport) {
-+ org.leavesmc.leaves.bot.ServerBot.getBots().forEach(bot1 ->
-+ bot1.sendFakeDataIfNeed(this, true)); // Leaves - render bot
++ this.server.getBotList().bots.forEach(bot -> bot.sendFakeDataIfNeed(this, true)); // Leaves - render bot
+ }
+ // Leaves end - bot support
+
@@ -221,7 +270,7 @@ index 763cffdc2e1e2e7cc9af88cc46bbaa240a20fd0d..647a6c9dd39e113625377273281d74ae
PlayerChangedWorldEvent changeEvent = new PlayerChangedWorldEvent(this.getBukkitEntity(), worldserver1.getWorld());
this.level().getCraftServer().getPluginManager().callEvent(changeEvent);
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
-index 8ccd40b562691e757c7b5efa1497d93a95040a9a..901e9ff7a89b48b7acca7e5b96f640045c1d265e 100644
+index 8ccd40b562691e757c7b5efa1497d93a95040a9a..69f44d0351e8127e38cbe9e74c05b365c37b2c44 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;
@@ -233,19 +282,17 @@ index 8ccd40b562691e757c7b5efa1497d93a95040a9a..901e9ff7a89b48b7acca7e5b96f64004
public abstract class PlayerList {
public static final File USERBANLIST_FILE = new File("banned-players.json");
-@@ -351,6 +353,21 @@ public abstract class PlayerList {
+@@ -351,6 +353,19 @@ public abstract class PlayerList {
org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol
+ // Leaves start - bot support
+ if (org.leavesmc.leaves.LeavesConfig.fakeplayerSupport) {
-+ ServerBot bot = ServerBot.getBot(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT));
++ ServerBot bot = this.server.getBotList().getBotByName(player.getScoreboardName());
+ 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);
++ this.server.getBotList().removeBot(bot, org.leavesmc.leaves.event.bot.BotRemoveEvent.RemoveReason.INTERNAL, player.getBukkitEntity(), false);
+ }
-+ ServerBot.getBots().forEach(bot1 -> {
++ this.server.getBotList().bots.forEach(bot1 -> {
+ bot1.sendPlayerInfo(player);
+ bot1.sendFakeDataIfNeed(player, true);
+ }); // Leaves - render bot
@@ -255,55 +302,67 @@ index 8ccd40b562691e757c7b5efa1497d93a95040a9a..901e9ff7a89b48b7acca7e5b96f64004
final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage();
if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure
-@@ -937,6 +954,13 @@ public abstract class PlayerList {
+@@ -937,6 +952,12 @@ public abstract class PlayerList {
}
// Paper end - Add PlayerPostRespawnEvent
+ // Leaves start - bot support
+ if (org.leavesmc.leaves.LeavesConfig.fakeplayerSupport) {
-+ ServerBot.getBots().forEach(bot1 ->
-+ bot1.sendFakeDataIfNeed(entityplayer1, true)); // Leaves - render bot
++ this.server.getBotList().bots.forEach(bot -> bot.sendFakeDataIfNeed(entityplayer1, true)); // Leaves - render bot
+ }
+ // Leaves end - bot support
+
// CraftBukkit end
return entityplayer1;
-@@ -1069,11 +1093,16 @@ public abstract class PlayerList {
+@@ -1069,11 +1090,16 @@ public abstract class PlayerList {
}
public String[] getPlayerNamesArray() {
- String[] astring = new String[this.players.size()];
-+ String[] astring = new String[this.players.size() + ServerBot.getBots().size()]; // Leaves - fakeplayer support
++ String[] astring = new String[this.players.size() + this.server.getBotList().bots.size()]; // Leaves - fakeplayer support
for (int i = 0; i < this.players.size(); ++i) {
astring[i] = ((ServerPlayer) this.players.get(i)).getGameProfile().getName();
}
+ // 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();
++ astring[i] = ((ServerPlayer) this.server.getBotList().bots.get(i - this.players.size())).getGameProfile().getName();
+ }
+ // Leaves end - fakeplayer support
return astring;
}
-@@ -1553,4 +1582,16 @@ public abstract class PlayerList {
- public boolean isAllowCommandsForAllPlayers() {
- return this.allowCommandsForAllPlayers;
+@@ -1159,7 +1185,13 @@ public abstract class PlayerList {
+
+ @Nullable
+ public ServerPlayer getPlayerByName(String name) {
+- return this.playersByName.get(name.toLowerCase(java.util.Locale.ROOT)); // Spigot
++ // Leaves start - fakeplayer support
++ ServerPlayer player = this.playersByName.get(name.toLowerCase(java.util.Locale.ROOT));
++ if (player == null) {
++ player = this.server.getBotList().getBotByName(name);
++ }
++ return player; // Spigot
++ // Leaves end - fakeplayer support
+ }
+
+ public void broadcast(@Nullable net.minecraft.world.entity.player.Player player, double x, double y, double z, double distance, ResourceKey worldKey, Packet> packet) {
+@@ -1499,7 +1531,13 @@ public abstract class PlayerList {
+
+ @Nullable
+ public ServerPlayer getPlayer(UUID uuid) {
+- return (ServerPlayer) this.playersByUUID.get(uuid);
++ // Leaves start - fakeplayer support
++ ServerPlayer player = this.playersByUUID.get(uuid);
++ if (player == null) {
++ player = this.server.getBotList().getBot(uuid);
++ }
++ return player;
++ // Leaves start - fakeplayer support
}
-+
-+ // 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
- }
+
+ public boolean canBypassPlayerLimit(GameProfile profile) {
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
index 7d7258c56cfade2d82acdf83dfa20cd0416c0dab..1beefbd05c13181214e188f50e88fb67f6712a45 100644
--- a/src/main/java/net/minecraft/world/entity/Entity.java
@@ -317,6 +376,41 @@ index 7d7258c56cfade2d82acdf83dfa20cd0416c0dab..1beefbd05c13181214e188f50e88fb67
// 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/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java
+index 09bcbc0ae36e4e69fee87a7e0c49acf496117a39..65b002f2ae062327f48df0e157aa35721478c5fd 100644
+--- a/src/main/java/net/minecraft/world/entity/player/Player.java
++++ b/src/main/java/net/minecraft/world/entity/player/Player.java
+@@ -183,7 +183,7 @@ public abstract class Player extends LivingEntity {
+ private int lastLevelUpTime;
+ public GameProfile gameProfile;
+ private boolean reducedDebugInfo;
+- private ItemStack lastItemInMainHand;
++ protected ItemStack lastItemInMainHand; // Leaves - private -> protected
+ private final ItemCooldowns cooldowns;
+ private Optional lastDeathLocation;
+ @Nullable
+@@ -337,6 +337,12 @@ public abstract class Player extends LivingEntity {
+
+ }
+
++ // Leaves start - fakeplayer
++ protected void livingEntityTick() {
++ super.tick();
++ }
++ // Leaves end - fakeplayer
++
+ @Override
+ protected float getMaxHeadRotationRelativeToBody() {
+ return this.isBlocking() ? 15.0F : super.getMaxHeadRotationRelativeToBody();
+@@ -635,7 +641,7 @@ public abstract class Player extends LivingEntity {
+
+ }
+
+- private void touch(Entity entity) {
++ public void touch(Entity entity) { // Leaves - private -> public
+ entity.playerTouch(this);
+ }
+
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 1223c5d23d0ea6aed068bdf0f5725e2ad49fc82c..0e00f59a8962dd6356d483ef5be3209a3a410008 100644
--- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
@@ -367,7 +461,7 @@ index 32910f677b0522ac8ec513fa0d00b714b52cfae4..961a7193fda00fa62acea9c39fda1c93
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 1b1b475ca27e799e251d6f8a8c9fe1a4fd8bae83..aff4e1e4d2462adf00d6e984b0a0540592257056 100644
+index 1b1b475ca27e799e251d6f8a8c9fe1a4fd8bae83..22f8e7d62df86a12c5b9ad709538d6ac564d3338 100644
--- a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java
+++ b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java
@@ -67,6 +67,11 @@ public class PhantomSpawner implements CustomSpawner {
@@ -375,26 +469,60 @@ index 1b1b475ca27e799e251d6f8a8c9fe1a4fd8bae83..aff4e1e4d2462adf00d6e984b0a05405
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 org.leavesmc.leaves.bot.ServerBot bot && bot.spawnPhantom) {
++ if (entityplayer instanceof org.leavesmc.leaves.bot.ServerBot bot && bot.getConfigValue(org.leavesmc.leaves.bot.agent.Configs.SPAWN_PHANTOM)) {
+ j = Math.max(bot.notSleepTicks, 1);
+ }
+ // Leaves end - fakeplayer spawn
if (randomsource.nextInt(j) >= world.paperConfig().entities.behavior.playerInsomniaStartTicks) { // Paper - Ability to control player's insomnia and phantoms
BlockPos blockposition1 = blockposition.above(20 + randomsource.nextInt(15)).east(-10 + randomsource.nextInt(21)).south(-10 + randomsource.nextInt(21));
+diff --git a/src/main/java/net/minecraft/world/level/storage/LevelResource.java b/src/main/java/net/minecraft/world/level/storage/LevelResource.java
+index fee8367d2812db559b15970f0a60023bedaaefc5..f6b59b00bb1611aff8d161d1ad03df7fc911f994 100644
+--- a/src/main/java/net/minecraft/world/level/storage/LevelResource.java
++++ b/src/main/java/net/minecraft/world/level/storage/LevelResource.java
+@@ -15,7 +15,7 @@ public class LevelResource {
+ public static final LevelResource ROOT = new LevelResource(".");
+ private final String id;
+
+- private LevelResource(String relativePath) {
++ public LevelResource(String relativePath) { // Leaves - private -> public
+ this.id = relativePath;
+ }
+
+diff --git a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
+index 8ab7ca373a885fbe658013c9c6a2e38d32d77bb2..2262eaab7af5f1d7c37ef028479842c0fb45f3ee 100644
+--- a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
++++ b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
+@@ -21,7 +21,7 @@ import net.minecraft.world.entity.player.Player;
+ import org.bukkit.craftbukkit.entity.CraftPlayer;
+ import org.slf4j.Logger;
+
+-public class PlayerDataStorage {
++public class PlayerDataStorage implements org.leavesmc.leaves.bot.IPlayerDataStorage {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private final File playerDir;
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
-index f2a0a1f32bf456c302e5d18b91367aa0c041cc6c..59cdcf7b25c17705b613c83dea107934b683af28 100644
+index f2a0a1f32bf456c302e5d18b91367aa0c041cc6c..97d09246b5bab3fe85491d06c7b16f932bcd1cb2 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -310,6 +310,7 @@ public final class CraftServer implements Server {
private final io.papermc.paper.logging.SysoutCatcher sysoutCatcher = new io.papermc.paper.logging.SysoutCatcher(); // Paper
private final io.papermc.paper.potion.PaperPotionBrewer potionBrewer; // Paper - Custom Potion Mixes
public final io.papermc.paper.SparksFly spark; // Paper - spark
-+ private final org.leavesmc.leaves.entity.CraftBotManager botManager = new org.leavesmc.leaves.entity.CraftBotManager(); // Leaves
++ private final org.leavesmc.leaves.entity.CraftBotManager botManager; // Leaves
// Paper start - Folia region threading API
private final io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler();
-@@ -1463,7 +1464,7 @@ public final class CraftServer implements Server {
+@@ -477,6 +478,7 @@ public final class CraftServer implements Server {
+ datapackManager = new io.papermc.paper.datapack.PaperDatapackManager(console.getPackRepository()); // Paper
+ this.spark = new io.papermc.paper.SparksFly(this); // Paper - spark
+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.init(); // Leaves - protocol
++ this.botManager = new org.leavesmc.leaves.entity.CraftBotManager(); // Leaves
+ }
+
+ public boolean getCommandBlockOverride(String command) {
+@@ -1463,7 +1465,7 @@ public final class CraftServer implements Server {
return false;
}
@@ -403,7 +531,7 @@ index f2a0a1f32bf456c302e5d18b91367aa0c041cc6c..59cdcf7b25c17705b613c83dea107934
return false;
}
-@@ -3229,4 +3230,11 @@ public final class CraftServer implements Server {
+@@ -3229,4 +3231,11 @@ public final class CraftServer implements Server {
return this.potionBrewer;
}
// Paper end
@@ -496,80 +624,24 @@ index 8dd85b9ca3b3e3429de4d0ec0654982589c6e93e..de9f63fb3b8dcf11a9271794850ce448
// Paper start - make cancellable
if (event.isCancelled()) {
return event;
-diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
-index 22f1ed383313829b8af4badda9ef8dc85cae8fd1..1c47e320e464af9651953ff308a2583fcb965891 100644
---- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
-+++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
-@@ -1,7 +1,7 @@
- package org.bukkit.craftbukkit.scheduler;
-
- import com.google.common.base.Preconditions;
--import com.google.common.util.concurrent.ThreadFactoryBuilder;
-+
- import java.util.ArrayList;
- import java.util.Comparator;
- import java.util.Iterator;
-@@ -10,7 +10,6 @@ import java.util.PriorityQueue;
- import java.util.concurrent.Callable;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.concurrent.Executor;
--import java.util.concurrent.Executors;
- import java.util.concurrent.Future;
- import java.util.concurrent.atomic.AtomicInteger;
- import java.util.concurrent.atomic.AtomicReference;
-@@ -23,6 +22,7 @@ import org.bukkit.scheduler.BukkitRunnable;
- import org.bukkit.scheduler.BukkitScheduler;
- import org.bukkit.scheduler.BukkitTask;
- import org.bukkit.scheduler.BukkitWorker;
-+import org.leavesmc.leaves.plugin.MinecraftInternalPlugin;
-
- /**
- * The fundamental concepts for this implementation:
-@@ -46,6 +46,8 @@ import org.bukkit.scheduler.BukkitWorker;
- */
- public class CraftScheduler implements BukkitScheduler {
-
-+ public static final Plugin MINECRAFT = new MinecraftInternalPlugin(); // Leaves - run async task
-+
- /**
- * The start ID for the counter.
- */
-@@ -478,10 +480,14 @@ public class CraftScheduler implements BukkitScheduler {
- "Task #%s for %s generated an exception",
- task.getTaskId(),
- task.getOwner().getDescription().getFullName());
-- task.getOwner().getLogger().log(
-- Level.WARNING,
-- logMessage,
-- throwable);
-+ if (task.getOwner() instanceof MinecraftInternalPlugin) {
-+ net.minecraft.server.MinecraftServer.LOGGER.error(logMessage, throwable);
-+ } else {
-+ task.getOwner().getLogger().log(
-+ Level.WARNING,
-+ logMessage,
-+ throwable);
-+ }
- org.bukkit.Bukkit.getServer().getPluginManager().callEvent(
- new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerSchedulerException(logMessage, throwable, task)));
- // Paper end
diff --git a/src/main/java/org/leavesmc/leaves/bot/BotCommand.java b/src/main/java/org/leavesmc/leaves/bot/BotCommand.java
new file mode 100644
-index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5ce1e32c0
+index 0000000000000000000000000000000000000000..a06f24253a2d941c8fd265f50dcaec356b5cfbcc
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/BotCommand.java
-@@ -0,0 +1,472 @@
+@@ -0,0 +1,522 @@
+package org.leavesmc.leaves.bot;
+
++import io.papermc.paper.command.CommandUtil;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.format.NamedTextColor;
+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.craftbukkit.scheduler.CraftScheduler;
+import org.bukkit.entity.Player;
+import org.bukkit.generator.WorldInfo;
+import org.bukkit.permissions.Permission;
@@ -577,24 +649,32 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+import org.bukkit.plugin.PluginManager;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.LeavesConfig;
++import org.leavesmc.leaves.LeavesLogger;
+import org.leavesmc.leaves.bot.agent.Actions;
+import org.leavesmc.leaves.bot.agent.BotAction;
++import org.leavesmc.leaves.bot.agent.BotConfig;
++import org.leavesmc.leaves.bot.agent.Configs;
+import org.leavesmc.leaves.bot.agent.actions.CraftCustomBotAction;
++import org.leavesmc.leaves.command.CommandArgumentResult;
+import org.leavesmc.leaves.entity.Bot;
-+import org.leavesmc.leaves.event.bot.BotActionEvent;
+import org.leavesmc.leaves.event.bot.BotConfigModifyEvent;
+import org.leavesmc.leaves.event.bot.BotCreateEvent;
+import org.leavesmc.leaves.event.bot.BotRemoveEvent;
++import org.leavesmc.leaves.plugin.MinecraftInternalPlugin;
+
+import java.util.*;
-+import java.util.stream.Stream;
++
++import static net.kyori.adventure.text.Component.text;
+
+public class BotCommand extends Command {
+
++ private final Component unknownMessage;
++
+ public BotCommand(String name) {
+ super(name);
+ this.description = "FakePlayer Command";
+ this.usageMessage = "/bot [create | remove | action | list | config]";
++ this.unknownMessage = text("Usage: " + usageMessage, NamedTextColor.RED);
+ this.setPermission("bukkit.command.bot");
+ final PluginManager pluginManager = Bukkit.getServer().getPluginManager();
+ if (pluginManager.getPermission("bukkit.command.bot") == null) {
@@ -604,7 +684,8 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+
+ @Override
+ public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, String @NotNull [] args, Location location) throws IllegalArgumentException {
-+ var list = new ArrayList();
++ List list = new ArrayList<>();
++ BotList botList = BotList.INSTANCE;
+
+ if (args.length <= 1) {
+ list.add("create");
@@ -615,14 +696,19 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+ if (LeavesConfig.fakeplayerModifyConfig) {
+ list.add("config");
+ }
++ if (LeavesConfig.fakeplayerManualSaveAndLoad) {
++ list.add("save");
++ list.add("load");
++ }
+ 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 "remove", "action", "config", "save" -> list.addAll(botList.bots.stream().map(e -> e.getName().getString()).toList());
+ case "list" -> list.addAll(Bukkit.getWorlds().stream().map(WorldInfo::getName).toList());
++ case "load" -> list.addAll(botList.getSavedBotList().getAllKeys());
+ }
+ }
+
@@ -630,6 +716,7 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+ switch (args[0]) {
+ case "action" -> {
+ list.add("list");
++ list.add("stop");
+ list.addAll(Actions.getNames());
+ }
+ case "create" -> list.add("");
@@ -638,39 +725,43 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+ }
+ }
+
-+ if (args.length == 4) {
-+ switch (args[0]) {
-+ case "config" -> {
-+ if (args[2].equals(BotConfig.SIMULATION_DISTANCE.configName)) {
-+ list.add("10");
-+ list.add("2");
-+ list.add("");
-+ } else {
-+ list.add("true");
-+ list.add("false");
-+ }
-+ }
-+ case "remove" -> {
-+ if (!Objects.equals(args[3], "cancel")) {
-+ list.add("[minute]");
-+ }
++ if (args[0].equals("remove") && args.length >= 3) {
++ if (!Objects.equals(args[3], "cancel")) {
++ switch (args.length) {
++ case 4 -> list.add("[minute]");
++ case 5 -> list.add("[second]");
+ }
+ }
+ }
-+ if (args.length == 5 && args[0].equals("remove")) {
-+ if (!Objects.equals(args[3], "cancel")) {
-+ list.add("[second]");
++
++ if (args.length >= 4 && args[0].equals("action")) {
++ ServerBot bot = botList.getBotByName(args[1]);
++
++ if (bot == null) {
++ return Collections.singletonList("<" + args[1] + " not found>");
++ }
++
++ if (args[2].equals("stop")) {
++ list.add("all");
++ for (int i = 0; i < bot.getBotActions().size(); i++) {
++ list.add(String.valueOf(i));
++ }
++ } else {
++ BotAction> action = Actions.getForName(args[2]);
++ if (action != null) {
++ list.addAll(action.getArgument().tabComplete(args.length - 4));
++ }
+ }
+ }
+
-+ 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));
++ if (args.length >= 4 && args[0].equals("config")) {
++ Configs> config = Configs.getConfig(args[2]);
++ if (config != null) {
++ list.addAll(config.config.getArgument().tabComplete(args.length - 4));
+ }
+ }
+
-+ return list;
++ return CommandUtil.getListMatchingLast(sender, args, list);
+ }
+
+ @Override
@@ -678,23 +769,20 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+ if (!testPermission(sender) || !LeavesConfig.fakeplayerSupport) return true;
+
+ if (args.length == 0) {
-+ sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage);
++ sender.sendMessage(unknownMessage);
+ 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);
-+
++ case "save" -> this.onSave(sender, args);
++ case "load" -> this.onLoad(sender, args);
+ default -> {
-+ sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage);
++ sender.sendMessage(unknownMessage);
+ return false;
+ }
+ }
@@ -704,53 +792,59 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+
+ 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");
++ sender.sendMessage(text("Use /bot create [skin_name] to create a fakeplayer", NamedTextColor.RED));
+ 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], BotCreateEvent.CreateReason.COMMAND, player).create(null);
-+ } else if (sender instanceof ConsoleCommandSender csender) {
-+ if (args.length < 6) {
-+ sender.sendMessage(ChatColor.RED + "Use /bot create to create a fakeplayer");
-+ return;
-+ }
++ String botName = args[1];
++ if (this.canCreate(sender, botName)) {
++ BotCreateState.Builder builder = BotCreateState.builder(botName, Bukkit.getWorlds().getFirst().getSpawnLocation()).createReason(BotCreateEvent.CreateReason.COMMAND).creator(sender);
+
-+ 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 (args.length >= 3) {
++ builder.skinName(args[2]);
++ }
+
-+ if (world != null) {
-+ new ServerBot.BotCreateState(new Location(world, x, y, z), args[1], args[2], BotCreateEvent.CreateReason.COMMAND, csender).create(null);
++ if (sender instanceof Player player) {
++ builder.location(player.getLocation());
++ } else if (sender instanceof ConsoleCommandSender) {
++ if (args.length >= 7) {
++ 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) {
++ builder.location(new Location(world, x, y, z));
++ }
++ } catch (Exception e) {
++ LeavesLogger.LOGGER.warning("Can't build location", e);
+ }
-+ } catch (Exception e) {
-+ e.printStackTrace();
+ }
+ }
++
++ builder.spawnWithSkin(null);
+ }
+ }
+
+ private boolean canCreate(CommandSender sender, @NotNull String name) {
++ BotList botList = BotList.INSTANCE;
+ if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) {
-+ sender.sendMessage(ChatColor.RED + "This name is illegal");
++ sender.sendMessage(text("This name is illegal", NamedTextColor.RED));
+ return false;
+ }
+
-+ if (Bukkit.getPlayerExact(name) != null || ServerBot.getBot(name) != null) {
-+ sender.sendMessage(ChatColor.RED + "This player is in server");
++ if (Bukkit.getPlayerExact(name) != null || botList.getBotByName(name) != null) {
++ sender.sendMessage(text("This player is in server", NamedTextColor.RED));
+ return false;
+ }
+
+ if (org.leavesmc.leaves.LeavesConfig.unableFakeplayerNames.contains(name)) {
-+ sender.sendMessage(ChatColor.RED + "This name is not allowed");
++ sender.sendMessage(text("This name is not allowed", NamedTextColor.RED));
+ return false;
+ }
+
-+ if (ServerBot.getBots().size() >= org.leavesmc.leaves.LeavesConfig.fakeplayerLimit) {
-+ sender.sendMessage(ChatColor.RED + "Fakeplayer limit is full");
++ if (botList.bots.size() >= org.leavesmc.leaves.LeavesConfig.fakeplayerLimit) {
++ sender.sendMessage(text("Fakeplayer limit is full", NamedTextColor.RED));
+ return false;
+ }
+
@@ -759,26 +853,27 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+
+ private void onRemove(CommandSender sender, String @NotNull [] args) {
+ if (args.length < 2 || args.length > 5) {
-+ sender.sendMessage(ChatColor.RED + "Use /bot remove [hour] [minute] [second] to remove a fakeplayer");
++ sender.sendMessage(text("Use /bot remove [hour] [minute] [second] to remove a fakeplayer", NamedTextColor.RED));
+ return;
+ }
+
-+ ServerBot bot = ServerBot.getBot(args[1]);
++ BotList botList = BotList.INSTANCE;
++ ServerBot bot = botList.getBotByName(args[1]);
+
+ if (bot == null) {
-+ sender.sendMessage(ChatColor.RED + "This fakeplayer is not in server");
++ sender.sendMessage(text("This fakeplayer is not in server", NamedTextColor.RED));
+ return;
+ }
+
+ if (args.length > 2) {
+ if (args[2].equals("cancel")) {
+ if (bot.removeTaskId == -1) {
-+ sender.sendMessage(ChatColor.RED + "This fakeplayer is not scheduled to be removed");
++ sender.sendMessage(text("This fakeplayer is not scheduled to be removed", NamedTextColor.RED));
+ return;
+ }
+ Bukkit.getScheduler().cancelTask(bot.removeTaskId);
+ bot.removeTaskId = -1;
-+ sender.sendMessage("Remove cancel");
++ sender.sendMessage(text("Remove cancel"));
+ return;
+ }
+
@@ -808,7 +903,7 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+ time += s * 20;
+ }
+ } catch (NumberFormatException e) {
-+ sender.sendMessage(ChatColor.RED + "This fakeplayer is not scheduled to be removed");
++ sender.sendMessage(text("This fakeplayer is not scheduled to be removed", NamedTextColor.RED));
+ return;
+ }
+
@@ -817,9 +912,9 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+ if (isReschedule) {
+ Bukkit.getScheduler().cancelTask(bot.removeTaskId);
+ }
-+ bot.removeTaskId = Bukkit.getScheduler().runTaskLater(CraftScheduler.MINECRAFT, () -> {
++ bot.removeTaskId = Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> {
+ bot.removeTaskId = -1;
-+ bot.onRemove(BotRemoveEvent.RemoveReason.COMMAND, sender);
++ botList.removeBot(bot, BotRemoveEvent.RemoveReason.COMMAND, sender, false);
+ }, time).getTaskId();
+
+ sender.sendMessage("This fakeplayer will be removed in " + h + "h " + m + "m " + s + "s" + (isReschedule ? " (rescheduled)" : ""));
@@ -827,7 +922,7 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+ return;
+ }
+
-+ bot.onRemove(BotRemoveEvent.RemoveReason.COMMAND, sender);
++ botList.removeBot(bot, BotRemoveEvent.RemoveReason.COMMAND, sender, false);
+ }
+
+ private void onAction(CommandSender sender, String @NotNull [] args) {
@@ -836,27 +931,54 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+ }
+
+ if (args.length < 3) {
-+ sender.sendMessage(ChatColor.RED + "Use /bot action to make fakeplayer do action");
++ sender.sendMessage(text("Use /bot action to make fakeplayer do action", NamedTextColor.RED));
+ return;
+ }
+
-+ ServerBot bot = ServerBot.getBot(args[1]);
++ ServerBot bot = BotList.INSTANCE.getBotByName(args[1]);
+ if (bot == null) {
-+ sender.sendMessage(ChatColor.RED + "This fakeplayer is not in server");
++ sender.sendMessage(text("This fakeplayer is not in server", NamedTextColor.RED));
+ return;
+ }
+
+ if (args[2].equals("list")) {
+ sender.sendMessage(bot.getScoreboardName() + "'s action list:");
-+ for (BotAction action : bot.getBotActions()) {
-+ sender.sendMessage(action.getName());
++ for (int i = 0; i < bot.getBotActions().size(); i++) {
++ sender.sendMessage(i + " " + bot.getBotActions().get(i).getName());
++ }
++ return;
++ }
++
++ if (args[2].equals("stop")) {
++ if (args.length < 4) {
++ sender.sendMessage(text("Invalid index", NamedTextColor.RED));
++ return;
++ }
++
++ String index = args[3];
++ if (index.equals("all")) {
++ bot.getBotActions().clear();
++ sender.sendMessage(bot.getScoreboardName() + "'s action list cleared.");
++ } else {
++ try {
++ int i = Integer.parseInt(index);
++ if (i < 0 || i >= bot.getBotActions().size()) {
++ sender.sendMessage(text("Invalid index", NamedTextColor.RED));
++ return;
++ }
++
++ BotAction> action = bot.getBotActions().remove(i);
++ sender.sendMessage(bot.getScoreboardName() + "'s " + action.getName() + " stopped.");
++ } catch (NumberFormatException e) {
++ sender.sendMessage(text("Invalid index", NamedTextColor.RED));
++ }
+ }
+ return;
+ }
+
-+ BotAction action = Actions.getForName(args[2]);
++ BotAction> action = Actions.getForName(args[2]);
+ if (action == null) {
-+ sender.sendMessage(ChatColor.RED + "Invalid action");
++ sender.sendMessage(text("Invalid action", NamedTextColor.RED));
+ return;
+ }
+
@@ -872,38 +994,28 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+ System.arraycopy(args, 3, realArgs, 0, realArgs.length);
+ }
+
-+ BotAction newAction;
-+ if (action instanceof CraftCustomBotAction customBotAction) {
-+ newAction = customBotAction.getNew(player, realArgs);
-+ } else {
-+ newAction = action.getNew(player.getHandle(), action.getArgument().parse(0, realArgs));
++ BotAction> newAction = null;
++ try {
++ if (action instanceof CraftCustomBotAction customBotAction) {
++ newAction = customBotAction.createCraft(player, realArgs);
++ } else {
++ newAction = action.create();
++ newAction.loadCommand(player.getHandle(), action.getArgument().parse(0, realArgs));
++ }
++ } catch (IllegalArgumentException ignore) {
+ }
+
+ if (newAction == null) {
-+ sender.sendMessage(ChatColor.RED + "Action create error, please check your arguments");
++ sender.sendMessage(text("Action create error, please check your arguments", NamedTextColor.RED));
+ return;
+ }
+
-+ BotActionEvent event = new BotActionEvent(bot.getBukkitEntity(), newAction.getName(), realArgs);
-+ Bukkit.getPluginManager().callEvent(event);
-+
-+ if (!event.isCancelled()) {
-+ bot.setBotAction(newAction);
++ if (bot.addBotAction(newAction)) {
+ sender.sendMessage("Action " + action.getName() + " has been issued to " + bot.getName().getString());
+ }
+ }
+
-+ public enum BotConfig {
-+ SKIP_SLEEP, SPAWN_PHANTOM, ALWAYS_SEND_DATA, SIMULATION_DISTANCE;
-+
-+ public final String configName;
-+
-+ BotConfig() {
-+ this.configName = this.name().toLowerCase(Locale.ROOT);
-+ }
-+ }
-+
-+ private static final List acceptConfig = Stream.of(BotConfig.values()).map(config -> config.configName).toList();
++ private static final List acceptConfig = Configs.getConfigs().stream().map(config -> config.config.getName()).toList();
+
+ private void onConfig(CommandSender sender, String @NotNull [] args) {
+ if (!LeavesConfig.fakeplayerModifyConfig) {
@@ -911,114 +1023,123 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+ }
+
+ if (args.length < 3) {
-+ sender.sendMessage(ChatColor.RED + "Use /bot config to modify fakeplayer's config");
++ sender.sendMessage(text("Use /bot config to modify fakeplayer's config", NamedTextColor.RED));
+ return;
+ }
+
-+ ServerBot bot = ServerBot.getBot(args[1]);
++ ServerBot bot = BotList.INSTANCE.getBotByName(args[1]);
+ if (bot == null) {
-+ sender.sendMessage(ChatColor.RED + "This fakeplayer is not in server");
++ sender.sendMessage(text("This fakeplayer is not in server", NamedTextColor.RED));
+ return;
+ }
+
+ if (!acceptConfig.contains(args[2])) {
-+ sender.sendMessage(ChatColor.RED + "This config is not accept");
++ sender.sendMessage(text("This config is not accept", NamedTextColor.RED));
+ return;
+ }
+
-+ BotConfig config = BotConfig.valueOf(args[2].toUpperCase(Locale.ROOT));
++ BotConfig> config = Objects.requireNonNull(Configs.getConfig(args[2])).config;
+ if (args.length < 4) {
-+ String value = null;
-+ switch (config) {
-+ case SKIP_SLEEP -> value = String.valueOf(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);
-+ }
-+ return;
-+ }
-+ case ALWAYS_SEND_DATA -> value = String.valueOf(bot.alwaysSendData);
-+ case SIMULATION_DISTANCE -> value = String.valueOf(bot.getBukkitEntity().getSimulationDistance());
-+ }
-+ sender.sendMessage(bot.getScoreboardName() + "'s " + config.configName + ": " + value);
++ config.getMessage().forEach(sender::sendMessage);
+ } else {
-+ String value = args[3];
++ String[] realArgs = new String[args.length - 3];
++ System.arraycopy(args, 3, realArgs, 0, realArgs.length);
+
-+ BotConfigModifyEvent event = new BotConfigModifyEvent(bot.getBukkitEntity(), config.configName, value);
++ BotConfigModifyEvent event = new BotConfigModifyEvent(bot.getBukkitEntity(), config.getName(), realArgs);
+ Bukkit.getPluginManager().callEvent(event);
+
+ if (event.isCancelled()) {
+ return;
+ }
++ CommandArgumentResult result = config.getArgument().parse(0, realArgs);
+
-+ switch (config) {
-+ case SKIP_SLEEP -> {
-+ boolean realValue = value.equals("true");
-+ bot.fauxSleeping = realValue;
-+ value = String.valueOf(realValue);
-+ }
-+ case SPAWN_PHANTOM -> {
-+ boolean realValue = value.equals("true");
-+ bot.spawnPhantom = realValue;
-+ value = String.valueOf(realValue);
-+ }
-+ case ALWAYS_SEND_DATA -> {
-+ boolean realValue = value.equals("true");
-+ bot.alwaysSendData = realValue;
-+ value = String.valueOf(realValue);
-+ }
-+ case SIMULATION_DISTANCE -> {
-+ try {
-+ int realValue = Integer.parseInt(value);
-+ if (realValue < 2 || realValue > 32) {
-+ sender.sendMessage("simulation_distance must be a number between 2 and 32, got: " + value);
-+ return;
-+ }
-+ bot.getBukkitEntity().setSimulationDistance(realValue);
-+ } catch (NumberFormatException e) {
-+ sender.sendMessage("simulation_distance must be a number between 2 and 32, got: " + value);
-+ return;
-+ }
-+ }
++ try {
++ config.setValue(result);
++ config.getChangeMessage().forEach(sender::sendMessage);
++ } catch (IllegalArgumentException e) {
++ sender.sendMessage(text(e.getMessage(), NamedTextColor.RED));
+ }
-+ sender.sendMessage(bot.getScoreboardName() + "'s " + config.configName + " changed: " + value);
++ }
++ }
++
++ private void onSave(CommandSender sender, String @NotNull [] args) {
++ if (!LeavesConfig.fakeplayerManualSaveAndLoad) {
++ return;
++ }
++
++ if (args.length < 2) {
++ sender.sendMessage(text("Use /bot save to save a fakeplayer", NamedTextColor.RED));
++ return;
++ }
++
++ BotList botList = BotList.INSTANCE;
++ ServerBot bot = botList.getBotByName(args[1]);
++
++ if (bot == null) {
++ sender.sendMessage(text("This fakeplayer is not in server", NamedTextColor.RED));
++ return;
++ }
++
++ botList.removeBot(bot, BotRemoveEvent.RemoveReason.COMMAND, sender, true);
++ sender.sendMessage(bot.getScoreboardName() + " saved to " + bot.createState.realName());
++ }
++
++ private void onLoad(CommandSender sender, String @NotNull [] args) {
++ if (!LeavesConfig.fakeplayerManualSaveAndLoad) {
++ return;
++ }
++
++ if (args.length < 2) {
++ sender.sendMessage(text("Use /bot save to save a fakeplayer", NamedTextColor.RED));
++ return;
++ }
++
++ String realName = args[1];
++ BotList botList = BotList.INSTANCE;
++ if (!botList.getSavedBotList().contains(realName)) {
++ sender.sendMessage(text("This fakeplayer is not saved", NamedTextColor.RED));
++ return;
++ }
++
++ if (botList.loadNewBot(realName) == null) {
++ sender.sendMessage(text("Can't load bot, please check", NamedTextColor.RED));
+ }
+ }
+
+ private void onList(CommandSender sender, String @NotNull [] args) {
++ BotList botList = BotList.INSTANCE;
+ 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();
++ for (ServerBot bot : botList.bots) {
++ Bot bukkitBot = bot.getBukkitEntity();
+ botMap.get(bukkitBot.getWorld()).add(bukkitBot.getName());
+ }
+
-+ sender.sendMessage("Total number: (" + ServerBot.getBots().size() + "/" + org.leavesmc.leaves.LeavesConfig.fakeplayerLimit + ")");
++ sender.sendMessage("Total number: (" + botList.bots.size() + "/" + org.leavesmc.leaves.LeavesConfig.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]);
++ World world = Bukkit.getWorld(args[1]);
+
+ if (world == null) {
-+ sender.sendMessage(ChatColor.RED + "Unknown world");
++ sender.sendMessage(text("Unknown world", NamedTextColor.RED));
+ return;
+ }
+
-+ List botList = new ArrayList<>();
-+ for (ServerBot bot : ServerBot.getBots()) {
-+ Bot bukkitBot = bot.getBukkitPlayer();
++ List snowBotList = new ArrayList<>();
++ for (ServerBot bot : botList.bots) {
++ Bot bukkitBot = bot.getBukkitEntity();
+ if (bukkitBot.getWorld() == world) {
-+ botList.add(bukkitBot.getName());
++ snowBotList.add(bukkitBot.getName());
+ }
+ }
+
-+ sender.sendMessage(world.getName() + "(" + botList.size() + "): " + formatPlayerNameList(botList));
++ sender.sendMessage(world.getName() + "(" + botList.bots.size() + "): " + formatPlayerNameList(snowBotList));
+ }
+ }
+
@@ -1031,95 +1152,348 @@ index 0000000000000000000000000000000000000000..918cdffedddba7cebb8013600bf3a2f5
+ return string.substring(1, string.length() - 1);
+ }
+}
-diff --git a/src/main/java/org/leavesmc/leaves/bot/BotInventoryContainer.java b/src/main/java/org/leavesmc/leaves/bot/BotInventoryContainer.java
+diff --git a/src/main/java/org/leavesmc/leaves/bot/BotCreateState.java b/src/main/java/org/leavesmc/leaves/bot/BotCreateState.java
new file mode 100644
-index 0000000000000000000000000000000000000000..4f5e6e5c1b9d8bd38c98e97fd31b38338f35faa6
+index 0000000000000000000000000000000000000000..ebb3aa94718382e8fac2a8fa617c796a9387709b
--- /dev/null
-+++ b/src/main/java/org/leavesmc/leaves/bot/BotInventoryContainer.java
-@@ -0,0 +1,191 @@
++++ b/src/main/java/org/leavesmc/leaves/bot/BotCreateState.java
+@@ -0,0 +1,120 @@
+package org.leavesmc.leaves.bot;
+
-+import com.google.common.collect.ImmutableList;
-+import com.mojang.datafixers.util.Pair;
-+import net.minecraft.core.NonNullList;
-+import net.minecraft.core.component.DataComponentPatch;
-+import net.minecraft.core.component.DataComponents;
-+import net.minecraft.nbt.CompoundTag;
-+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 net.minecraft.world.item.component.CustomData;
++import net.minecraft.server.MinecraftServer;
++import org.bukkit.Bukkit;
++import org.bukkit.Location;
++import org.bukkit.command.CommandSender;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++import org.leavesmc.leaves.LeavesConfig;
++import org.leavesmc.leaves.entity.Bot;
++import org.leavesmc.leaves.entity.BotCreator;
++import org.leavesmc.leaves.entity.CraftBot;
++import org.leavesmc.leaves.event.bot.BotCreateEvent;
++import org.leavesmc.leaves.plugin.MinecraftInternalPlugin;
+
-+import javax.annotation.Nonnull;
-+import java.util.List;
++import java.util.Objects;
++import java.util.function.Consumer;
+
-+// Power by gugle-carpet-addition(https://github.com/Gu-ZT/gugle-carpet-addition)
-+public class BotInventoryContainer extends SimpleContainer {
++public record BotCreateState(String realName, String name, String skinName, String[] skin, Location location, BotCreateEvent.CreateReason createReason, CommandSender creator) {
+
-+ 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;
++ private static final MinecraftServer server = MinecraftServer.getServer();
+
-+ 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();
++ public ServerBot createNow() {
++ return server.getBotList().createNewBot(this);
+ }
+
-+ @Override
-+ public int getContainerSize() {
-+ return this.items.size() + this.armor.size() + this.offhand.size() + this.buttons.size();
++ @NotNull
++ public static Builder builder(@NotNull String realName, @Nullable Location location) {
++ return new Builder(realName, location);
+ }
+
-+ @Override
-+ public boolean isEmpty() {
-+ for (ItemStack itemStack : this.items) {
-+ if (itemStack.isEmpty()) {
-+ continue;
-+ }
-+ return false;
++ public static class Builder implements BotCreator {
++
++ private final String realName;
++
++ private String name;
++ private Location location;
++
++ private String skinName;
++ private String[] skin;
++
++ private BotCreateEvent.CreateReason createReason;
++ private CommandSender creator;
++
++ private Builder(@NotNull String realName, @Nullable Location location) {
++ Objects.requireNonNull(realName);
++
++ this.realName = realName;
++ this.location = location;
++
++ this.name = LeavesConfig.fakeplayerPrefix + realName + LeavesConfig.fakeplayerSuffix;
++ this.skinName = this.realName;
++ this.skin = null;
++ this.createReason = BotCreateEvent.CreateReason.UNKNOWN;
++ this.creator = null;
+ }
-+ for (ItemStack itemStack : this.armor) {
-+ if (itemStack.isEmpty()) {
-+ continue;
-+ }
-+ return false;
++
++ public Builder name(@NotNull String name) {
++ Objects.requireNonNull(name);
++ this.name = name;
++ return this;
+ }
-+ for (ItemStack itemStack : this.offhand) {
-+ if (itemStack.isEmpty()) {
-+ continue;
-+ }
-+ return false;
++
++ public Builder skinName(@Nullable String skinName) {
++ this.skinName = skinName;
++ return this;
+ }
-+ 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 Builder skin(@Nullable String[] skin) {
++ this.skin = skin;
++ return this;
+ }
-+ }
+
-+ public Pair, Integer> getItemSlot(int slot) {
-+ switch (slot) {
-+ case 0 -> {
-+ return new Pair<>(buttons, 0);
++ public Builder mojangAPISkin() {
++ if (this.skinName != null) {
++ this.skin = MojangAPI.getSkin(this.skinName);
+ }
-+ case 1, 2, 3, 4 -> {
++ return this;
++ }
++
++ public Builder location(@NotNull Location location) {
++ this.location = location;
++ return this;
++ }
++
++ public Builder createReason(@NotNull BotCreateEvent.CreateReason createReason) {
++ Objects.requireNonNull(createReason);
++ this.createReason = createReason;
++ return this;
++ }
++
++ public Builder creator(CommandSender creator) {
++ this.creator = creator;
++ return this;
++ }
++
++ public BotCreateState build() {
++ return new BotCreateState(realName, name, skinName, skin, location, createReason, creator);
++ }
++
++ public void spawnWithSkin(Consumer consumer) {
++ Bukkit.getScheduler().runTaskAsynchronously(MinecraftInternalPlugin.INSTANCE, () -> {
++ this.mojangAPISkin();
++ Bukkit.getScheduler().runTask(MinecraftInternalPlugin.INSTANCE, () -> {
++ CraftBot bot = this.spawn();
++ if (bot != null && consumer != null) {
++ consumer.accept(bot);
++ }
++ });
++ });
++ }
++
++ @Nullable
++ public CraftBot spawn() {
++ Objects.requireNonNull(this.location);
++ ServerBot bot = this.build().createNow();
++ return bot != null ? bot.getBukkitEntity() : null;
++ }
++ }
++}
+diff --git a/src/main/java/org/leavesmc/leaves/bot/BotDataStorage.java b/src/main/java/org/leavesmc/leaves/bot/BotDataStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..53bece66534df40ef8cf559c12e2c472a791b9c3
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/bot/BotDataStorage.java
+@@ -0,0 +1,121 @@
++package org.leavesmc.leaves.bot;
++
++import com.mojang.logging.LogUtils;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.NbtAccounter;
++import net.minecraft.nbt.NbtIo;
++import net.minecraft.world.entity.player.Player;
++import net.minecraft.world.level.storage.LevelResource;
++import net.minecraft.world.level.storage.LevelStorageSource;
++import org.jetbrains.annotations.NotNull;
++import org.slf4j.Logger;
++
++import java.io.File;
++import java.io.IOException;
++import java.util.Optional;
++
++public class BotDataStorage implements IPlayerDataStorage {
++
++ private static final LevelResource BOT_DATA_DIR = new LevelResource("fakeplayerdata");
++ private static final LevelResource BOT_LIST_FILE = new LevelResource("fakeplayer.dat");
++
++ private static final Logger LOGGER = LogUtils.getLogger();
++ private final File botDir;
++ private final File botListFile;
++
++ private CompoundTag savedBotList;
++
++ public BotDataStorage(LevelStorageSource.@NotNull LevelStorageAccess session) {
++ this.botDir = session.getLevelPath(BOT_DATA_DIR).toFile();
++ this.botListFile = session.getLevelPath(BOT_LIST_FILE).toFile();
++ this.botDir.mkdirs();
++
++ this.savedBotList = new CompoundTag();
++ if (this.botListFile.exists() && this.botListFile.isFile()) {
++ try {
++ Optional.of(NbtIo.readCompressed(this.botListFile.toPath(), NbtAccounter.unlimitedHeap())).ifPresent(tag -> this.savedBotList = tag);
++ } catch (Exception exception) {
++ BotDataStorage.LOGGER.warn("Failed to load player data list");
++ }
++ }
++ }
++
++ @Override
++ public void save(Player player) {
++ boolean flag = true;
++ try {
++ CompoundTag nbt = player.saveWithoutId(new CompoundTag());
++ File file = new File(this.botDir, player.getStringUUID() + ".dat");
++
++ if (file.exists() && file.isFile()) {
++ if (!file.delete()) {
++ throw new IOException("Failed to delete file: " + file);
++ }
++ }
++ if (!file.createNewFile()) {
++ throw new IOException("Failed to create nbt file: " + file);
++ }
++ NbtIo.writeCompressed(nbt, file.toPath());
++ } catch (Exception exception) {
++ BotDataStorage.LOGGER.warn("Failed to save fakeplayer data for {}", player.getScoreboardName(), exception);
++ flag = false;
++ }
++
++ if (flag && player instanceof ServerBot bot) {
++ CompoundTag nbt = new CompoundTag();
++ nbt.putString("name", bot.createState.name());
++ nbt.putUUID("uuid", bot.getUUID());
++ nbt.putBoolean("resume", bot.resume);
++ this.savedBotList.put(bot.createState.realName(), nbt);
++ this.saveBotList();
++ }
++ }
++
++ @Override
++ public Optional load(Player player) {
++ return this.load(player.getScoreboardName(), player.getStringUUID()).map((nbt) -> {
++ player.load(nbt);
++ return nbt;
++ });
++ }
++
++ private Optional load(String name, String uuid) {
++ File file = new File(this.botDir, uuid + ".dat");
++
++ if (file.exists() && file.isFile()) {
++ try {
++ Optional optional = Optional.of(NbtIo.readCompressed(file.toPath(), NbtAccounter.unlimitedHeap()));
++ if (!file.delete()) {
++ throw new IOException("Failed to delete fakeplayer data");
++ }
++ this.savedBotList.remove(name);
++ this.saveBotList();
++ return optional;
++ } catch (Exception exception) {
++ BotDataStorage.LOGGER.warn("Failed to load fakeplayer data for {}", name);
++ }
++ }
++
++ return Optional.empty();
++ }
++
++ private void saveBotList() {
++ try {
++ if (this.botListFile.exists() && this.botListFile.isFile()) {
++ if (!this.botListFile.delete()) {
++ throw new IOException("Failed to delete file: " + this.botListFile);
++ }
++ }
++ if (!this.botListFile.createNewFile()) {
++ throw new IOException("Failed to create nbt file: " + this.botListFile);
++ }
++ NbtIo.writeCompressed(this.savedBotList, this.botListFile.toPath());
++ } catch (Exception exception) {
++ BotDataStorage.LOGGER.warn("Failed to save player data list");
++ }
++ }
++
++ public CompoundTag getSavedBotList() {
++ return savedBotList;
++ }
++}
+diff --git a/src/main/java/org/leavesmc/leaves/bot/BotInventoryContainer.java b/src/main/java/org/leavesmc/leaves/bot/BotInventoryContainer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4f5e6e5c1b9d8bd38c98e97fd31b38338f35faa6
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/bot/BotInventoryContainer.java
+@@ -0,0 +1,191 @@
++package org.leavesmc.leaves.bot;
++
++import com.google.common.collect.ImmutableList;
++import com.mojang.datafixers.util.Pair;
++import net.minecraft.core.NonNullList;
++import net.minecraft.core.component.DataComponentPatch;
++import net.minecraft.core.component.DataComponents;
++import net.minecraft.nbt.CompoundTag;
++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 net.minecraft.world.item.component.CustomData;
++
++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 -> {
@@ -1228,12 +1602,356 @@ index 0000000000000000000000000000000000000000..4f5e6e5c1b9d8bd38c98e97fd31b3833
+ }
+ }
+}
+diff --git a/src/main/java/org/leavesmc/leaves/bot/BotList.java b/src/main/java/org/leavesmc/leaves/bot/BotList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..003f1e01ba56ea2fa9ff311675c58b1d0a38d53b
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/bot/BotList.java
+@@ -0,0 +1,338 @@
++package org.leavesmc.leaves.bot;
++
++import com.google.common.collect.Maps;
++import com.mojang.authlib.GameProfile;
++import com.mojang.authlib.properties.Property;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.adventure.PaperAdventure;
++import net.kyori.adventure.text.format.NamedTextColor;
++import net.kyori.adventure.text.format.Style;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.network.chat.Component;
++import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket;
++import net.minecraft.resources.ResourceKey;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.entity.EntityType;
++import net.minecraft.world.level.Level;
++import org.bukkit.Bukkit;
++import org.bukkit.Location;
++import org.bukkit.command.CommandSender;
++import org.bukkit.craftbukkit.CraftWorld;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++import org.leavesmc.leaves.LeavesConfig;
++import org.leavesmc.leaves.event.bot.BotCreateEvent;
++import org.leavesmc.leaves.event.bot.BotJoinEvent;
++import org.leavesmc.leaves.event.bot.BotLoadEvent;
++import org.leavesmc.leaves.event.bot.BotRemoveEvent;
++import org.leavesmc.leaves.event.bot.BotSpawnLocationEvent;
++import org.slf4j.Logger;
++
++import java.util.Iterator;
++import java.util.List;
++import java.util.Locale;
++import java.util.Map;
++import java.util.Optional;
++import java.util.UUID;
++import java.util.concurrent.CopyOnWriteArrayList;
++
++public class BotList {
++
++ public static BotList INSTANCE;
++
++ private static final Logger LOGGER = LogUtils.getLogger();
++
++ private final MinecraftServer server;
++
++ public final List bots = new CopyOnWriteArrayList<>();
++ private final BotDataStorage dataStorage;
++
++ private final Map botsByUUID = Maps.newHashMap();
++ private final Map botsByName = Maps.newHashMap();
++
++ public BotList(MinecraftServer server) {
++ this.server = server;
++ this.dataStorage = new BotDataStorage(server.storageSource);
++ INSTANCE = this;
++ }
++
++ public ServerBot createNewBot(BotCreateState state) {
++ BotCreateEvent event = new BotCreateEvent(state.name(), state.skinName(), state.location(), state.createReason(), state.creator());
++ event.setCancelled(!isCreateLegal(state.name()));
++ this.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(this.server, world, profile);
++ bot.createState = state;
++ if (event.getCreator() instanceof org.bukkit.entity.Player player) {
++ bot.createPlayer = player.getUniqueId();
++ }
++
++ return this.placeNewBot(bot, world, location, null);
++ }
++
++ public ServerBot loadNewBot(String realName) {
++ return this.loadNewBot(realName, this.dataStorage);
++ }
++
++ public ServerBot loadNewBot(String realName, IPlayerDataStorage playerIO) {
++ UUID uuid = BotUtil.getBotUUID(realName);
++
++ BotLoadEvent event = new BotLoadEvent(realName, uuid);
++ this.server.server.getPluginManager().callEvent(event);
++ if (event.isCancelled()) {
++ return null;
++ }
++
++ ServerBot bot = new ServerBot(this.server, this.server.getLevel(Level.OVERWORLD), new GameProfile(uuid, realName));
++ bot.connection = new ServerBotPacketListenerImpl(this.server, bot);
++ Optional optional = playerIO.load(bot);
++
++ if (optional.isEmpty()) {
++ return null;
++ }
++
++ ResourceKey resourcekey = null;
++ if (optional.get().contains("WorldUUIDMost") && optional.get().contains("WorldUUIDLeast")) {
++ org.bukkit.World bWorld = Bukkit.getServer().getWorld(new UUID(optional.get().getLong("WorldUUIDMost"), optional.get().getLong("WorldUUIDLeast")));
++ if (bWorld != null) {
++ resourcekey = ((CraftWorld) bWorld).getHandle().dimension();
++ }
++ }
++ if (resourcekey == null) {
++ return null;
++ }
++
++ ServerLevel world = this.server.getLevel(resourcekey);
++ return this.placeNewBot(bot, world, bot.getLocation(), optional.get());
++ }
++
++ public ServerBot placeNewBot(ServerBot bot, ServerLevel world, Location location, @Nullable CompoundTag nbt) {
++ bot.isRealPlayer = true;
++ bot.connection = new ServerBotPacketListenerImpl(this.server, bot);
++ bot.setServerLevel(world);
++
++ BotSpawnLocationEvent event = new BotSpawnLocationEvent(bot.getBukkitEntity(), location);
++ this.server.server.getPluginManager().callEvent(event);
++ location = event.getSpawnLocation();
++
++ bot.spawnIn(world);
++ bot.gameMode.setLevel((ServerLevel) bot.level());
++
++ bot.setPosRaw(location.getX(), location.getY(), location.getZ());
++ bot.setRot(location.getYaw(), location.getPitch());
++
++ this.bots.add(bot);
++ this.botsByName.put(bot.getScoreboardName().toLowerCase(Locale.ROOT), bot);
++ this.botsByUUID.put(bot.getUUID(), bot);
++
++ bot.supressTrackerForLogin = true;
++ world.addNewPlayer(bot);
++ this.mountSavedVehicle(bot, world, nbt);
++
++ BotJoinEvent event1 = new BotJoinEvent(bot.getBukkitEntity(), PaperAdventure.asAdventure(Component.translatable("multiplayer.player.joined", bot.getDisplayName())).style(Style.style(NamedTextColor.YELLOW)));
++ this.server.server.getPluginManager().callEvent(event1);
++
++ net.kyori.adventure.text.Component joinMessage = event1.joinMessage();
++ if (joinMessage != null && !joinMessage.equals(net.kyori.adventure.text.Component.empty())) {
++ this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(joinMessage), false);
++ }
++
++ bot.renderAll();
++ bot.supressTrackerForLogin = false;
++ bot.serverLevel().getChunkSource().chunkMap.addEntity(bot);
++ BotList.LOGGER.info("{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", bot.getName().getString(), "Local", bot.getId(), bot.serverLevel().serverLevelData.getLevelName(), bot.getX(), bot.getY(), bot.getZ());
++ return bot;
++ }
++
++ private void mountSavedVehicle(ServerPlayer player, ServerLevel worldserver1, @Nullable CompoundTag nbt) {
++ Optional optional = Optional.ofNullable(nbt);
++ if (optional.isPresent() && optional.get().contains("RootVehicle", 10)) {
++ CompoundTag nbttagcompound = optional.get().getCompound("RootVehicle");
++ Entity entity = EntityType.loadEntityRecursive(nbttagcompound.getCompound("Entity"), worldserver1, (entity1) -> {
++ return !worldserver1.addWithUUID(entity1, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.MOUNT) ? null : entity1;
++ });
++
++ if (entity != null) {
++ UUID uuid;
++
++ if (nbttagcompound.hasUUID("Attach")) {
++ uuid = nbttagcompound.getUUID("Attach");
++ } else {
++ uuid = null;
++ }
++
++ Iterator iterator;
++ Entity entity1;
++
++ if (entity.getUUID().equals(uuid)) {
++ player.startRiding(entity, true);
++ } else {
++ iterator = entity.getIndirectPassengers().iterator();
++
++ while (iterator.hasNext()) {
++ entity1 = iterator.next();
++ if (entity1.getUUID().equals(uuid)) {
++ player.startRiding(entity1, true);
++ break;
++ }
++ }
++ }
++
++ if (!player.isPassenger()) {
++ BotList.LOGGER.warn("Couldn't reattach entity to fakeplayer");
++ entity.discard();
++ iterator = entity.getIndirectPassengers().iterator();
++
++ while (iterator.hasNext()) {
++ entity1 = iterator.next();
++ entity1.discard();
++ }
++ }
++ }
++ }
++ }
++
++ public void removeBot(@NotNull ServerBot bot, @NotNull BotRemoveEvent.RemoveReason reason, @Nullable CommandSender remover, boolean saved) {
++ this.removeBot(bot, reason, remover, saved, this.dataStorage);
++ }
++
++ public void removeBot(@NotNull ServerBot bot, @NotNull BotRemoveEvent.RemoveReason reason, @Nullable CommandSender remover, boolean saved, IPlayerDataStorage playerIO) {
++ BotRemoveEvent event = new BotRemoveEvent(bot.getBukkitEntity(), reason, remover, PaperAdventure.asAdventure(Component.translatable("multiplayer.player.left", bot.getDisplayName())).style(Style.style(NamedTextColor.YELLOW)), saved);
++ this.server.server.getPluginManager().callEvent(event);
++
++ if (event.isCancelled() && event.getReason() != BotRemoveEvent.RemoveReason.INTERNAL) {
++ return;
++ }
++
++ if (bot.removeTaskId != -1) {
++ Bukkit.getScheduler().cancelTask(bot.removeTaskId);
++ bot.removeTaskId = -1;
++ }
++
++ if (this.server.isSameThread()) {
++ bot.doTick();
++ }
++
++ if (event.shouldSave()) {
++ playerIO.save(bot);
++ } else {
++ bot.dropAll();
++ }
++
++ if (bot.isPassenger()) {
++ Entity entity = bot.getRootVehicle();
++ if (entity.hasExactlyOnePlayerPassenger()) {
++ bot.stopRiding();
++ entity.getPassengersAndSelf().forEach((entity1) -> {
++ if (entity1 instanceof net.minecraft.world.entity.npc.AbstractVillager villager) {
++ final net.minecraft.world.entity.player.Player human = villager.getTradingPlayer();
++ if (human != null) {
++ villager.setTradingPlayer(null);
++ }
++ }
++ entity1.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER);
++ });
++ }
++ }
++
++ bot.unRide();
++ bot.serverLevel().removePlayerImmediately(bot, Entity.RemovalReason.UNLOADED_WITH_PLAYER);
++ this.bots.remove(bot);
++ this.botsByName.remove(bot.getScoreboardName().toLowerCase(Locale.ROOT));
++
++ UUID uuid = bot.getUUID();
++ ServerBot bot1 = this.botsByUUID.get(uuid);
++ if (bot1 == bot) {
++ this.botsByUUID.remove(uuid);
++ }
++
++ bot.removeTab();
++ for (ServerPlayer player : bot.serverLevel().players()) {
++ if (!(player instanceof ServerBot) && !bot.needSendFakeData(player)) {
++ player.connection.send(new ClientboundRemoveEntitiesPacket(bot.getId()));
++ }
++ }
++
++ net.kyori.adventure.text.Component removeMessage = event.removeMessage();
++ if (removeMessage != null && !removeMessage.equals(net.kyori.adventure.text.Component.empty())) {
++ this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(removeMessage), false);
++ }
++ }
++
++ public void removeAll() {
++ for (ServerBot bot : this.bots) {
++ bot.resume = LeavesConfig.fakeplayerResident;
++ this.removeBot(bot, BotRemoveEvent.RemoveReason.INTERNAL, null, LeavesConfig.fakeplayerResident);
++ }
++ }
++
++ public void loadResume() {
++ if (LeavesConfig.fakeplayerSupport && LeavesConfig.fakeplayerResident) {
++ for (String realName : this.getSavedBotList().getAllKeys()) {
++ CompoundTag nbt = this.getSavedBotList().getCompound(realName);
++ if (nbt.getBoolean("resume")) {
++ this.loadNewBot(realName);
++ }
++ }
++ }
++ }
++
++ public void networkTick() {
++ this.bots.forEach(ServerBot::doTick);
++ }
++
++ @Nullable
++ public ServerBot getBot(@NotNull UUID uuid) {
++ return this.botsByUUID.get(uuid);
++ }
++
++ @Nullable
++ public ServerBot getBotByName(@NotNull String name) {
++ return this.botsByName.get(name.toLowerCase(Locale.ROOT));
++ }
++
++ public CompoundTag getSavedBotList() {
++ return this.dataStorage.getSavedBotList();
++ }
++
++ public boolean isCreateLegal(@NotNull String name) {
++ if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) {
++ return false;
++ }
++
++ if (Bukkit.getPlayerExact(name) != null || this.getBotByName(name) != null) {
++ return false;
++ }
++
++ if (LeavesConfig.unableFakeplayerNames.contains(name)) {
++ return false;
++ }
++
++ return this.bots.size() < LeavesConfig.fakeplayerLimit;
++ }
++
++ public static class CustomGameProfile extends GameProfile {
++
++ public CustomGameProfile(UUID uuid, String name, String[] skin) {
++ super(uuid, name);
++ this.setSkin(skin);
++ }
++
++ public void setSkin(String[] skin) {
++ if (skin != null) {
++ this.getProperties().put("textures", new Property("textures", skin[0], skin[1]));
++ }
++ }
++ }
++}
diff --git a/src/main/java/org/leavesmc/leaves/bot/BotStatsCounter.java b/src/main/java/org/leavesmc/leaves/bot/BotStatsCounter.java
new file mode 100644
-index 0000000000000000000000000000000000000000..5bd34353b6ea86cd15ff48b8d6570167f35d75f0
+index 0000000000000000000000000000000000000000..10494446f915bc1720a18cfe75b2cab2404646e9
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/BotStatsCounter.java
-@@ -0,0 +1,38 @@
+@@ -0,0 +1,36 @@
+package org.leavesmc.leaves.bot;
+
+import com.mojang.datafixers.DataFixer;
@@ -1241,6 +1959,7 @@ index 0000000000000000000000000000000000000000..5bd34353b6ea86cd15ff48b8d6570167
+import net.minecraft.stats.ServerStatsCounter;
+import net.minecraft.stats.Stat;
+import net.minecraft.world.entity.player.Player;
++import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
@@ -1254,57 +1973,35 @@ index 0000000000000000000000000000000000000000..5bd34353b6ea86cd15ff48b8d6570167
+
+ @Override
+ public void save() {
-+
+ }
+
+ @Override
-+ public void setValue(Player player, Stat> stat, int value) {
-+
++ public void setValue(@NotNull Player player, @NotNull Stat> stat, int value) {
+ }
+
+ @Override
-+ public void parseLocal(DataFixer dataFixer, String json) {
-+
++ public void parseLocal(@NotNull DataFixer dataFixer, @NotNull String json) {
+ }
+
+ @Override
-+ public int getValue(Stat> stat) {
++ public int getValue(@NotNull Stat> stat) {
+ return 0;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/BotUtil.java b/src/main/java/org/leavesmc/leaves/bot/BotUtil.java
new file mode 100644
-index 0000000000000000000000000000000000000000..0882cd685a6c3a15dd8514e26cc4c31b1d5a8525
+index 0000000000000000000000000000000000000000..78414d1f53328cdc2963264ecb4f5a65e9783798
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/BotUtil.java
-@@ -0,0 +1,228 @@
+@@ -0,0 +1,73 @@
+package org.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 org.leavesmc.leaves.LeavesLogger;
-+import org.leavesmc.leaves.bot.agent.Actions;
-+import org.leavesmc.leaves.bot.agent.BotAction;
-+import org.leavesmc.leaves.event.bot.BotCreateEvent;
+
-+import java.io.File;
-+import java.io.IOException;
-+import java.util.Collection;
-+import java.util.Map;
+import java.util.UUID;
+
+public class BotUtil {
@@ -1362,175 +2059,57 @@ index 0000000000000000000000000000000000000000..0882cd685a6c3a15dd8514e26cc4c31b
+ }
+
+ @NotNull
-+ public static JsonObject saveBot(@NotNull ServerBot bot) {
-+ JsonObject fakePlayer = getBotJson(bot);
-+
-+ 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()) {
-+ if (!parent.mkdirs()) {
-+ throw new IOException("Failed to create directory: " + parent);
-+ }
-+ }
-+ if (file.exists() && file.isFile()) {
-+ if (!file.delete()) {
-+ throw new IOException("Failed to delete file: " + file);
-+ }
-+ }
-+ if (!file.createNewFile()) {
-+ throw new IOException("Failed to create nbt file: " + file);
-+ }
-+ NbtIo.writeCompressed(invnbt, file.toPath());
-+ } catch (IOException e) {
-+ LeavesLogger.LOGGER.warning("Failed to save fakeplayer inv: ", e);
-+ }
-+
-+ return fakePlayer;
++ public static UUID getBotUUID(@NotNull BotCreateState state) {
++ return getBotUUID(state.realName());
+ }
+
-+ private static @NotNull JsonObject getBotJson(@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;
-+ String realName = bot.createState.getRealName();
-+ String name = bot.createState.getName();
-+ String[] skinValue = bot.createState.skin;
-+
-+ 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);
-+ fakePlayer.addProperty("real_name", realName);
-+ fakePlayer.addProperty("name", name);
-+
-+ if (skinValue != null) {
-+ JsonArray jsonArray = new JsonArray();
-+ for (String str : skinValue) {
-+ jsonArray.add(str);
-+ }
-+ fakePlayer.add("skin_value", jsonArray);
-+ }
-+
-+ return fakePlayer;
++ public static UUID getBotUUID(@NotNull String realName) {
++ return UUID.nameUUIDFromBytes(("Fakeplayer:" + realName).getBytes(Charsets.UTF_8));
+ }
++}
+diff --git a/src/main/java/org/leavesmc/leaves/bot/IPlayerDataStorage.java b/src/main/java/org/leavesmc/leaves/bot/IPlayerDataStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7ebe4d6c71e90be92387a585ea581c6b2c4af89d
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/bot/IPlayerDataStorage.java
+@@ -0,0 +1,13 @@
++package org.leavesmc.leaves.bot;
+
-+ public static void loadBot(Map.@NotNull Entry entry) {
-+ JsonObject fakePlayer = entry.getValue().getAsJsonObject();
-+
-+ String name = entry.getKey();
-+ String realName = name;
-+ if (fakePlayer.has("real_name")) {
-+ realName = fakePlayer.get("real_name").getAsString();
-+ name = fakePlayer.get("name").getAsString();
-+ }
-+
-+ 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();
-+
-+ String[] skinValue = null;
-+ if (fakePlayer.has("skin_value")) {
-+ JsonArray jsonArray = fakePlayer.get("skin_value").getAsJsonArray();
-+ skinValue = new String[jsonArray.size()];
-+ for (int i = 0; i < jsonArray.size(); i++) {
-+ skinValue[i] = jsonArray.get(i).getAsString();
-+ }
-+ }
-+
-+ Location location = new Location(Bukkit.getWorld(dimension), pos_x, pos_y, pos_z, yaw, pitch);
-+ ServerBot.BotCreateState state = new ServerBot.BotCreateState(location, name, realName, skin, skinValue, BotCreateEvent.CreateReason.INTERNAL, null);
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.world.entity.player.Player;
+
-+ 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.toPath(), NbtAccounter.unlimitedHeap());
-+ inv = nbt.getList("Inventory", 10);
-+ } catch (IOException e) {
-+ LeavesLogger.LOGGER.warning("Failed to load inventory: ", e);
-+ }
-+ if (!file.delete()) {
-+ LeavesLogger.LOGGER.warning("Failed to delete file: " + file);
-+ }
-+ }
++import java.util.Optional;
+
-+ final JsonArray finalActions = fakePlayer.get("actions").getAsJsonArray();
-+ final ListTag finalInv = inv;
-+ state.createNow(serverBot -> {
-+ if (finalInv != null) {
-+ serverBot.getInventory().load(finalInv);
-+ }
++public interface IPlayerDataStorage {
+
-+ 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);
-+ }
-+ }
-+ });
-+ }
++ void save(Player player);
+
-+ @NotNull
-+ public static UUID getBotUUID(ServerBot.@NotNull BotCreateState state) {
-+ return UUID.nameUUIDFromBytes(("Fakeplayer:" + state.getRealName()).getBytes(Charsets.UTF_8));
-+ }
++ Optional load(Player player);
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/MojangAPI.java b/src/main/java/org/leavesmc/leaves/bot/MojangAPI.java
new file mode 100644
-index 0000000000000000000000000000000000000000..0db337866c71283464d026a4f230016b31d1a8cd
+index 0000000000000000000000000000000000000000..517e3321b866abe9d17a6fe9e919528b50bb130a
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/MojangAPI.java
-@@ -0,0 +1,41 @@
+@@ -0,0 +1,39 @@
+package org.leavesmc.leaves.bot;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
++import org.leavesmc.leaves.LeavesConfig;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
-+import java.net.URL;
++import java.net.URI;
+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)) {
++ if (LeavesConfig.fakeplayerCacheSkin && CACHE.containsKey(name)) {
+ return CACHE.get(name);
+ }
+
@@ -1542,246 +2121,131 @@ index 0000000000000000000000000000000000000000..0db337866c71283464d026a4f230016b
+ // 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) {
++ String uuid = JsonParser.parseReader(new InputStreamReader(URI.create("https://api.mojang.com/users/profiles/minecraft/" + name).toURL().openStream()))
++ .getAsJsonObject().get("id").getAsString();
++ JsonObject property = JsonParser.parseReader(new InputStreamReader(URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false").toURL().openStream()))
++ .getAsJsonObject().get("properties").getAsJsonArray().get(0).getAsJsonObject();
++ return new String[]{property.get("value").getAsString(), property.get("signature").getAsString()};
++ } catch (IOException | IllegalStateException | IllegalArgumentException e) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/ServerBot.java b/src/main/java/org/leavesmc/leaves/bot/ServerBot.java
new file mode 100644
-index 0000000000000000000000000000000000000000..462d58ad184ebe6bd6f161bff1481745b5723cf0
+index 0000000000000000000000000000000000000000..18cf5182c33c43690d7329d6bd435ebc8ee517e5
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/ServerBot.java
-@@ -0,0 +1,782 @@
+@@ -0,0 +1,543 @@
+package org.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.google.common.collect.ImmutableMap;
+import com.mojang.authlib.GameProfile;
-+import com.mojang.authlib.properties.Property;
+import io.papermc.paper.adventure.PaperAdventure;
+import io.papermc.paper.event.entity.EntityKnockbackEvent;
-+import net.kyori.adventure.text.format.NamedTextColor;
-+import net.kyori.adventure.text.format.Style;
-+import net.minecraft.Util;
+import net.minecraft.core.BlockPos;
-+import net.minecraft.network.Connection;
-+import net.minecraft.network.PacketSendListener;
++import net.minecraft.core.particles.BlockParticleOption;
++import net.minecraft.core.particles.ParticleTypes;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.ListTag;
++import net.minecraft.nbt.StringTag;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.protocol.Packet;
-+import net.minecraft.network.protocol.PacketFlow;
+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.ChunkMap;
+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.util.Mth;
+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.item.ItemStack;
++import net.minecraft.world.level.GameRules;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.gameevent.GameEvent;
+import net.minecraft.world.level.portal.DimensionTransition;
-+import net.minecraft.world.level.storage.LevelResource;
-+import net.minecraft.world.phys.AABB;
+import net.minecraft.world.phys.EntityHitResult;
+import net.minecraft.world.phys.Vec3;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
-+import org.bukkit.Material;
-+import org.bukkit.command.CommandSender;
-+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 org.jetbrains.annotations.Nullable;
+import org.leavesmc.leaves.LeavesConfig;
+import org.leavesmc.leaves.LeavesLogger;
++import org.leavesmc.leaves.bot.agent.Actions;
+import org.leavesmc.leaves.bot.agent.BotAction;
-+import org.leavesmc.leaves.bot.agent.actions.StopAction;
-+import org.leavesmc.leaves.entity.Bot;
++import org.leavesmc.leaves.bot.agent.BotConfig;
++import org.leavesmc.leaves.bot.agent.Configs;
+import org.leavesmc.leaves.entity.CraftBot;
++import org.leavesmc.leaves.event.bot.BotActionScheduleEvent;
+import org.leavesmc.leaves.event.bot.BotCreateEvent;
++import org.leavesmc.leaves.event.bot.BotDeathEvent;
+import org.leavesmc.leaves.event.bot.BotInventoryOpenEvent;
-+import org.leavesmc.leaves.event.bot.BotJoinEvent;
+import org.leavesmc.leaves.event.bot.BotRemoveEvent;
++import org.leavesmc.leaves.plugin.MinecraftInternalPlugin;
+import org.leavesmc.leaves.util.MathUtils;
+
-+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.ArrayList;
+import java.util.EnumSet;
-+import java.util.HashMap;
-+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
++import java.util.Objects;
+import java.util.UUID;
-+import java.util.concurrent.CopyOnWriteArrayList;
-+import java.util.function.Consumer;
-+import java.util.function.Predicate;
-+
-+// 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;
-+
-+ public int removeTaskId = -1;
-+
-+ 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.gameMode = new ServerBotGameMode(this);
-+ 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 = LeavesConfig.fakeplayerSkipSleep;
-+ this.spawnPhantom = LeavesConfig.fakeplayerSpawnPhantom;
-+ this.alwaysSendData = LeavesConfig.alwaysSendFakeplayerData;
-+ }
-+
-+ public static ServerBot createBot(@NotNull BotCreateState state) {
-+ if (!isCreateLegal(state.name)) {
-+ return null;
-+ }
-+
-+ MinecraftServer server = MinecraftServer.getServer();
-+
-+ BotCreateEvent event = new BotCreateEvent(state.name, state.skinName, state.loc, state.createReason, state.creator);
-+ 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) {
-+ }
++import java.util.function.Predicate;
+
-+ @Override
-+ public void send(@NotNull Packet> packet, @Nullable PacketSendListener packetsendlistener) {
-+ }
++// TODO test
++public class ServerBot extends ServerPlayer {
+
-+ @Override
-+ public void send(@NotNull Packet> packet, @Nullable PacketSendListener callbacks, boolean flush) {
-+ }
-+ }, bot, CommonListenerCookie.createInitial(profile, false));
-+ bot.isRealPlayer = true;
-+ bot.createState = state;
-+ if (event.getCreator().isPresent() && event.getCreator().get() instanceof org.bukkit.entity.Player player) {
-+ bot.createPlayer = player.getUniqueId();
-+ }
++ private final Map, BotConfig>> configs;
++ private final List> actions;
+
-+ bot.teleportTo(location.getX(), location.getY(), location.getZ());
-+ bot.setRot(location.getYaw(), location.getPitch());
-+ world.addFreshEntity(bot, CreatureSpawnEvent.SpawnReason.COMMAND);
++ public boolean resume = false;
++ public BotCreateState createState;
++ public UUID createPlayer;
+
-+ bot.renderAll();
-+ server.getPlayerList().addNewBot(bot);
-+ bots.add(bot);
++ private final int tracingRange;
++ private final ServerStatsCounter stats;
++ private final BotInventoryContainer container;
+
-+ BotJoinEvent event1 = new BotJoinEvent(bot.getBukkitPlayer(), PaperAdventure.asAdventure(Component.translatable("multiplayer.player.joined", bot.getDisplayName())).style(Style.style(NamedTextColor.YELLOW)));
-+ server.server.getPluginManager().callEvent(event1);
++ public int notSleepTicks;
+
-+ net.kyori.adventure.text.Component joinMessage = event1.joinMessage();
-+ if (joinMessage != null && !joinMessage.equals(net.kyori.adventure.text.Component.empty())) {
-+ server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(joinMessage), false);
-+ }
++ public int removeTaskId = -1;
+
-+ return bot;
-+ }
++ private Vec3 knockback = Vec3.ZERO;
+
-+ public static boolean isCreateLegal(@NotNull String name) {
-+ if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) {
-+ return false;
-+ }
++ public ServerBot(MinecraftServer server, ServerLevel world, GameProfile profile) {
++ super(server, world, profile, ClientInformation.createDefault());
++ this.entityData.set(Player.DATA_PLAYER_MODE_CUSTOMISATION, (byte) -2);
+
-+ if (Bukkit.getPlayerExact(name) != null || ServerBot.getBot(name) != null) {
-+ return false;
-+ }
++ this.gameMode = new ServerBotGameMode(this);
++ this.actions = new ArrayList<>();
+
-+ if (org.leavesmc.leaves.LeavesConfig.unableFakeplayerNames.contains(name)) {
-+ return false;
++ ImmutableMap.Builder, BotConfig>> configBuilder = ImmutableMap.builder();
++ for (Configs> config : Configs.getConfigs()) {
++ configBuilder.put(config, config.config.create(this));
+ }
++ this.configs = configBuilder.build();
+
-+ return ServerBot.getBots().size() < org.leavesmc.leaves.LeavesConfig.fakeplayerLimit;
-+ }
++ this.stats = new BotStatsCounter(server);
++ this.container = new BotInventoryContainer(this);
++ this.tracingRange = world.spigotConfig.playerTrackingRange * world.spigotConfig.playerTrackingRange;
+
-+ public void renderAll() {
-+ MinecraftServer.getServer().getPlayerList().getPlayers().forEach(
-+ player -> {
-+ this.sendPlayerInfo(player);
-+ this.sendFakeData(player.connection, false);
-+ }
-+ );
++ this.notSleepTicks = 0;
++ this.fauxSleeping = LeavesConfig.fakeplayerSkipSleep;
+ }
+
+ public void sendPlayerInfo(ServerPlayer player) {
@@ -1789,7 +2253,7 @@ index 0000000000000000000000000000000000000000..462d58ad184ebe6bd6f161bff1481745
+ }
+
+ public boolean needSendFakeData(ServerPlayer player) {
-+ return alwaysSendData && (player.level() == this.level() && player.position().distanceToSqr(this.position()) > this.tracingRange);
++ return this.getConfigValue(Configs.ALWAYS_SEND_DATA) && (player.level() == this.level() && player.position().distanceToSqr(this.position()) > this.tracingRange);
+ }
+
+ public void sendFakeDataIfNeed(ServerPlayer player, boolean login) {
@@ -1808,130 +2272,72 @@ index 0000000000000000000000000000000000000000..462d58ad184ebe6bd6f161bff1481745
+
+ playerConnection.send(this.getAddEntityPacket(entityTracker.serverEntity));
+ if (login) {
-+ Bukkit.getScheduler().runTaskLater(CraftScheduler.MINECRAFT, () -> {
-+ playerConnection.send(new ClientboundRotateHeadPacket(this, (byte) ((getYRot() * 256f) / 360f)));
-+ }, 10);
++ Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> playerConnection.send(new ClientboundRotateHeadPacket(this, (byte) ((getYRot() * 256f) / 360f))), 10);
+ } else {
+ playerConnection.send(new ClientboundRotateHeadPacket(this, (byte) ((getYRot() * 256f) / 360f)));
+ }
+ }
+
-+ private void sendPacket(Packet> packet) {
-+ MinecraftServer.getServer().getPlayerList().getPlayers().forEach(
-+ player -> player.connection.send(packet)
++ public void renderAll() {
++ this.server.getPlayerList().getPlayers().forEach(
++ player -> {
++ this.sendPlayerInfo(player);
++ this.sendFakeDataIfNeed(player, false);
++ }
+ );
+ }
+
-+ // die check start
-+ @Override
-+ public void die(@NotNull DamageSource damageSource) {
-+ if (removeOnDeath) {
-+ onRemove(BotRemoveEvent.RemoveReason.DEATH);
-+ }
++ private void sendPacket(Packet> packet) {
++ this.server.getPlayerList().getPlayers().forEach(player -> player.connection.send(packet));
+ }
+
-+ public void onRemove(BotRemoveEvent.RemoveReason reason) {
-+ onRemove(reason, null);
-+ }
++ @Override
++ public void die(@NotNull DamageSource damageSource) {
++ boolean flag = this.level().getGameRules().getBoolean(GameRules.RULE_SHOWDEATHMESSAGES);
++ Component defaultMessage = this.getCombatTracker().getDeathMessage();
+
-+ public void onRemove(BotRemoveEvent.RemoveReason reason, @Nullable CommandSender remover) {
-+ BotRemoveEvent event = new BotRemoveEvent(this.getBukkitPlayer(), reason, remover, PaperAdventure.asAdventure(Component.translatable("multiplayer.player.left", this.getDisplayName())).style(Style.style(NamedTextColor.YELLOW)));
++ BotDeathEvent event = new BotDeathEvent(this.getBukkitEntity(), PaperAdventure.asAdventure(defaultMessage), flag);
+ this.server.server.getPluginManager().callEvent(event);
+
+ if (event.isCancelled()) {
++ if (this.getHealth() <= 0) {
++ this.setHealth(0.1f);
++ }
+ return;
+ }
+
-+ this.dropAll();
-+ if (this.removeTaskId != -1) {
-+ Bukkit.getScheduler().cancelTask(this.removeTaskId);
-+ this.removeTaskId = -1;
-+ }
-+ bots.remove(this);
-+ this.server.getPlayerList().removeBot(this);
-+ this.remove(RemovalReason.DISCARDED);
-+ this.setDead();
-+ this.removeTab();
++ this.gameEvent(GameEvent.ENTITY_DIE);
+
-+ net.kyori.adventure.text.Component removeMessage = event.removeMessage();
-+ if (removeMessage != null && !removeMessage.equals(net.kyori.adventure.text.Component.empty())) {
-+ server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(removeMessage), false);
++ net.kyori.adventure.text.Component deathMessage = event.deathMessage();
++ if (event.isSendDeathMessage() && deathMessage != null && !deathMessage.equals(net.kyori.adventure.text.Component.empty())) {
++ this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(deathMessage), false);
+ }
-+ }
+
-+ private void removeTab() {
-+ sendPacket(new ClientboundPlayerInfoRemovePacket(List.of(this.getUUID())));
++ this.server.getBotList().removeBot(this, BotRemoveEvent.RemoveReason.DEATH, null, false);
+ }
+
-+ private void setDead() {
-+ sendPacket(new ClientboundRemoveEntitiesPacket(getId()));
-+ this.dead = true;
-+ this.inventoryMenu.removed(this);
-+ this.containerMenu.removed(this);
++ public void removeTab() {
++ this.sendPacket(new ClientboundPlayerInfoRemovePacket(List.of(this.getUUID())));
+ }
+
-+ // die check end
-+
+ @Nullable
+ @Override
+ public Entity changeDimension(@NotNull DimensionTransition teleportTarget) {
+ 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()) {
++ if (!this.isAlive()) {
+ return;
+ }
++ super.tick();
+
-+ if (spawnPhantom) {
++ if (this.getConfigValue(Configs.SPAWN_PHANTOM)) {
+ notSleepTicks++;
+ }
+
-+ if (fireTicks > 0) {
-+ --fireTicks;
-+ }
-+ if (jumpTicks > 0) {
-+ --jumpTicks;
-+ }
-+ if (noFallTicks > 0) {
-+ --noFallTicks;
-+ }
-+ if (takeXpDelay > 0) {
-+ --takeXpDelay;
-+ }
-+
-+ this.updateLocation();
-+ this.updatePlayerPose();
-+ this.serverLevel().getChunkSource().move(this);
-+
-+ if (server.getTickCount() % 20 == 0) {
++ if (LeavesConfig.fakeplayerRegenAmount > 0.0 && server.getTickCount() % 20 == 0) {
+ float health = getHealth();
+ float maxHealth = getMaxHealth();
+ float regenAmount = (float) (LeavesConfig.fakeplayerRegenAmount * 20);
@@ -1945,65 +2351,6 @@ index 0000000000000000000000000000000000000000..462d58ad184ebe6bd6f161bff1481745
+
+ 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);
-+ }
-+ }
-+ }
-+
-+ public Entity getTargetEntity(int maxDistance, Predicate super Entity> predicate) {
-+ List entities = this.level().getEntities((Entity) null, this.getBoundingBox(), (e -> e != this && (predicate == null || predicate.test(e))));
-+ if (!entities.isEmpty()) {
-+ return entities.getFirst();
-+ } else {
-+ EntityHitResult result = this.getBukkitEntity().rayTraceEntity(maxDistance, false);
-+ if (result != null && (predicate == null || predicate.test(result.getEntity()))) {
-+ return result.getEntity();
-+ }
-+ }
-+ return null;
-+ }
-+
-+ private void touch(@NotNull Entity entity) {
-+ entity.playerTouch(this);
+ }
+
+ @Override
@@ -2013,7 +2360,7 @@ index 0000000000000000000000000000000000000000..462d58ad184ebe6bd6f161bff1481745
+ }
+
+ public void updateItemInHand(InteractionHand hand) {
-+ net.minecraft.world.item.ItemStack item = getItemInHand(hand);
++ ItemStack item = this.getItemInHand(hand);
+
+ if (!item.isEmpty()) {
+ BotUtil.replenishment(item, getInventory().items);
@@ -2021,15 +2368,54 @@ index 0000000000000000000000000000000000000000..462d58ad184ebe6bd6f161bff1481745
+ BotUtil.replaceTool(hand == InteractionHand.MAIN_HAND ? EquipmentSlot.MAINHAND : EquipmentSlot.OFFHAND, this);
+ }
+ }
-+ detectEquipmentUpdatesPublic();
++ this.detectEquipmentUpdatesPublic();
+ }
+
-+ public long getEatStartTime() {
-+ return eatStartTime;
++ @Override
++ public @NotNull InteractionResult interact(@NotNull Player player, @NotNull InteractionHand hand) {
++ if (LeavesConfig.openFakeplayerInventory) {
++ if (player instanceof ServerPlayer player1 && player.getMainHandItem().isEmpty()) {
++ BotInventoryOpenEvent event = new BotInventoryOpenEvent(this.getBukkitEntity(), player1.getBukkitEntity());
++ this.server.server.getPluginManager().callEvent(event);
++ if (!event.isCancelled()) {
++ Component menuName = this.getDisplayName() != null ? this.getDisplayName() : Component.literal(this.createState.name());
++ player.openMenu(new SimpleMenuProvider((i, inventory, p) -> ChestMenu.sixRows(i, inventory, this.container), menuName));
++ return InteractionResult.SUCCESS;
++ }
++ }
++ }
++ return super.interact(player, hand);
+ }
+
+ @Override
+ public void checkFallDamage(double heightDifference, boolean onGround, @NotNull BlockState state, @NotNull BlockPos landedPosition) {
++ if (onGround && this.fallDistance > 0.0F) {
++ this.onChangedBlock(this.serverLevel(), landedPosition);
++ double d1 = this.getAttributeValue(Attributes.SAFE_FALL_DISTANCE);
++
++ if ((double) this.fallDistance > d1 && !state.isAir()) {
++ double d2 = this.getX();
++ double d3 = this.getY();
++ double d4 = this.getZ();
++ BlockPos blockposition = this.blockPosition();
++
++ if (landedPosition.getX() != blockposition.getX() || landedPosition.getZ() != blockposition.getZ()) {
++ double d5 = d2 - (double) landedPosition.getX() - 0.5D;
++ double d6 = d4 - (double) landedPosition.getZ() - 0.5D;
++ double d7 = Math.max(Math.abs(d5), Math.abs(d6));
++
++ d2 = (double) landedPosition.getX() + 0.5D + d5 / d7 * 0.5D;
++ d4 = (double) landedPosition.getZ() + 0.5D + d6 / d7 * 0.5D;
++ }
++
++ float f = (float) Mth.ceil((double) this.fallDistance - d1);
++ double d8 = Math.min(0.2F + f / 15.0F, 2.5D);
++ int i = (int) (150.0D * d8);
++
++ this.serverLevel().sendParticles(this, new BlockParticleOption(ParticleTypes.BLOCK, state), d2, d3, d4, i, 0.0D, 0.0D, 0.0D, 0.15000000596046448D, false);
++ }
++ }
++
+ if (onGround) {
+ if (this.fallDistance > 0.0F) {
+ state.getBlock().fallOn(this.level(), state, landedPosition, this, this.fallDistance);
@@ -2046,311 +2432,263 @@ index 0000000000000000000000000000000000000000..462d58ad184ebe6bd6f161bff1481745
+
+ @Override
+ public void doTick() {
-+ if (this.hurtTime > 0) {
-+ this.hurtTime -= 1;
-+ }
-+
-+ baseTick();
++ this.absMoveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot());
+
-+ this.lerpSteps = (int) this.zza;
-+ this.animStep = this.run;
-+ this.yRotO = this.getYRot();
-+ this.xRotO = this.getXRot();
-+ }
++ if (this.takeXpDelay > 0) {
++ --this.takeXpDelay;
++ }
+
-+ public Location getLocation() {
-+ return getBukkitPlayer().getLocation();
-+ }
++ if (this.isSleeping()) {
++ ++this.sleepCounter;
++ if (this.sleepCounter > 100) {
++ this.sleepCounter = 100;
++ this.notSleepTicks = 0;
++ }
+
-+ @Override
-+ public void knockback(double strength, double x, double z, @Nullable Entity attacker, @NotNull EntityKnockbackEvent.Cause cause) {
-+ 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);
++ if (!this.level().isClientSide && this.level().isDay()) {
++ this.stopSleepInBed(false, true);
++ }
++ } else if (this.sleepCounter > 0) {
++ ++this.sleepCounter;
++ if (this.sleepCounter >= 110) {
++ this.sleepCounter = 0;
++ }
+ }
-+ }
+
-+ private void updateLocation() {
-+ this.velocity = new Vec3(this.xxa, this.yya, this.zza);
++ this.updateIsUnderwater();
+
-+ if (waterSwim && isInWater()) {
-+ this.addDeltaMovement(new Vec3(0, 0.05, 0));
-+ }
+ this.addDeltaMovement(knockback);
-+ knockback = Vec3.ZERO;
++ this.knockback = Vec3.ZERO;
+
-+ this.travel(this.velocity);
-+ }
++ this.server.tell(this.server.wrapRunnable(this::runAction));
+
-+ public void faceLocation(@NotNull Location loc) {
-+ look(loc.toVector().subtract(getLocation().toVector()), false);
-+ }
++ this.livingEntityTick();
+
-+ public void look(Vector dir, boolean keepYaw) {
-+ float yaw, pitch;
++ this.foodData.tick(this);
+
-+ if (keepYaw) {
-+ yaw = this.getYHeadRot();
-+ pitch = MathUtils.fetchPitch(dir);
-+ } else {
-+ float[] vals = MathUtils.fetchYawPitch(dir);
-+ yaw = vals[0];
-+ pitch = vals[1];
++ ++this.attackStrengthTicker;
++ ItemStack itemstack = this.getMainHandItem();
++ if (!ItemStack.matches(this.lastItemInMainHand, itemstack)) {
++ if (!ItemStack.isSameItem(this.lastItemInMainHand, itemstack)) {
++ this.resetAttackStrengthTicker();
++ }
+
-+ sendPacket(new ClientboundRotateHeadPacket(this, (byte) (yaw * 256 / 360f)));
++ this.lastItemInMainHand = itemstack.copy();
+ }
+
-+ this.setRot(yaw, pitch);
-+ }
-+
-+ @Override
-+ public void setRot(float yaw, float pitch) {
-+ this.getBukkitEntity().setRotation(yaw, pitch);
-+ }
++ this.getCooldowns().tick();
++ this.updatePlayerPose();
+
-+ public void attack(@NotNull Entity target) {
-+ super.attack(target);
-+ swing(InteractionHand.MAIN_HAND);
++ if (this.hurtTime > 0) {
++ this.hurtTime -= 1;
++ }
+ }
+
+ @Override
-+ 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 (!LeavesConfig.fakeplayerUseAction) {
-+ return;
-+ }
-+ if (action instanceof StopAction) {
-+ this.actions.clear();
++ public void knockback(double strength, double x, double z, @Nullable Entity attacker, @NotNull EntityKnockbackEvent.Cause cause) {
++ strength *= 1.0D - this.getAttributeValue(Attributes.KNOCKBACK_RESISTANCE);
++ if (strength > 0.0D) {
++ Vec3 vec3d = this.getDeltaMovement();
++ Vec3 vec3d1 = (new Vec3(x, 0.0D, z)).normalize().scale(strength);
++ this.hasImpulse = true;
++ this.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).subtract(vec3d);
+ }
-+ 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;
++ public void setRot(float yaw, float pitch) {
++ this.getBukkitEntity().setRotation(yaw, pitch);
+ }
+
+ @Override
-+ public @NotNull InteractionResult interact(@NotNull Player player, @NotNull InteractionHand hand) {
-+ if (LeavesConfig.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()) {
-+ Component menuName = this.getDisplayName();
-+ player.openMenu(new SimpleMenuProvider((i, inventory, p) -> ChestMenu.sixRows(i, inventory, container), menuName != null ? menuName : Component.literal(this.createState.name)));
-+ 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 void attack(@NotNull Entity target) {
++ super.attack(target);
++ this.swing(InteractionHand.MAIN_HAND);
+ }
+
-+ public static void saveOrRemoveAllBot() {
-+ if (LeavesConfig.fakeplayerSupport && LeavesConfig.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 {
-+ if (!file.createNewFile()) {
-+ throw new IOException("Failed to create fakeplayer file: " + file);
-+ }
-+ } catch (IOException e) {
-+ LeavesLogger.LOGGER.severe("Failed to save fakeplayer", e);
-+ return;
-+ }
-+ }
-+ try (BufferedWriter bfw = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) {
-+ bfw.write(new Gson().toJson(fakePlayerList));
-+ } catch (IOException e) {
-+ LeavesLogger.LOGGER.severe("Failed to save fakeplayer", e);
++ @Override
++ public void addAdditionalSaveData(@NotNull CompoundTag nbt) {
++ super.addAdditionalSaveData(nbt);
++ nbt.putBoolean("isShiftKeyDown", this.isShiftKeyDown());
++
++ CompoundTag createNbt = new CompoundTag();
++ createNbt.putString("realName", this.createState.realName());
++ createNbt.putString("name", this.createState.name());
++
++ createNbt.putString("skinName", this.createState.skinName());
++ if (this.createState.skin() != null) {
++ ListTag skin = new ListTag();
++ for (String s : this.createState.skin()) {
++ skin.add(StringTag.valueOf(s));
+ }
-+ } else {
-+ removeAllBot(BotRemoveEvent.RemoveReason.INTERNAL);
++ createNbt.put("skin", skin);
+ }
-+ }
+
-+ public static void loadAllBot() {
-+ if (LeavesConfig.fakeplayerSupport && LeavesConfig.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) {
-+ LeavesLogger.LOGGER.severe("Failed to load fakeplayer", e);
-+ }
-+ for (Map.Entry entry : fakePlayerList.entrySet()) {
-+ BotUtil.loadBot(entry);
++ nbt.put("createStatus", createNbt);
++
++ if (!this.actions.isEmpty()) {
++ ListTag actionNbt = new ListTag();
++ for (BotAction> action : this.actions) {
++ actionNbt.add(action.save(new CompoundTag()));
+ }
-+ if (!file.delete()) {
-+ LeavesLogger.LOGGER.warning("Failed to delete " + file);
++ nbt.put("actions", actionNbt);
++ }
++
++ if (!this.configs.isEmpty()) {
++ ListTag configNbt = new ListTag();
++ for (BotConfig> config : this.configs.values()) {
++ configNbt.add(config.save(new CompoundTag()));
+ }
++ nbt.put("configs", configNbt);
+ }
+ }
+
-+ public static void removeAllBot(BotRemoveEvent.RemoveReason reason) {
-+ Iterator iterator = bots.iterator();
-+ while (iterator.hasNext()) {
-+ ServerBot bot = iterator.next();
-+ bot.onRemove(reason);
++ @Override
++ public void readAdditionalSaveData(@NotNull CompoundTag nbt) {
++ super.readAdditionalSaveData(nbt);
++ this.setShiftKeyDown(nbt.getBoolean("isShiftKeyDown"));
++
++ CompoundTag createNbt = nbt.getCompound("createStatus");
++ BotCreateState.Builder createBuilder = BotCreateState.builder(createNbt.getString("realName"), null).name(createNbt.getString("name"));
++
++ String[] skin = null;
++ if (createNbt.contains("skin")) {
++ ListTag skinTag = createNbt.getList("skin", 8);
++ skin = new String[skinTag.size()];
++ for (int i = 0; i < skinTag.size(); i++) {
++ skin[i] = skinTag.getString(i);
++ }
+ }
-+ }
+
-+ public static List getBots() {
-+ return bots;
-+ }
++ createBuilder.skinName(createNbt.getString("skinName")).skin(skin);
++ createBuilder.createReason(BotCreateEvent.CreateReason.INTERNAL).creator(null);
+
-+ public static class CustomGameProfile extends GameProfile {
++ this.createState = createBuilder.build();
++ this.gameProfile = new BotList.CustomGameProfile(this.getUUID(), this.createState.name(), this.createState.skin());
+
-+ public CustomGameProfile(UUID uuid, String name, String[] skin) {
-+ super(uuid, name);
-+ setSkin(skin);
++
++ if (nbt.contains("actions")) {
++ ListTag actionNbt = nbt.getList("actions", 10);
++ for (int i = 0; i < actionNbt.size(); i++) {
++ CompoundTag actionTag = actionNbt.getCompound(i);
++ BotAction> action = Actions.getForName(actionTag.getString("actionName"));
++ if (action != null) {
++ BotAction> newAction = action.create();
++ newAction.load(actionTag);
++ this.actions.add(newAction);
++ }
++ }
+ }
+
-+ public void setSkin(String[] skin) {
-+ if (skin != null) {
-+ getProperties().put("textures", new Property("textures", skin[0], skin[1]));
++ if (nbt.contains("configs")) {
++ ListTag configNbt = nbt.getList("configs", 10);
++ for (int i = 0; i < configNbt.size(); i++) {
++ CompoundTag configTag = configNbt.getCompound(i);
++ Configs> configKey = Configs.getConfig(configTag.getString("configName"));
++ if (configKey != null) {
++ this.configs.get(configKey).load(configTag);
++ }
+ }
+ }
+ }
+
-+ public static class BotCreateState {
-+
-+ private final String realName;
-+ private final String name;
-+
-+ public Location loc;
++ public void faceLocation(@NotNull Location loc) {
++ this.look(loc.toVector().subtract(getLocation().toVector()), false);
++ }
+
-+ public String[] skin;
-+ public String skinName;
++ public void look(Vector dir, boolean keepYaw) {
++ float yaw, pitch;
+
-+ public BotCreateEvent.CreateReason createReason;
-+ public CommandSender creator;
++ if (keepYaw) {
++ yaw = this.getYHeadRot();
++ pitch = MathUtils.fetchPitch(dir);
++ } else {
++ float[] vals = MathUtils.fetchYawPitch(dir);
++ yaw = vals[0];
++ pitch = vals[1];
+
-+ public BotCreateState(Location loc, String realName, String skinName, BotCreateEvent.CreateReason createReason, CommandSender creator) {
-+ this(loc, LeavesConfig.fakeplayerPrefix + realName + LeavesConfig.fakeplayerSuffix, realName, skinName, null, createReason, creator);
++ this.sendPacket(new ClientboundRotateHeadPacket(this, (byte) (yaw * 256 / 360f)));
+ }
+
-+ public BotCreateState(Location loc, String name, String realName, String skinName, String[] skin, BotCreateEvent.CreateReason createReason, CommandSender creator) {
-+ this.loc = loc;
-+ this.skinName = skinName;
-+ this.skin = skin;
-+ this.realName = realName;
-+ this.name = name;
-+ this.createReason = createReason;
-+ this.creator = creator;
-+ }
++ this.setRot(yaw, pitch);
++ }
+
-+ @Nullable
-+ public Bot createNow(Consumer consumer) {
-+ ServerBot bot = createBot(this);
-+ if (bot != null && consumer != null) {
-+ consumer.accept(bot);
++ public Location getLocation() {
++ return this.getBukkitEntity().getLocation();
++ }
++
++ public Entity getTargetEntity(int maxDistance, Predicate super Entity> predicate) {
++ List entities = this.level().getEntities((Entity) null, this.getBoundingBox(), (e -> e != this && (predicate == null || predicate.test(e))));
++ if (!entities.isEmpty()) {
++ return entities.getFirst();
++ } else {
++ EntityHitResult result = this.getBukkitEntity().rayTraceEntity(maxDistance, false);
++ if (result != null && (predicate == null || predicate.test(result.getEntity()))) {
++ return result.getEntity();
+ }
-+ return bot != null ? bot.getBukkitEntity() : null;
+ }
++ return null;
++ }
+
-+ public void create(Consumer consumer) {
-+ Bukkit.getScheduler().runTaskAsynchronously(CraftScheduler.MINECRAFT, () -> {
-+ if (skin == null && skinName != null) {
-+ this.skin = MojangAPI.getSkin(skinName);
-+ }
++ public void dropAll() {
++ this.getInventory().dropAll();
++ this.detectEquipmentUpdatesPublic();
++ }
+
-+ Bukkit.getScheduler().runTask(CraftScheduler.MINECRAFT, () -> {
-+ ServerBot bot = createBot(this);
-+ if (bot != null && consumer != null) {
-+ consumer.accept(bot);
-+ }
-+ });
-+ });
++ private void runAction() {
++ if (LeavesConfig.fakeplayerUseAction) {
++ this.actions.forEach(action -> action.tryTick(this));
++ this.actions.removeIf(BotAction::isCancelled);
+ }
++ }
+
-+ public String getName() {
-+ return name;
++ public boolean addBotAction(BotAction> action) {
++ if (!LeavesConfig.fakeplayerUseAction) {
++ return false;
+ }
+
-+ public String getRealName() {
-+ return realName;
++ if (!new BotActionScheduleEvent(this.getBukkitEntity(), action.getName(), action.getUUID()).callEvent()) {
++ return false;
+ }
++
++ action.init();
++ this.actions.add(action);
++ return true;
++ }
++
++ public List> getBotActions() {
++ return actions;
++ }
++
++ @Override
++ public @NotNull ServerStatsCounter getStats() {
++ return stats;
++ }
++
++ @SuppressWarnings("unchecked")
++ public BotConfig getConfig(Configs config) {
++ return (BotConfig) Objects.requireNonNull(this.configs.get(config));
++ }
++
++ public E getConfigValue(Configs config) {
++ return this.getConfig(config).getValue();
++ }
++
++ @Override
++ @NotNull
++ public CraftBot getBukkitEntity() {
++ return (CraftBot) super.getBukkitEntity();
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java b/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java
new file mode 100644
-index 0000000000000000000000000000000000000000..d626ac47af400d01993c358fa5a93671dce9abe9
+index 0000000000000000000000000000000000000000..bc1e29f6080c4783940848456620be8c06c32cce
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java
-@@ -0,0 +1,139 @@
+@@ -0,0 +1,138 @@
+package org.leavesmc.leaves.bot;
+
+import net.kyori.adventure.text.Component;
-+import net.minecraft.advancements.CriteriaTriggers;
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
@@ -2486,16 +2824,108 @@ index 0000000000000000000000000000000000000000..d626ac47af400d01993c358fa5a93671
+ public void setLevel(@NotNull ServerLevel world) {
+ }
+}
+diff --git a/src/main/java/org/leavesmc/leaves/bot/ServerBotPacketListenerImpl.java b/src/main/java/org/leavesmc/leaves/bot/ServerBotPacketListenerImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c62f9258e4114ff686642b7f472d0e14151f37d5
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/bot/ServerBotPacketListenerImpl.java
+@@ -0,0 +1,85 @@
++package org.leavesmc.leaves.bot;
++
++import net.minecraft.network.Connection;
++import net.minecraft.network.DisconnectionDetails;
++import net.minecraft.network.PacketSendListener;
++import net.minecraft.network.protocol.Packet;
++import net.minecraft.network.protocol.PacketFlow;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.network.CommonListenerCookie;
++import net.minecraft.server.network.ServerGamePacketListenerImpl;
++import org.bukkit.event.player.PlayerKickEvent;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++public class ServerBotPacketListenerImpl extends ServerGamePacketListenerImpl {
++
++ public ServerBotPacketListenerImpl(MinecraftServer server, ServerBot bot) {
++ super(server, BotConnection.INSTANCE, bot, CommonListenerCookie.createInitial(bot.gameProfile, false));
++ }
++
++ @Override
++ public void sendPacket(@NotNull Packet> packet) {
++ }
++
++ @Override
++ public void send(@NotNull Packet> packet) {
++ }
++
++ @Override
++ public void send(@NotNull Packet> packet, @Nullable PacketSendListener callbacks) {
++ }
++
++ @Override
++ public void disconnect(@NotNull DisconnectionDetails disconnectionInfo, PlayerKickEvent.@NotNull Cause cause) {
++ }
++
++ @Override
++ public boolean isAcceptingMessages() {
++ return true;
++ }
++
++ @Override
++ public void tick() {
++ }
++
++ public static class BotConnection extends Connection {
++
++ private static final BotConnection INSTANCE = new BotConnection();
++
++ public BotConnection() {
++ super(PacketFlow.SERVERBOUND);
++ }
++
++ @Override
++ public void tick() {
++ }
++
++ @Override
++ public boolean isConnected() {
++ return true;
++ }
++
++ @Override
++ public boolean isConnecting() {
++ return false;
++ }
++
++ @Override
++ public boolean isMemoryConnection() {
++ return false;
++ }
++
++ @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) {
++ }
++ }
++}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/Actions.java b/src/main/java/org/leavesmc/leaves/bot/agent/Actions.java
new file mode 100644
-index 0000000000000000000000000000000000000000..15cbf363f587a27d55f4bc7ec897787158a7d534
+index 0000000000000000000000000000000000000000..a37513e1ba8443c702ab0c01fbe5e052e5f0f2ab
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/Actions.java
-@@ -0,0 +1,66 @@
+@@ -0,0 +1,67 @@
+package org.leavesmc.leaves.bot.agent;
+
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
+import org.leavesmc.leaves.bot.agent.actions.*;
+
+import java.util.Collection;
@@ -2505,7 +2935,7 @@ index 0000000000000000000000000000000000000000..15cbf363f587a27d55f4bc7ec8977871
+
+public class Actions {
+
-+ private static final Map actions = new HashMap<>();
++ private static final Map> actions = new HashMap<>();
+
+ public static void registerAll() {
+ register(new AttackAction());
@@ -2514,7 +2944,6 @@ index 0000000000000000000000000000000000000000..15cbf363f587a27d55f4bc7ec8977871
+ register(new JumpAction());
+ register(new RotateAction());
+ register(new SneakAction());
-+ register(new StopAction());
+ register(new UseItemAction());
+ register(new UseItemOnAction());
+ register(new UseItemToAction());
@@ -2527,7 +2956,7 @@ index 0000000000000000000000000000000000000000..15cbf363f587a27d55f4bc7ec8977871
+ register(new RotationAction());
+ }
+
-+ public static boolean register(@NotNull BotAction action) {
++ public static boolean register(@NotNull BotAction> action) {
+ if (!actions.containsKey(action.getName())) {
+ actions.put(action.getName(), action);
+ return true;
@@ -2545,7 +2974,7 @@ index 0000000000000000000000000000000000000000..15cbf363f587a27d55f4bc7ec8977871
+
+ @NotNull
+ @Contract(pure = true)
-+ public static Collection getAll() {
++ public static Collection> getAll() {
+ return actions.values();
+ }
+
@@ -2554,139 +2983,345 @@ index 0000000000000000000000000000000000000000..15cbf363f587a27d55f4bc7ec8977871
+ return actions.keySet();
+ }
+
-+ public static BotAction getForName(String name) {
++ @Nullable
++ public static BotAction> getForName(String name) {
+ return actions.get(name);
+ }
+}
-diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/BotAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/BotAction.java
+diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/BotAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/BotAction.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3ad5484f26e3dc7fb45c5d2ee0687604e6974404
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/bot/agent/BotAction.java
+@@ -0,0 +1,163 @@
++package org.leavesmc.leaves.bot.agent;
++
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.server.level.ServerPlayer;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++import org.leavesmc.leaves.bot.ServerBot;
++import org.leavesmc.leaves.command.CommandArgument;
++import org.leavesmc.leaves.command.CommandArgumentResult;
++import org.leavesmc.leaves.event.bot.BotActionExecuteEvent;
++import org.leavesmc.leaves.event.bot.BotActionStopEvent;
++
++import java.util.List;
++import java.util.UUID;
++import java.util.function.Supplier;
++
++public abstract class BotAction> {
++
++ private final String name;
++ private final CommandArgument argument;
++ private final Supplier creator;
++
++ private boolean cancel;
++ private int tickDelay;
++ private int number;
++ private UUID uuid;
++
++ private int needWaitTick;
++ private int canDoNumber;
++
++ public BotAction(String name, CommandArgument argument, Supplier creator) {
++ this.name = name;
++ this.argument = argument;
++ this.uuid = UUID.randomUUID();
++ this.creator = creator;
++
++ this.cancel = false;
++ this.tickDelay = 20;
++ this.number = -1;
++ }
++
++ public void init() {
++ this.needWaitTick = 0;
++ this.canDoNumber = this.getNumber();
++ this.setCancelled(false);
++ }
++
++ public String getName() {
++ return this.name;
++ }
++
++ public UUID getUUID() {
++ return uuid;
++ }
++
++ public int getTickDelay() {
++ return this.tickDelay;
++ }
++
++ @SuppressWarnings("unchecked")
++ public E setTickDelay(int tickDelay) {
++ this.tickDelay = Math.max(0, tickDelay);
++ return (E) this;
++ }
++
++ public int getNumber() {
++ return this.number;
++ }
++
++ @SuppressWarnings("unchecked")
++ public E setNumber(int number) {
++ this.number = Math.max(-1, number);
++ return (E) this;
++ }
++
++ public int getCanDoNumber() {
++ return this.canDoNumber;
++ }
++
++ public boolean isCancelled() {
++ return cancel;
++ }
++
++ public void setCancelled(boolean cancel) {
++ this.cancel = cancel;
++ }
++
++ public void stop(@NotNull ServerBot bot, BotActionStopEvent.Reason reason) {
++ new BotActionStopEvent(bot.getBukkitEntity(), this.name, this.uuid, reason).callEvent();
++ this.setCancelled(true);
++ }
++
++ public CommandArgument getArgument() {
++ return this.argument;
++ }
++
++ @SuppressWarnings("unchecked")
++ public E setTabComplete(int index, List list) {
++ this.argument.setTabComplete(index, list);
++ return (E) this;
++ }
++
++ public void tryTick(ServerBot bot) {
++ if (this.canDoNumber == 0) {
++ this.stop(bot, BotActionStopEvent.Reason.DONE);
++ return;
++ }
++
++ if (this.needWaitTick <= 0) {
++ BotActionExecuteEvent event = new BotActionExecuteEvent(bot.getBukkitEntity(), name, uuid);
++
++ event.callEvent();
++ if (event.getResult() == BotActionExecuteEvent.Result.SOFT_CANCEL) {
++ this.needWaitTick = this.getTickDelay();
++ return;
++ } else if (event.getResult() == BotActionExecuteEvent.Result.HARD_CANCEL) {
++ if (this.canDoNumber > 0) {
++ this.canDoNumber--;
++ }
++ this.needWaitTick = this.getTickDelay();
++ return;
++ }
++
++ if (this.doTick(bot)) {
++ if (this.canDoNumber > 0) {
++ this.canDoNumber--;
++ }
++ this.needWaitTick = this.getTickDelay();
++ }
++ } else {
++ this.needWaitTick--;
++ }
++ }
++
++ @NotNull
++ public E create() {
++ return this.creator.get();
++ }
++
++ @NotNull
++ public CompoundTag save(@NotNull CompoundTag nbt) {
++ if (!this.cancel) {
++ nbt.putString("actionName", this.name);
++ nbt.putUUID("actionUUID", this.uuid);
++
++ nbt.putInt("canDoNumber", this.canDoNumber);
++ nbt.putInt("needWaitTick", this.needWaitTick);
++ nbt.putInt("tickDelay", this.tickDelay);
++ }
++ return nbt;
++ }
++
++ public void load(@NotNull CompoundTag nbt) {
++ this.tickDelay = nbt.getInt("tickDelay");
++ this.needWaitTick = nbt.getInt("needWaitTick");
++ this.canDoNumber = nbt.getInt("canDoNumber");
++ this.uuid = nbt.getUUID("actionUUID");
++ }
++
++ public abstract void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result);
++
++ public abstract boolean doTick(@NotNull ServerBot bot);
++}
+diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/BotConfig.java b/src/main/java/org/leavesmc/leaves/bot/agent/BotConfig.java
new file mode 100644
-index 0000000000000000000000000000000000000000..9abcb8852ac031abaa991881a7cd6b33bc523b26
+index 0000000000000000000000000000000000000000..c889a2409d8b9f5979a10b61c98638054bd8f5bc
--- /dev/null
-+++ b/src/main/java/org/leavesmc/leaves/bot/agent/BotAction.java
-@@ -0,0 +1,93 @@
++++ b/src/main/java/org/leavesmc/leaves/bot/agent/BotConfig.java
+@@ -0,0 +1,62 @@
+package org.leavesmc.leaves.bot.agent;
+
-+import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.nbt.CompoundTag;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.bot.ServerBot;
+import org.leavesmc.leaves.command.CommandArgument;
+import org.leavesmc.leaves.command.CommandArgumentResult;
+
+import java.util.List;
++import java.util.function.Supplier;
+
-+public abstract class BotAction {
++public abstract class BotConfig {
+
+ private final String name;
+ private final CommandArgument argument;
++ private final Supplier> creator;
++ protected ServerBot bot;
+
-+ private boolean cancel;
-+ private int tickDelay;
-+ private int number;
-+
-+ private int needWaitTick;
-+ private int canDoNumber;
-+
-+ public BotAction(String name, CommandArgument argument) {
++ public BotConfig(String name, CommandArgument argument, Supplier> creator) {
+ this.name = name;
+ this.argument = argument;
-+
-+ this.cancel = false;
-+ this.tickDelay = 20;
-+ this.number = -1;
++ this.creator = creator;
+ }
+
-+ public String getName() {
-+ return name;
++ public BotConfig setBot(ServerBot bot) {
++ this.bot = bot;
++ return this;
+ }
+
-+ public int getTickDelay() {
-+ return tickDelay;
-+ }
++ public abstract E getValue();
+
-+ public int getNumber() {
-+ return number;
-+ }
++ public abstract void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException;
+
-+ public boolean isCancel() {
-+ return cancel;
++ public List getMessage() {
++ return List.of(this.bot.getScoreboardName() + "'s " + this.getName() + ": " + this.getValue());
+ }
+
-+ public BotAction setTickDelay(int tickDelay) {
-+ this.tickDelay = Math.max(0, tickDelay);
-+ return this;
++ public List getChangeMessage() {
++ return List.of(this.bot.getScoreboardName() + "'s " + this.getName() + " changed: " + this.getValue());
+ }
+
-+ public BotAction setTabComplete(int index, List list) {
-+ argument.setTabComplete(index, list);
-+ return this;
++ public String getName() {
++ return name;
+ }
+
-+ public BotAction setNumber(int number) {
-+ this.number = Math.max(-1, number);
-+ return this;
++ public CommandArgument getArgument() {
++ return argument;
+ }
+
-+ public void setCancel(boolean cancel) {
-+ this.cancel = cancel;
++ @NotNull
++ public BotConfig create(ServerBot bot) {
++ return this.creator.get().setBot(bot);
+ }
+
-+ public void init() {
-+ this.needWaitTick = 0;
-+ this.canDoNumber = this.getNumber();
-+ this.setCancel(false);
++ @NotNull
++ public CompoundTag save(@NotNull CompoundTag nbt) {
++ nbt.putString("configName", this.name);
++ return nbt;
+ }
+
-+ public void tryTick(ServerBot bot) {
-+ if (canDoNumber == 0) {
-+ this.setCancel(true);
-+ return;
-+ }
-+ if (needWaitTick-- <= 0) {
-+ if (this.doTick(bot)) {
-+ canDoNumber--;
-+ needWaitTick = this.getTickDelay();
-+ }
-+ }
++ public abstract void load(@NotNull CompoundTag nbt);
++}
+diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/Configs.java b/src/main/java/org/leavesmc/leaves/bot/agent/Configs.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d99f459b2e323474174cfd5d892cb7573a32ca12
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/bot/agent/Configs.java
+@@ -0,0 +1,44 @@
++package org.leavesmc.leaves.bot.agent;
++
++import org.jetbrains.annotations.Contract;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++import org.leavesmc.leaves.bot.agent.configs.*;
++
++import java.util.Collection;
++import java.util.HashMap;
++import java.util.Map;
++
++public class Configs {
++
++ private static final Map> configs = new HashMap<>();
++
++ public static final Configs SKIP_SLEEP = register(new SkipSleepConfig());
++ public static final Configs ALWAYS_SEND_DATA = register(new AlwaysSendDataConfig());
++ public static final Configs SPAWN_PHANTOM = register(new SpawnPhantomConfig());
++ public static final Configs SIMULATION_DISTANCE = register(new SimulationDistanceConfig());
++
++ public final BotConfig config;
++
++ private Configs(BotConfig config) {
++ this.config = config;
+ }
+
-+ public CommandArgument getArgument() {
-+ return argument;
++ @NotNull
++ @Contract(pure = true)
++ public static Collection> getConfigs() {
++ return configs.values();
+ }
+
-+ public abstract BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result);
++ @Nullable
++ public static Configs> getConfig(String name) {
++ return configs.get(name);
++ }
+
-+ public abstract boolean doTick(@NotNull ServerBot bot);
++ @NotNull
++ private static Configs register(BotConfig botConfig) {
++ Configs config = new Configs<>(botConfig);
++ configs.put(botConfig.getName(), config);
++ return config;
++ }
+}
-diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/AttackAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/AttackAction.java
+diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/AbstractTimerAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/AbstractTimerAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..1366939121876902669b264f2ffa05c039cbc4af
+index 0000000000000000000000000000000000000000..be55a3085a53542c08e7f0209883a4f3f72602e7
--- /dev/null
-+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/AttackAction.java
-@@ -0,0 +1,36 @@
++++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/AbstractTimerAction.java
+@@ -0,0 +1,25 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.world.entity.Entity;
+import org.jetbrains.annotations.NotNull;
-+import org.leavesmc.leaves.bot.ServerBot;
++import org.jetbrains.annotations.Nullable;
+import org.leavesmc.leaves.bot.agent.BotAction;
+import org.leavesmc.leaves.command.CommandArgument;
+import org.leavesmc.leaves.command.CommandArgumentResult;
+import org.leavesmc.leaves.command.CommandArgumentType;
+
+import java.util.List;
++import java.util.function.Supplier;
+
-+public class AttackAction extends BotAction {
++public abstract class AbstractTimerAction> extends BotAction {
+
-+ public AttackAction() {
-+ super("attack", new CommandArgument(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER));
-+ setTabComplete(0, List.of("[TickDelay]"));
-+ setTabComplete(1, List.of("[DoNumber]"));
++ public AbstractTimerAction(String name, Supplier creator) {
++ super(name, CommandArgument.of(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER), creator);
++ this.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));
++ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) {
++ this.setTickDelay(result.readInt(20)).setNumber(result.readInt(-1));
++ }
++}
+diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/AttackAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/AttackAction.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..03e9baf9b7c2da0fd1d7d9b0058b70daddedeeaa
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/AttackAction.java
+@@ -0,0 +1,22 @@
++package org.leavesmc.leaves.bot.agent.actions;
++
++import net.minecraft.world.entity.Entity;
++import org.jetbrains.annotations.NotNull;
++import org.leavesmc.leaves.bot.ServerBot;
++
++public class AttackAction extends AbstractTimerAction {
++
++ public AttackAction() {
++ super("attack", AttackAction::new);
+ }
+
+ @Override
@@ -2701,61 +3336,32 @@ index 0000000000000000000000000000000000000000..1366939121876902669b264f2ffa05c0
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..adcce8f8c39b0a1e445f5552ce74436053b604e7
+index 0000000000000000000000000000000000000000..bf7d20374cd7bff7cb7e09d209c6da5d297fe1f1
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java
-@@ -0,0 +1,105 @@
+@@ -0,0 +1,75 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import net.minecraft.core.BlockPos;
-+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.InteractionHand;
+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 org.leavesmc.leaves.bot.ServerBot;
-+import org.leavesmc.leaves.bot.agent.BotAction;
-+import org.leavesmc.leaves.command.CommandArgument;
-+import org.leavesmc.leaves.command.CommandArgumentResult;
-+import org.leavesmc.leaves.command.CommandArgumentType;
+
-+import java.util.List;
-+
-+public class BreakBlockAction extends BotAction {
++public class BreakBlockAction extends AbstractTimerAction {
+
+ 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;
++ super("break", BreakBlockAction::new);
+ }
+
-+ 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();
@@ -2795,7 +3401,6 @@ index 0000000000000000000000000000000000000000..adcce8f8c39b0a1e445f5552ce744360
+ lastPos = null;
+ destroyProgressTime = 0;
+ lastSentState = -1;
-+ nowDelay = delay;
+ }
+
+ private float incrementDestroyProgress(ServerBot bot, @NotNull BlockState state, BlockPos pos) {
@@ -2810,41 +3415,102 @@ index 0000000000000000000000000000000000000000..adcce8f8c39b0a1e445f5552ce744360
+ return f;
+ }
+}
+diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftBotAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftBotAction.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d96fc7b97ff826efe1bd36988f2d1a9ea04654cb
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftBotAction.java
+@@ -0,0 +1,54 @@
++package org.leavesmc.leaves.bot.agent.actions;
++
++import org.bukkit.craftbukkit.entity.CraftPlayer;
++import org.jetbrains.annotations.Contract;
++import org.jetbrains.annotations.NotNull;
++import org.leavesmc.leaves.bot.agent.Actions;
++import org.leavesmc.leaves.bot.agent.BotAction;
++import org.leavesmc.leaves.entity.botaction.BotActionType;
++import org.leavesmc.leaves.entity.botaction.LeavesBotAction;
++
++public class CraftBotAction extends LeavesBotAction {
++
++ private final BotAction> handle;
++
++ public CraftBotAction(@NotNull BotAction> action) {
++ super(BotActionType.valueOf(action.getName()), action.getTickDelay(), action.getCanDoNumber());
++ this.handle = action;
++ }
++
++ @Contract("_ -> new")
++ @NotNull
++ public static LeavesBotAction asAPICopy(BotAction> action) {
++ return new CraftBotAction(action);
++ }
++
++ @NotNull
++ public static BotAction> asInternalCopy(@NotNull LeavesBotAction action) {
++ BotAction> act = Actions.getForName(action.getActionName());
++ if (act == null) {
++ throw new IllegalArgumentException("Invalid action name!");
++ }
++
++ BotAction> newAction = null;
++ String[] args = new String[]{String.valueOf(action.getExecuteInterval()), String.valueOf(action.getRemainingExecuteTime())};
++ try {
++ if (act instanceof CraftCustomBotAction customBotAction) {
++ newAction = customBotAction.createCraft(action.getActionPlayer(), args);
++ } else {
++ newAction = act.create();
++ newAction.loadCommand(action.getActionPlayer() == null ? null : ((CraftPlayer) action.getActionPlayer()).getHandle(), act.getArgument().parse(0, args));
++ }
++ } catch (IllegalArgumentException ignore) {
++ }
++
++ if (newAction == null) {
++ throw new IllegalArgumentException("Invalid action!"); // TODO look action
++ }
++ return newAction;
++ }
++
++ public BotAction> getHandle() {
++ return handle;
++ }
++}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..f9b48f38a9b530125849f30eeab497895da77a21
+index 0000000000000000000000000000000000000000..7b149243b08a44f1181e82217a8645ccab7732d7
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java
-@@ -0,0 +1,48 @@
+@@ -0,0 +1,49 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import net.minecraft.server.level.ServerPlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
+import org.leavesmc.leaves.bot.ServerBot;
+import org.leavesmc.leaves.bot.agent.BotAction;
+import org.leavesmc.leaves.command.CommandArgument;
+import org.leavesmc.leaves.command.CommandArgumentResult;
+import org.leavesmc.leaves.entity.botaction.CustomBotAction;
+
-+public class CraftCustomBotAction extends BotAction {
++public class CraftCustomBotAction extends BotAction {
+
+ private final CustomBotAction realAction;
+
+ public CraftCustomBotAction(String name, @NotNull CustomBotAction realAction) {
-+ super(name, new CommandArgument().setAllTabComplete(realAction.getTabComplete()));
++ super(name, CommandArgument.of().setAllTabComplete(realAction.getTabComplete()), null);
+ this.realAction = realAction;
+ }
+
+ @Override
-+ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) {
++ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
-+ public BotAction getNew(@NotNull Player player, String[] args) {
++ public CraftCustomBotAction createCraft(@Nullable Player player, String[] args) {
+ CustomBotAction newRealAction = realAction.getNew(player, args);
+ if (newRealAction != null) {
-+ return new CraftCustomBotAction(getName(), newRealAction);
++ return new CraftCustomBotAction(this.getName(), newRealAction);
+ }
+ return null;
+ }
@@ -2861,33 +3527,32 @@ index 0000000000000000000000000000000000000000..f9b48f38a9b530125849f30eeab49789
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
-+ return realAction.doTick(bot.getBukkitPlayer());
++ return realAction.doTick(bot.getBukkitEntity());
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/DropAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/DropAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..c534e5f5a51021a5f08bae2e4bce55fcedf93cf0
+index 0000000000000000000000000000000000000000..c71e483e8938ef3b181c95d8e297e54203b5b914
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/DropAction.java
-@@ -0,0 +1,26 @@
+@@ -0,0 +1,25 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
+import org.leavesmc.leaves.bot.ServerBot;
-+import org.leavesmc.leaves.bot.agent.BotAction;
-+import org.leavesmc.leaves.command.CommandArgument;
+import org.leavesmc.leaves.command.CommandArgumentResult;
+
-+public class DropAction extends BotAction {
++public class DropAction extends AbstractTimerAction {
+
+ public DropAction() {
-+ super("drop", new CommandArgument());
++ super("drop", DropAction::new);
+ }
+
+ @Override
-+ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) {
-+ return this.setTickDelay(0).setNumber(1);
++ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) {
++ this.setTickDelay(result.readInt(100)).setNumber(result.readInt(1));
+ }
+
+ @Override
@@ -2898,53 +3563,56 @@ index 0000000000000000000000000000000000000000..c534e5f5a51021a5f08bae2e4bce55fc
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/FishAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/FishAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..18b2f81f9edcfc2f30dde82c832a899b84c8cd3a
+index 0000000000000000000000000000000000000000..3a13f8ac73e042d939496fb5602e4aa4ea368e0d
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/FishAction.java
-@@ -0,0 +1,70 @@
+@@ -0,0 +1,73 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
-+import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.nbt.CompoundTag;
+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 org.leavesmc.leaves.bot.ServerBot;
-+import org.leavesmc.leaves.bot.agent.BotAction;
-+import org.leavesmc.leaves.command.CommandArgument;
-+import org.leavesmc.leaves.command.CommandArgumentResult;
-+import org.leavesmc.leaves.command.CommandArgumentType;
+
-+import java.util.List;
-+
-+public class FishAction extends BotAction {
++public class FishAction extends AbstractTimerAction {
+
+ public FishAction() {
-+ super("fish", new CommandArgument(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER));
-+ setTabComplete(0, List.of("[TickDelay]"));
-+ setTabComplete(1, List.of("[DoNumber]"));
++ super("fish", FishAction::new);
+ }
+
-+ @Override
-+ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) {
-+ return new FishAction().setTickDelay(result.readInt(20)).setNumber(result.readInt(-1));
-+ }
++ private int delay = 0;
++ private int nowDelay = 0;
+
+ @Override
-+ public BotAction setTickDelay(int tickDelay) {
++ public FishAction setTickDelay(int tickDelay) {
+ super.setTickDelay(0);
+ this.delay = tickDelay;
+ return this;
+ }
+
-+ private int delay = 0;
-+ private int nowDelay = 0;
++ @Override
++ @NotNull
++ public CompoundTag save(@NotNull CompoundTag nbt) {
++ super.save(nbt);
++ nbt.putInt("fishDelay", this.delay);
++ nbt.putInt("fishNowDelay", this.nowDelay);
++ return nbt;
++ }
++
++ @Override
++ public void load(@NotNull CompoundTag nbt) {
++ super.load(nbt);
++ this.delay = nbt.getInt("fishDelay");
++ this.nowDelay = nbt.getInt("fishNowDelay");
++ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
-+ if (nowDelay > 0) {
-+ nowDelay--;
++ if (this.nowDelay > 0) {
++ this.nowDelay--;
+ return false;
+ }
+
@@ -2957,12 +3625,12 @@ index 0000000000000000000000000000000000000000..18b2f81f9edcfc2f30dde82c832a899b
+ if (fishingHook != null) {
+ if (fishingHook.currentState == FishingHook.FishHookState.HOOKED_IN_ENTITY) {
+ mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND);
-+ nowDelay = 20;
++ this.nowDelay = 20;
+ return false;
+ }
+ if (fishingHook.nibble > 0) {
+ mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND);
-+ nowDelay = delay;
++ this.nowDelay = this.delay;
+ return true;
+ }
+ } else {
@@ -2974,33 +3642,19 @@ index 0000000000000000000000000000000000000000..18b2f81f9edcfc2f30dde82c832a899b
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/JumpAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/JumpAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..de4c2aaf8f2f212e346ed41a514c803aa4fe7ac6
+index 0000000000000000000000000000000000000000..6fc9ba9bf94cb19ed32cfafa3a44fad0201b14a6
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/JumpAction.java
-@@ -0,0 +1,35 @@
+@@ -0,0 +1,21 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
-+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.bot.ServerBot;
-+import org.leavesmc.leaves.bot.agent.BotAction;
-+import org.leavesmc.leaves.command.CommandArgument;
-+import org.leavesmc.leaves.command.CommandArgumentResult;
-+import org.leavesmc.leaves.command.CommandArgumentType;
-+
-+import java.util.List;
+
-+public class JumpAction extends BotAction {
++public class JumpAction extends AbstractTimerAction {
+
+ 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));
++ super("jump", JumpAction::new);
+ }
+
+ @Override
@@ -3015,15 +3669,17 @@ index 0000000000000000000000000000000000000000..de4c2aaf8f2f212e346ed41a514c803a
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/LookAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/LookAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..bd8a62fb4385a00a7f532835f7d75e47ae45405b
+index 0000000000000000000000000000000000000000..8be962cf7dc273ccb6a6754684a9be8353865225
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/LookAction.java
-@@ -0,0 +1,49 @@
+@@ -0,0 +1,63 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
++import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerPlayer;
+import org.bukkit.util.Vector;
+import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
+import org.leavesmc.leaves.bot.ServerBot;
+import org.leavesmc.leaves.bot.agent.BotAction;
+import org.leavesmc.leaves.command.CommandArgument;
@@ -3032,34 +3688,46 @@ index 0000000000000000000000000000000000000000..bd8a62fb4385a00a7f532835f7d75e47
+
+import java.util.List;
+
-+public class LookAction extends BotAction {
++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(""));
++ super("look", CommandArgument.of(CommandArgumentType.DOUBLE, CommandArgumentType.DOUBLE, CommandArgumentType.DOUBLE), LookAction::new);
++ this.setTabComplete(0, List.of(""));
++ this.setTabComplete(1, List.of(""));
++ this.setTabComplete(2, List.of(""));
+ }
+
++ private Vector pos;
++
+ @Override
-+ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) {
++ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) throws IllegalArgumentException {
+ Vector pos = result.readVector();
+ if (pos != null) {
-+ return new LookAction().setPos(pos).setTickDelay(0).setNumber(1);
++ this.setPos(pos).setTickDelay(0).setNumber(1);
+ } else {
-+ return null;
++ throw new IllegalArgumentException("pos?");
+ }
+ }
+
-+ private Vector pos;
++ @Override
++ @NotNull
++ public CompoundTag save(@NotNull CompoundTag nbt) {
++ super.save(nbt);
++ nbt.putDouble("x", this.pos.getX());
++ nbt.putDouble("y", this.pos.getY());
++ nbt.putDouble("z", this.pos.getZ());
++ return nbt;
++ }
++
++ @Override
++ public void load(@NotNull CompoundTag nbt) {
++ super.load(nbt);
++ this.setPos(new Vector(nbt.getDouble("x"), nbt.getDouble("y"), nbt.getDouble("z")));
++ }
+
+ public LookAction setPos(Vector pos) {
-+ if (pos != null) {
-+ this.pos = pos;
-+ return this;
-+ } else {
-+ return null;
-+ }
++ this.pos = pos;
++ return this;
+ }
+
+ @Override
@@ -3070,38 +3738,56 @@ index 0000000000000000000000000000000000000000..bd8a62fb4385a00a7f532835f7d75e47
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotateAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotateAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..fe872cad0cbf9ede825e0561cdb4ecb24df32821
+index 0000000000000000000000000000000000000000..84eb7bd727a0085d005a6ee518dfbb8b44fce991
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotateAction.java
-@@ -0,0 +1,33 @@
+@@ -0,0 +1,51 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
++import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
+import org.leavesmc.leaves.bot.ServerBot;
+import org.leavesmc.leaves.bot.agent.BotAction;
+import org.leavesmc.leaves.command.CommandArgument;
+import org.leavesmc.leaves.command.CommandArgumentResult;
+
-+public class RotateAction extends BotAction {
++public class RotateAction extends BotAction {
+
+ public RotateAction() {
-+ super("rotate", new CommandArgument());
++ super("rotate", CommandArgument.EMPTY, RotateAction::new);
+ }
+
++ private ServerPlayer player;
++
+ @Override
-+ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) {
-+ return new RotateAction().setPlayer(player).setTickDelay(0).setNumber(1);
++ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) {
++ this.setPlayer(player).setTickDelay(0).setNumber(1);
+ }
+
-+ private ServerPlayer player;
-+
+ public RotateAction setPlayer(ServerPlayer player) {
+ this.player = player;
+ return this;
+ }
+
+ @Override
++ @NotNull
++ public CompoundTag save(@NotNull CompoundTag nbt) {
++ super.save(nbt);
++ nbt.putString("actionName", "look"); // to player loc
++ nbt.putDouble("x", player.getX());
++ nbt.putDouble("y", player.getY());
++ nbt.putDouble("z", player.getZ());
++ return nbt;
++ }
++
++ @Override
++ public void load(@NotNull CompoundTag nbt) {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ bot.faceLocation(player.getBukkitEntity().getLocation());
+ return true;
@@ -3109,14 +3795,16 @@ index 0000000000000000000000000000000000000000..fe872cad0cbf9ede825e0561cdb4ecb2
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotationAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotationAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..6983bf8555058cb715328f761df80d0b89c0b8f0
+index 0000000000000000000000000000000000000000..671d1aada7aa3cac0f3df8eec235b2f1ae389492
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotationAction.java
-@@ -0,0 +1,44 @@
+@@ -0,0 +1,65 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
++import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
+import org.leavesmc.leaves.bot.ServerBot;
+import org.leavesmc.leaves.bot.agent.BotAction;
+import org.leavesmc.leaves.command.CommandArgument;
@@ -3125,22 +3813,26 @@ index 0000000000000000000000000000000000000000..6983bf8555058cb715328f761df80d0b
+
+import java.util.List;
+
-+public class RotationAction extends BotAction {
++public class RotationAction extends BotAction {
+
+ public RotationAction() {
-+ super("rotation", new CommandArgument(CommandArgumentType.FLOAT, CommandArgumentType.FLOAT));
-+ setTabComplete(0, List.of(""));
-+ setTabComplete(1, List.of(""));
-+ }
-+
-+ @Override
-+ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) {
-+ return new RotationAction().setYaw(result.readFloat(player.getYRot())).setPitch(result.readFloat(player.getXRot())).setTickDelay(0).setNumber(1);
++ super("rotation", CommandArgument.of(CommandArgumentType.FLOAT, CommandArgumentType.FLOAT), RotateAction::new);
++ this.setTabComplete(0, List.of(""));
++ this.setTabComplete(1, List.of(""));
+ }
+
+ private float yaw;
+ private float pitch;
+
++ @Override
++ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) {
++ if (player == null) {
++ return;
++ }
++
++ this.setYaw(result.readFloat(player.getYRot())).setPitch(result.readFloat(player.getXRot())).setTickDelay(0).setNumber(1);
++ }
++
+ public RotationAction setYaw(float yaw) {
+ this.yaw = yaw;
+ return this;
@@ -3152,143 +3844,118 @@ index 0000000000000000000000000000000000000000..6983bf8555058cb715328f761df80d0b
+ }
+
+ @Override
-+ public boolean doTick(@NotNull ServerBot bot) {
-+ bot.setRot(yaw, pitch);
-+ return true;
-+ }
-+}
-diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/SneakAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/SneakAction.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..9b4406bc0b418abc6a253e047c504b4ad15f059a
---- /dev/null
-+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/SneakAction.java
-@@ -0,0 +1,27 @@
-+package org.leavesmc.leaves.bot.agent.actions;
-+
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.world.entity.Pose;
-+import org.jetbrains.annotations.NotNull;
-+import org.leavesmc.leaves.bot.ServerBot;
-+import org.leavesmc.leaves.bot.agent.BotAction;
-+import org.leavesmc.leaves.command.CommandArgument;
-+import org.leavesmc.leaves.command.CommandArgumentResult;
-+
-+public class SneakAction extends BotAction {
-+
-+ public SneakAction() {
-+ super("sneak", new CommandArgument());
++ @NotNull
++ public CompoundTag save(@NotNull CompoundTag nbt) {
++ super.save(nbt);
++ nbt.putFloat("yaw", this.yaw);
++ nbt.putFloat("pitch", this.pitch);
++ return nbt;
+ }
+
+ @Override
-+ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) {
-+ return this.setTickDelay(0).setNumber(1);
++ public void load(@NotNull CompoundTag nbt) {
++ super.load(nbt);
++ this.setYaw(nbt.getFloat("yaw")).setPitch(nbt.getFloat("pitch"));
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
-+ bot.setShiftKeyDown(!bot.isShiftKeyDown());
++ bot.setRot(yaw, pitch);
+ return true;
+ }
+}
-diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/StopAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/StopAction.java
+diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/SneakAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/SneakAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..bd02fe0e6b1f1b6048d56ab9c9379808dcf227c4
+index 0000000000000000000000000000000000000000..923cf55d81fce5cf9db9a1c7adc6f3aed5753b16
--- /dev/null
-+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/StopAction.java
-@@ -0,0 +1,26 @@
++++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/SneakAction.java
+@@ -0,0 +1,27 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
+import org.leavesmc.leaves.bot.ServerBot;
+import org.leavesmc.leaves.bot.agent.BotAction;
+import org.leavesmc.leaves.command.CommandArgument;
+import org.leavesmc.leaves.command.CommandArgumentResult;
+
-+public class StopAction extends BotAction {
++public class SneakAction extends BotAction {
+
-+ public StopAction() {
-+ super("stop", new CommandArgument());
++ public SneakAction() {
++ super("sneak", CommandArgument.EMPTY, SneakAction::new);
+ }
+
+ @Override
-+ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) {
-+ return this.setTickDelay(0).setNumber(0);
++ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) {
++ this.setTickDelay(0).setNumber(1);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
-+ this.setCancel(true);
++ bot.setShiftKeyDown(!bot.isShiftKeyDown());
+ return true;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/SwimAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/SwimAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..c91ca987eb5922b8dbcd271deb33f80be5e1a9af
+index 0000000000000000000000000000000000000000..b5ccedee17857bc955301512ee965d81fd12017f
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/SwimAction.java
-@@ -0,0 +1,26 @@
+@@ -0,0 +1,30 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.phys.Vec3;
+import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
+import org.leavesmc.leaves.bot.ServerBot;
+import org.leavesmc.leaves.bot.agent.BotAction;
+import org.leavesmc.leaves.command.CommandArgument;
+import org.leavesmc.leaves.command.CommandArgumentResult;
+
-+public class SwimAction extends BotAction {
++public class SwimAction extends BotAction {
+
+ public SwimAction() {
-+ super("swim", new CommandArgument());
++ super("swim", CommandArgument.EMPTY, SwimAction::new);
+ }
+
+ @Override
-+ public BotAction getNew(@NotNull ServerPlayer player, @NotNull CommandArgumentResult result) {
-+ return this.setTickDelay(0).setNumber(1);
++ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) {
++ this.setTickDelay(0).setNumber(-1);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
-+ bot.waterSwim = !bot.waterSwim;
++ if (bot.isInWater()) {
++ bot.addDeltaMovement(new Vec3(0, 0.03, 0));
++ }
+ return true;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..cf56fbdd461ae7f3a00043dbe979421a1dd3b5a4
+index 0000000000000000000000000000000000000000..1bdde4f8dc5e379d45fac19ba11aa07c4a1b735c
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemAction.java
-@@ -0,0 +1,34 @@
+@@ -0,0 +1,22 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
-+import net.minecraft.core.component.DataComponents;
-+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.InteractionHand;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.bot.ServerBot;
-+import org.leavesmc.leaves.bot.agent.BotAction;
-+import org.leavesmc.leaves.command.CommandArgument;
-+import org.leavesmc.leaves.command.CommandArgumentResult;
-+import org.leavesmc.leaves.command.CommandArgumentType;
+
-+import java.util.List;
-+
-+public class UseItemAction extends BotAction {
++public class UseItemAction extends AbstractTimerAction {
+
+ 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));
++ super("use", UseItemAction::new);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
++ if (bot.isUsingItem()) {
++ return false;
++ }
+ bot.swing(InteractionHand.MAIN_HAND);
+ bot.updateItemInHand(InteractionHand.MAIN_HAND);
+ return bot.gameMode.useItem(bot, bot.level(), bot.getItemInHand(InteractionHand.MAIN_HAND), InteractionHand.MAIN_HAND).consumesAction();
@@ -3296,38 +3963,27 @@ index 0000000000000000000000000000000000000000..cf56fbdd461ae7f3a00043dbe979421a
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..475bf440d40ac35720b01da18440cf0d35166a5c
+index 0000000000000000000000000000000000000000..f6de022b7177da0eb7c089f11ce039ab22c34903
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java
-@@ -0,0 +1,33 @@
+@@ -0,0 +1,22 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
-+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.InteractionHand;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.bot.ServerBot;
-+import org.leavesmc.leaves.bot.agent.BotAction;
-+import org.leavesmc.leaves.command.CommandArgument;
-+import org.leavesmc.leaves.command.CommandArgumentResult;
-+import org.leavesmc.leaves.command.CommandArgumentType;
+
-+import java.util.List;
-+
-+public class UseItemOffHandAction extends BotAction {
++public class UseItemOffHandAction extends AbstractTimerAction {
+
+ 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));
++ super("use_offhand", UseItemOffHandAction::new);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
++ if (bot.isUsingItem()) {
++ return false;
++ }
+ bot.swing(InteractionHand.OFF_HAND);
+ bot.updateItemInHand(InteractionHand.OFF_HAND);
+ return bot.gameMode.useItem(bot, bot.level(), bot.getItemInHand(InteractionHand.OFF_HAND), InteractionHand.OFF_HAND).consumesAction();
@@ -3335,13 +3991,12 @@ index 0000000000000000000000000000000000000000..475bf440d40ac35720b01da18440cf0d
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..fe8c4ba7aff74dc297bdf4e271a2d7f1229c357d
+index 0000000000000000000000000000000000000000..f73cd841009117e4032f953a3e754305d97a68be
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java
-@@ -0,0 +1,56 @@
+@@ -0,0 +1,42 @@
+package org.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;
@@ -3351,27 +4006,14 @@ index 0000000000000000000000000000000000000000..fe8c4ba7aff74dc297bdf4e271a2d7f1
+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 org.leavesmc.leaves.bot.ServerBot;
-+import org.leavesmc.leaves.bot.agent.BotAction;
-+import org.leavesmc.leaves.command.CommandArgument;
-+import org.leavesmc.leaves.command.CommandArgumentResult;
-+import org.leavesmc.leaves.command.CommandArgumentType;
-+
-+import java.util.List;
++import org.leavesmc.leaves.plugin.MinecraftInternalPlugin;
+
-+public class UseItemOnAction extends BotAction {
++public class UseItemOnAction extends AbstractTimerAction {
+
+ 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));
++ super("use_on", UseItemOnAction::new);
+ }
+
+ @Override
@@ -3384,7 +4026,7 @@ index 0000000000000000000000000000000000000000..fe8c4ba7aff74dc297bdf4e271a2d7f1
+ BlockEntity entity = bot.serverLevel().getBlockEntity(blockHitResult.getBlockPos());
+ if (entity instanceof TrappedChestBlockEntity chestBlockEntity) {
+ chestBlockEntity.startOpen(bot);
-+ Bukkit.getScheduler().runTaskLater(CraftScheduler.MINECRAFT, () -> chestBlockEntity.stopOpen(bot), 1);
++ Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> chestBlockEntity.stopOpen(bot), 1);
+ return true;
+ }
+ } else {
@@ -3397,13 +4039,12 @@ index 0000000000000000000000000000000000000000..fe8c4ba7aff74dc297bdf4e271a2d7f1
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..a69ede01e5e5df31a144f98e9871183793f5f377
+index 0000000000000000000000000000000000000000..c0cd258151f690ccbf3df1ffd640b83d8f36aa7d
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java
-@@ -0,0 +1,56 @@
+@@ -0,0 +1,42 @@
+package org.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;
@@ -3413,27 +4054,14 @@ index 0000000000000000000000000000000000000000..a69ede01e5e5df31a144f98e98711837
+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 org.leavesmc.leaves.bot.ServerBot;
-+import org.leavesmc.leaves.bot.agent.BotAction;
-+import org.leavesmc.leaves.command.CommandArgument;
-+import org.leavesmc.leaves.command.CommandArgumentResult;
-+import org.leavesmc.leaves.command.CommandArgumentType;
-+
-+import java.util.List;
++import org.leavesmc.leaves.plugin.MinecraftInternalPlugin;
+
-+public class UseItemOnOffhandAction extends BotAction {
++public class UseItemOnOffhandAction extends AbstractTimerAction {
+
+ 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));
++ super("use_on_offhand", UseItemOnOffhandAction::new);
+ }
+
+ @Override
@@ -3446,7 +4074,7 @@ index 0000000000000000000000000000000000000000..a69ede01e5e5df31a144f98e98711837
+ BlockEntity entity = bot.serverLevel().getBlockEntity(blockHitResult.getBlockPos());
+ if (entity instanceof TrappedChestBlockEntity chestBlockEntity) {
+ chestBlockEntity.startOpen(bot);
-+ Bukkit.getScheduler().runTaskLater(CraftScheduler.MINECRAFT, () -> chestBlockEntity.stopOpen(bot), 1);
++ Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> chestBlockEntity.stopOpen(bot), 1);
+ return true;
+ }
+ } else {
@@ -3459,35 +4087,21 @@ index 0000000000000000000000000000000000000000..a69ede01e5e5df31a144f98e98711837
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..0dbe7fd2e63cd47f892e383d71fdbff1c0d8d2ab
+index 0000000000000000000000000000000000000000..2048024b754cce85d2bf4f2cbcb800a1f4727495
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToAction.java
-@@ -0,0 +1,38 @@
+@@ -0,0 +1,24 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
-+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.entity.Entity;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.bot.ServerBot;
-+import org.leavesmc.leaves.bot.agent.BotAction;
-+import org.leavesmc.leaves.command.CommandArgument;
-+import org.leavesmc.leaves.command.CommandArgumentResult;
-+import org.leavesmc.leaves.command.CommandArgumentType;
-+
-+import java.util.List;
+
-+public class UseItemToAction extends BotAction {
++public class UseItemToAction extends AbstractTimerAction {
+
+ 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));
++ super("use_to", UseItemToAction::new);
+ }
+
+ @Override
@@ -3503,35 +4117,21 @@ index 0000000000000000000000000000000000000000..0dbe7fd2e63cd47f892e383d71fdbff1
+}
diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java
new file mode 100644
-index 0000000000000000000000000000000000000000..0e3895073bcfb5944c7147395338d750137e1386
+index 0000000000000000000000000000000000000000..e42288a4f99f7de6655d49ee1a05d37b79652c22
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java
-@@ -0,0 +1,38 @@
+@@ -0,0 +1,24 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
-+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.entity.Entity;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.bot.ServerBot;
-+import org.leavesmc.leaves.bot.agent.BotAction;
-+import org.leavesmc.leaves.command.CommandArgument;
-+import org.leavesmc.leaves.command.CommandArgumentResult;
-+import org.leavesmc.leaves.command.CommandArgumentType;
+
-+import java.util.List;
-+
-+public class UseItemToOffhandAction extends BotAction {
++public class UseItemToOffhandAction extends AbstractTimerAction {
+
+ 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));
++ super("use_to_offhand", UseItemToOffhandAction::new);
+ }
+
+ @Override
@@ -3545,23 +4145,234 @@ index 0000000000000000000000000000000000000000..0e3895073bcfb5944c7147395338d750
+ return false;
+ }
+}
+diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/configs/AlwaysSendDataConfig.java b/src/main/java/org/leavesmc/leaves/bot/agent/configs/AlwaysSendDataConfig.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9a584603edbbe4ccd8a88c90ef3e9125480635f1
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/bot/agent/configs/AlwaysSendDataConfig.java
+@@ -0,0 +1,45 @@
++package org.leavesmc.leaves.bot.agent.configs;
++
++import net.minecraft.nbt.CompoundTag;
++import org.jetbrains.annotations.NotNull;
++import org.leavesmc.leaves.LeavesConfig;
++
++import org.leavesmc.leaves.bot.agent.BotConfig;
++import org.leavesmc.leaves.command.CommandArgument;
++import org.leavesmc.leaves.command.CommandArgumentResult;
++import org.leavesmc.leaves.command.CommandArgumentType;
++
++import java.util.List;
++
++public class AlwaysSendDataConfig extends BotConfig {
++
++ private boolean value;
++
++ public AlwaysSendDataConfig() {
++ super("always_send_data", CommandArgument.of(CommandArgumentType.BOOLEAN).setTabComplete(0, List.of("ture", "false")), AlwaysSendDataConfig::new);
++ this.value = LeavesConfig.alwaysSendFakeplayerData;
++ }
++
++ @Override
++ public Boolean getValue() {
++ return value;
++ }
++
++ @Override
++ public void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException {
++ this.value = result.readBoolean(this.value);
++ }
++
++ @Override
++ @NotNull
++ public CompoundTag save(@NotNull CompoundTag nbt) {
++ super.save(nbt);
++ nbt.putBoolean("always_send_data", this.getValue());
++ return nbt;
++ }
++
++ @Override
++ public void load(@NotNull CompoundTag nbt) {
++ this.value = nbt.getBoolean("always_send_data");
++ }
++}
+diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/configs/SimulationDistanceConfig.java b/src/main/java/org/leavesmc/leaves/bot/agent/configs/SimulationDistanceConfig.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c8a2243361cd03e9c64b6a04b37725b549e5b87f
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/bot/agent/configs/SimulationDistanceConfig.java
+@@ -0,0 +1,47 @@
++package org.leavesmc.leaves.bot.agent.configs;
++
++import net.minecraft.nbt.CompoundTag;
++import org.jetbrains.annotations.NotNull;
++import org.leavesmc.leaves.bot.agent.BotConfig;
++import org.leavesmc.leaves.command.CommandArgument;
++import org.leavesmc.leaves.command.CommandArgumentResult;
++import org.leavesmc.leaves.command.CommandArgumentType;
++
++import java.util.ArrayList;
++import java.util.List;
++
++public class SimulationDistanceConfig extends BotConfig {
++
++ public SimulationDistanceConfig() {
++ super("simulation_distance", CommandArgument.of(CommandArgumentType.INTEGER).setTabComplete(0, List.of("2", "10", "")), SimulationDistanceConfig::new);
++ }
++
++ @Override
++ public Integer getValue() {
++ return this.bot.getBukkitEntity().getSimulationDistance();
++ }
++
++ @Override
++ public void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException {
++ int realValue = result.readInt(this.bot.getBukkitEntity().getSimulationDistance());
++ if (realValue < 2 || realValue > 32) {
++ throw new IllegalArgumentException("simulation_distance must be a number between 2 and 32, got: " + result);
++ }
++ this.bot.getBukkitEntity().setSimulationDistance(realValue);
++ }
++
++ @Override
++ @NotNull
++ public CompoundTag save(@NotNull CompoundTag nbt) {
++ super.save(nbt);
++ nbt.putInt("simulation_distance", this.getValue());
++ return nbt;
++ }
++
++ @Override
++ public void load(@NotNull CompoundTag nbt) {
++ this.setValue(new CommandArgumentResult(new ArrayList<>(){{
++ add(nbt.getInt("simulation_distance"));
++ }}));
++ }
++}
+diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/configs/SkipSleepConfig.java b/src/main/java/org/leavesmc/leaves/bot/agent/configs/SkipSleepConfig.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0d934910cff745ea9a53d651e20079635ea6781c
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/bot/agent/configs/SkipSleepConfig.java
+@@ -0,0 +1,42 @@
++package org.leavesmc.leaves.bot.agent.configs;
++
++import net.minecraft.nbt.CompoundTag;
++import org.jetbrains.annotations.NotNull;
++import org.leavesmc.leaves.bot.agent.BotConfig;
++import org.leavesmc.leaves.command.CommandArgument;
++import org.leavesmc.leaves.command.CommandArgumentResult;
++import org.leavesmc.leaves.command.CommandArgumentType;
++
++import java.util.ArrayList;
++import java.util.List;
++
++public class SkipSleepConfig extends BotConfig {
++
++ public SkipSleepConfig() {
++ super("skip_sleep", CommandArgument.of(CommandArgumentType.BOOLEAN).setTabComplete(0, List.of("ture", "false")), SkipSleepConfig::new);
++ }
++
++ @Override
++ public Boolean getValue() {
++ return bot.fauxSleeping;
++ }
++
++ @Override
++ public void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException {
++ bot.fauxSleeping = result.readBoolean(bot.fauxSleeping);
++ }
++
++ @Override
++ public @NotNull CompoundTag save(@NotNull CompoundTag nbt) {
++ super.save(nbt);
++ nbt.putBoolean("skip_sleep", this.getValue());
++ return nbt;
++ }
++
++ @Override
++ public void load(@NotNull CompoundTag nbt) {
++ this.setValue(new CommandArgumentResult(new ArrayList<>() {{
++ add(nbt.getBoolean("skip_sleep"));
++ }}));
++ }
++}
+diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/configs/SpawnPhantomConfig.java b/src/main/java/org/leavesmc/leaves/bot/agent/configs/SpawnPhantomConfig.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a3f978318a67c3c5e147a50eb2b6c01c3f549dc2
+--- /dev/null
++++ b/src/main/java/org/leavesmc/leaves/bot/agent/configs/SpawnPhantomConfig.java
+@@ -0,0 +1,51 @@
++package org.leavesmc.leaves.bot.agent.configs;
++
++import net.minecraft.nbt.CompoundTag;
++import org.jetbrains.annotations.NotNull;
++import org.leavesmc.leaves.LeavesConfig;
++import org.leavesmc.leaves.bot.agent.BotConfig;
++import org.leavesmc.leaves.command.CommandArgument;
++import org.leavesmc.leaves.command.CommandArgumentResult;
++import org.leavesmc.leaves.command.CommandArgumentType;
++
++import java.util.List;
++
++public class SpawnPhantomConfig extends BotConfig {
++
++ private boolean value;
++
++ public SpawnPhantomConfig() {
++ super("spawn_phantom", CommandArgument.of(CommandArgumentType.BOOLEAN).setTabComplete(0, List.of("ture", "false")), SpawnPhantomConfig::new);
++ this.value = LeavesConfig.fakeplayerSpawnPhantom;
++ }
++
++ @Override
++ public Boolean getValue() {
++ return value;
++ }
++
++ @Override
++ public void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException {
++ this.value = result.readBoolean(this.value);
++ }
++
++ @Override
++ public List getMessage() {
++ return List.of(
++ bot.getScoreboardName() + "'s spawn_phantom: " + this.getValue(),
++ bot.getScoreboardName() + "'s not_sleeping_ticks: " + bot.notSleepTicks
++ );
++ }
++
++ @Override
++ public @NotNull CompoundTag save(@NotNull CompoundTag nbt) {
++ super.save(nbt);
++ nbt.putBoolean("spawn_phantom", this.getValue());
++ return nbt;
++ }
++
++ @Override
++ public void load(@NotNull CompoundTag nbt) {
++ this.value = nbt.getBoolean("spawn_phantom");
++ }
++}
diff --git a/src/main/java/org/leavesmc/leaves/entity/CraftBot.java b/src/main/java/org/leavesmc/leaves/entity/CraftBot.java
new file mode 100644
-index 0000000000000000000000000000000000000000..fe1df01906f15e130cf947bbecb5df4bddf98c7c
+index 0000000000000000000000000000000000000000..744f5533859bfe81267b3fd5133597e80e0fdc73
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/entity/CraftBot.java
-@@ -0,0 +1,67 @@
+@@ -0,0 +1,84 @@
+package org.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 org.leavesmc.leaves.bot.BotList;
+import org.leavesmc.leaves.bot.ServerBot;
-+import org.leavesmc.leaves.bot.agent.Actions;
+import org.leavesmc.leaves.bot.agent.BotAction;
++import org.leavesmc.leaves.bot.agent.actions.CraftBotAction;
+import org.leavesmc.leaves.entity.botaction.LeavesBotAction;
++import org.leavesmc.leaves.event.bot.BotActionStopEvent;
++import org.leavesmc.leaves.event.bot.BotRemoveEvent;
+
+import java.util.UUID;
+
@@ -3573,35 +4384,50 @@ index 0000000000000000000000000000000000000000..fe1df01906f15e130cf947bbecb5df4b
+
+ @Override
+ public String getSkinName() {
-+ return getHandle().createState.skinName;
++ return this.getHandle().createState.skinName();
+ }
+
+ @Override
+ public @NotNull String getRealName() {
-+ return getHandle().createState.getRealName();
++ return this.getHandle().createState.realName();
+ }
+
+ @Override
+ public @Nullable UUID getCreatePlayerUUID() {
-+ return getHandle().createPlayer;
++ return this.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;
-+ }
++ public void addAction(@NotNull LeavesBotAction action) {
++ this.getHandle().addBotAction(CraftBotAction.asInternalCopy(action));
++ }
++
++ @Override
++ public LeavesBotAction getAction(int index) {
++ return CraftBotAction.asAPICopy(this.getHandle().getBotActions().get(index));
++ }
++
++ @Override
++ public int getActionSize() {
++ return this.getHandle().getBotActions().size();
++ }
++
++ @Override
++ public void stopAction(int index) {
++ this.getHandle().getBotActions().get(index).stop(this.getHandle(), BotActionStopEvent.Reason.PLUGIN);
++ }
++
++ @Override
++ public void stopAllActions() {
++ for (BotAction> action : this.getHandle().getBotActions()) {
++ action.stop(this.getHandle(), BotActionStopEvent.Reason.PLUGIN);
+ }
-+ return false;
+ }
+
+ @Override
-+ public boolean setBotAction(@NotNull LeavesBotAction action, @NotNull Player player, @NotNull String[] args) {
-+ return setBotAction(action.getName(), player, args);
++ public boolean remove(boolean save) {
++ BotList.INSTANCE.removeBot(this.getHandle(), BotRemoveEvent.RemoveReason.PLUGIN, null, save);
++ return true;
+ }
+
+ @Override
@@ -3620,98 +4446,67 @@ index 0000000000000000000000000000000000000000..fe1df01906f15e130cf947bbecb5df4b
+}
diff --git a/src/main/java/org/leavesmc/leaves/entity/CraftBotManager.java b/src/main/java/org/leavesmc/leaves/entity/CraftBotManager.java
new file mode 100644
-index 0000000000000000000000000000000000000000..4eaee3c09372f314d8a5930b4162a7d88d7cc105
+index 0000000000000000000000000000000000000000..422640df346ccae612b2d3492780efa59d8b4d17
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/entity/CraftBotManager.java
-@@ -0,0 +1,102 @@
+@@ -0,0 +1,80 @@
+package org.leavesmc.leaves.entity;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
++import net.minecraft.server.MinecraftServer;
+import org.bukkit.Location;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
++import org.leavesmc.leaves.bot.BotCreateState;
++import org.leavesmc.leaves.bot.BotList;
+import org.leavesmc.leaves.bot.ServerBot;
+import org.leavesmc.leaves.bot.agent.Actions;
++import org.leavesmc.leaves.bot.agent.BotAction;
+import org.leavesmc.leaves.bot.agent.actions.CraftCustomBotAction;
+import org.leavesmc.leaves.entity.botaction.CustomBotAction;
+import org.leavesmc.leaves.event.bot.BotCreateEvent;
-+import org.leavesmc.leaves.event.bot.BotRemoveEvent;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.UUID;
-+import java.util.function.Consumer;
+
+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) {
-+ return this.createBot(name, realName, skin, skinName, location, null);
-+ }
-+
-+ @Override
-+ public @Nullable Bot createBot(@NotNull String name, @NotNull String realName, @Nullable String[] skin, @Nullable String skinName, @NotNull Location location, @Nullable Consumer consumer) {
-+ return new ServerBot.BotCreateState(location, name, realName, skinName, skin, BotCreateEvent.CreateReason.PLUGIN, null).createNow((serverBot -> {
-+ if (consumer != null) {
-+ consumer.accept(serverBot.getBukkitPlayer());
-+ }
-+ }));
-+ }
++ private final BotList botList;
++ private final Collection botViews;
+
-+ @Override
-+ public void createBot(@NotNull String name, @Nullable String skinName, @NotNull Location location, @Nullable Consumer consumer) {
-+ new ServerBot.BotCreateState(location, name, skinName, BotCreateEvent.CreateReason.PLUGIN, null).create((serverBot -> {
-+ if (consumer != null) {
-+ consumer.accept(serverBot.getBukkitPlayer());
++ public CraftBotManager() {
++ this.botList = MinecraftServer.getServer().getBotList();
++ this.botViews = Collections.unmodifiableList(Lists.transform(botList.bots, new Function() {
++ @Override
++ public CraftBot apply(ServerBot bot) {
++ return bot.getBukkitEntity();
+ }
+ }));
+ }
+
+ @Override
-+ public void removeBot(@NotNull String name) {
-+ ServerBot bot = ServerBot.getBot(name);
++ public @Nullable Bot getBot(@NotNull UUID uuid) {
++ ServerBot bot = botList.getBot(uuid);
+ if (bot != null) {
-+ bot.onRemove(org.leavesmc.leaves.event.bot.BotRemoveEvent.RemoveReason.PLUGIN);
++ return bot.getBukkitEntity();
++ } else {
++ return null;
+ }
+ }
+
+ @Override
-+ public void removeBot(@NotNull UUID uuid) {
-+ ServerBot bot = ServerBot.getBot(uuid);
++ public @Nullable Bot getBot(@NotNull String name) {
++ ServerBot bot = botList.getBotByName(name);
+ if (bot != null) {
-+ bot.onRemove(BotRemoveEvent.RemoveReason.PLUGIN);
++ return bot.getBukkitEntity();
++ } else {
++ return null;
+ }
+ }
+
+ @Override
-+ public void removeAllBots() {
-+ ServerBot.removeAllBot(BotRemoveEvent.RemoveReason.PLUGIN);
-+ }
-+
-+ @Override
-+ public void saveOrRemoveAllBots() {
-+ ServerBot.saveOrRemoveAllBot();
-+ }
-+
-+ @Override
+ public Collection getBots() {
+ return botViews;
+ }
@@ -3723,15 +4518,24 @@ index 0000000000000000000000000000000000000000..4eaee3c09372f314d8a5930b4162a7d8
+
+ @Override
+ public boolean unregisterCustomBotAction(String name) {
-+ return Actions.unregister(name);
++ BotAction> action = Actions.getForName(name);
++ if (action instanceof CraftCustomBotAction) {
++ return Actions.unregister(name);
++ }
++ return false;
++ }
++
++ @Override
++ public BotCreator botCreator(@NotNull String realName, @NotNull Location location) {
++ return BotCreateState.builder(realName, location).createReason(BotCreateEvent.CreateReason.PLUGIN);
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java b/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java
new file mode 100644
-index 0000000000000000000000000000000000000000..356b2de6ffd82e42a5d0f20ac1e3de7f4b5a7013
+index 0000000000000000000000000000000000000000..de06c854a9a5242cf632b38806e8e710496b7e4e
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java
-@@ -0,0 +1,149 @@
+@@ -0,0 +1,151 @@
+package org.leavesmc.leaves.plugin;
+
+import org.bukkit.Server;
@@ -3752,6 +4556,9 @@ index 0000000000000000000000000000000000000000..356b2de6ffd82e42a5d0f20ac1e3de7f
+import java.util.List;
+
+public class MinecraftInternalPlugin extends PluginBase {
++
++ public static final MinecraftInternalPlugin INSTANCE = new MinecraftInternalPlugin();
++
+ private boolean enabled = true;
+
+ private final PluginDescriptionFile pdf;
@@ -3774,12 +4581,11 @@ index 0000000000000000000000000000000000000000..356b2de6ffd82e42a5d0f20ac1e3de7f
+ public PluginDescriptionFile getDescription() {
+ return pdf;
+ }
-+ // Paper start
++
+ @Override
+ public io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() {
+ return pdf;
+ }
-+ // Paper end
+
+ @Override
+ public FileConfiguration getConfig() {
diff --git a/patches/server/0018-No-chat-sign.patch b/patches/server/0018-No-chat-sign.patch
index f98fa4cd..909c01fe 100644
--- a/patches/server/0018-No-chat-sign.patch
+++ b/patches/server/0018-No-chat-sign.patch
@@ -107,10 +107,10 @@ index 5705cb920084b775cce4b361683b32c6b6e003ed..cbff868303d751d09b68f431c78bb13b
}
}
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-index f97b2cb3ea855e6e250cabf357a050cd52be8f70..a80b7dfb5de950d0ebacfc041f903ed3322f77c9 100644
+index 4ac40259f31f3deef6b5fe4456576bbd44cc2569..e45acea31d61462c877c902b238209f14f2c8ad1 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-@@ -668,7 +668,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+@@ -669,7 +669,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
// Paper start - Add setting for proxy online mode status
return dedicatedserverproperties.enforceSecureProfile
&& io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()
@@ -149,10 +149,10 @@ index 01c2e26f92d5d1e46b98ebd20727beb779c98095..ced6a08caf546e245bd6a631df3dc9f8
if (packet == null || this.processedDisconnect) { // Spigot
return;
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
-index 901e9ff7a89b48b7acca7e5b96f640045c1d265e..a06b642d38f8367a5115e6e8a7bb4bae0acdb0bd 100644
+index 69f44d0351e8127e38cbe9e74c05b365c37b2c44..8ea03896e1d7f247d484628ec787d8a4f4488664 100644
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
-@@ -1443,7 +1443,7 @@ public abstract class PlayerList {
+@@ -1446,7 +1446,7 @@ public abstract class PlayerList {
}
public boolean verifyChatTrusted(PlayerChatMessage message) { // Paper - private -> public
diff --git a/patches/server/0023-Config-to-disable-method-profiler.patch b/patches/server/0023-Config-to-disable-method-profiler.patch
index 2750108b..09b46942 100644
--- a/patches/server/0023-Config-to-disable-method-profiler.patch
+++ b/patches/server/0023-Config-to-disable-method-profiler.patch
@@ -6,10 +6,10 @@ Subject: [PATCH] Config to disable method profiler
This patch is Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish)
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index a625473805fcde57f3987f3d788efb36fa89073e..0f07ee9d947235fc91f7d450d746f056a9b9e13a 100644
+index c68e0a6715c690298268457d6438b12cd5c6baf2..f742d01b441644f5de845c772a779eba0d49d6c1 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -2556,6 +2556,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop OVERWORLD = Level.OVERWORLD;
final ResourceKey THE_NETHER = Level.NETHER;
if (!((fromDim != OVERWORLD || toDim != THE_NETHER) && (fromDim != THE_NETHER || toDim != OVERWORLD))) {
@@ -493,22 +493,6 @@ index 7092a4d4a583f4e01cc02bca17f3bd1bd32677a0..32622ebdd9c5949ad995875d29e121a4
private static final int[] SLOTS_FOR_DOWN = new int[]{2, 1};
private static final int[] SLOTS_FOR_SIDES = new int[]{1};
public static final int DATA_LIT_DURATION = 1;
-diff --git a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
-index 8ab7ca373a885fbe658013c9c6a2e38d32d77bb2..bcf5e0045da9711f48689ffcd266411f71a7bae1 100644
---- a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
-+++ b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
-@@ -16,10 +16,10 @@ import net.minecraft.nbt.NbtAccounter;
- import net.minecraft.nbt.NbtIo;
- import net.minecraft.nbt.NbtUtils;
- import net.minecraft.server.level.ServerPlayer;
--import net.minecraft.util.datafix.DataFixTypes;
- import net.minecraft.world.entity.player.Player;
- import org.bukkit.craftbukkit.entity.CraftPlayer;
- import org.slf4j.Logger;
-+import org.leavesmc.leaves.util.ArrayConstants;
-
- public class PlayerDataStorage {
-
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftEquipmentSlot.java b/src/main/java/org/bukkit/craftbukkit/CraftEquipmentSlot.java
index ae86c45c1d49c7646c721991910592091e7333f8..f3dce7156d518193fe27a69f5792800b72742632 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftEquipmentSlot.java
diff --git a/patches/server/0091-Bow-infinity-fix.patch b/patches/server/0091-Bow-infinity-fix.patch
index 1fe200b1..4683509c 100644
--- a/patches/server/0091-Bow-infinity-fix.patch
+++ b/patches/server/0091-Bow-infinity-fix.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Bow infinity fix
diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java
-index 0ee227c0f7d54ed7c686a830873fab6c0cdd2ab5..e381b2d0e64b8d9834b573223f6370afc7c00b11 100644
+index b737848442843b4d7decc140f51f77ed6f7c5f4b..015104dfdfe9673a928f20f2946571a543a16772 100644
--- a/src/main/java/net/minecraft/world/entity/player/Player.java
+++ b/src/main/java/net/minecraft/world/entity/player/Player.java
-@@ -2342,7 +2342,7 @@ public abstract class Player extends LivingEntity {
+@@ -2348,7 +2348,7 @@ public abstract class Player extends LivingEntity {
}
}
diff --git a/patches/server/0095-Replay-Mod-API.patch b/patches/server/0095-Replay-Mod-API.patch
index a701767d..bdcdbe22 100644
--- a/patches/server/0095-Replay-Mod-API.patch
+++ b/patches/server/0095-Replay-Mod-API.patch
@@ -108,10 +108,10 @@ index c8d39e6e1c570c9219f6066da273dc0130920519..96a074281d16a7f64058619da4b102f3
if (((List) object).size() >= i) {
return (List) object;
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 9e5e89aa5220dcfa278931bc891b055ae4ef0ad8..49e5749d838883a9a292c283daba136b9629b8eb 100644
+index dc3972dd9340febc7540cc32160291c4945dec85..7ad8f7cbcf4238ba2d6771ef51fb09a42d0cdb96 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -1690,7 +1690,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {
++ this.server.getBotList().bots.forEach(bot1 -> {
+ bot1.sendPlayerInfo(player);
+ bot1.sendFakeDataIfNeed(player, true);
+ }); // Leaves - render bot
@@ -309,7 +307,7 @@ index 6e68ad42d4fff62e38f45fd09de26da3363dcdd9..d04ebc381ba3f52954b647bc76ccc9cf
public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData) {
player.isRealPlayer = true; // Paper
player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed
-@@ -328,6 +444,7 @@ public abstract class PlayerList {
+@@ -328,6 +442,7 @@ public abstract class PlayerList {
// entityplayer.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players)); // CraftBukkit - replaced with loop below
this.players.add(player);
@@ -317,7 +315,7 @@ index 6e68ad42d4fff62e38f45fd09de26da3363dcdd9..d04ebc381ba3f52954b647bc76ccc9cf
this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot
this.playersByUUID.put(player.getUUID(), player);
// this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityplayer))); // CraftBukkit - replaced with loop below
-@@ -399,6 +516,12 @@ public abstract class PlayerList {
+@@ -397,6 +512,12 @@ public abstract class PlayerList {
continue;
}
@@ -330,7 +328,7 @@ index 6e68ad42d4fff62e38f45fd09de26da3363dcdd9..d04ebc381ba3f52954b647bc76ccc9cf
onlinePlayers.add(entityplayer1); // Paper - Use single player info update packet on join
}
// Paper start - Use single player info update packet on join
-@@ -603,6 +726,43 @@ public abstract class PlayerList {
+@@ -601,6 +722,43 @@ public abstract class PlayerList {
}
@@ -374,7 +372,7 @@ index 6e68ad42d4fff62e38f45fd09de26da3363dcdd9..d04ebc381ba3f52954b647bc76ccc9cf
public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer) { // CraftBukkit - return string // Paper - return Component
// Paper start - Fix kick event leave message not being sent
return this.remove(entityplayer, net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? entityplayer.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(entityplayer.getDisplayName())));
-@@ -671,6 +831,7 @@ public abstract class PlayerList {
+@@ -669,6 +827,7 @@ public abstract class PlayerList {
entityplayer.retireScheduler(); // Paper - Folia schedulers
entityplayer.getAdvancements().stopListening();
this.players.remove(entityplayer);
@@ -382,7 +380,7 @@ index 6e68ad42d4fff62e38f45fd09de26da3363dcdd9..d04ebc381ba3f52954b647bc76ccc9cf
this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
this.server.getCustomBossEvents().onPlayerDisconnect(entityplayer);
UUID uuid = entityplayer.getUUID();
-@@ -765,7 +926,7 @@ public abstract class PlayerList {
+@@ -763,7 +922,7 @@ public abstract class PlayerList {
event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure
} else {
// return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile) ? IChatBaseComponent.translatable("multiplayer.disconnect.server_full") : null;
@@ -392,13 +390,13 @@ index 6e68ad42d4fff62e38f45fd09de26da3363dcdd9..d04ebc381ba3f52954b647bc76ccc9cf
}
}
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
-index 59cdcf7b25c17705b613c83dea107934b683af28..b759025019120a0029c46506f1eede4eb85d7550 100644
+index 97d09246b5bab3fe85491d06c7b16f932bcd1cb2..d4b4b4a4baf2321fa682d26885e07186375b53e7 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -311,6 +311,7 @@ public final class CraftServer implements Server {
private final io.papermc.paper.potion.PaperPotionBrewer potionBrewer; // Paper - Custom Potion Mixes
public final io.papermc.paper.SparksFly spark; // Paper - spark
- private final org.leavesmc.leaves.entity.CraftBotManager botManager = new org.leavesmc.leaves.entity.CraftBotManager(); // Leaves
+ private final org.leavesmc.leaves.entity.CraftBotManager botManager; // Leaves
+ private final org.leavesmc.leaves.entity.CraftPhotographerManager photographerManager = new org.leavesmc.leaves.entity.CraftPhotographerManager(); // Leaves
// Paper start - Folia region threading API
@@ -412,7 +410,7 @@ index 59cdcf7b25c17705b613c83dea107934b683af28..b759025019120a0029c46506f1eede4e
@Override
public CraftPlayer apply(ServerPlayer player) {
return player.getBukkitEntity();
-@@ -3237,4 +3238,11 @@ public final class CraftServer implements Server {
+@@ -3238,4 +3239,11 @@ public final class CraftServer implements Server {
return botManager;
}
// Leaves end - Bot API
diff --git a/patches/server/0096-Leaves-I18n.patch b/patches/server/0096-Leaves-I18n.patch
index 66803d72..72724b25 100644
--- a/patches/server/0096-Leaves-I18n.patch
+++ b/patches/server/0096-Leaves-I18n.patch
@@ -51,10 +51,10 @@ index a9bca1d10553f1406f7dbce3f7c40378b6abdc10..aa7f4f738c637ffc3d50f2bdb5ee61ff
Builder builder = ImmutableMap.builder();
BiConsumer biConsumer = builder::put;
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-index a80b7dfb5de950d0ebacfc041f903ed3322f77c9..fc45960fb9fe03323b5f6e9733bf9c7665dba5b7 100644
+index e45acea31d61462c877c902b238209f14f2c8ad1..3e4d4c417904fba6aeba0dee2e01d3b5bd95ac20 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-@@ -240,6 +240,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+@@ -241,6 +241,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
org.leavesmc.leaves.LeavesConfig.init((java.io.File) options.valueOf("leaves-settings")); // Leaves - Server Config
System.setProperty("spark.serverconfigs.extra", "leaves.yml"); // Leaves - spark config
diff --git a/patches/server/0105-Disable-offline-warn-if-use-proxy.patch b/patches/server/0105-Disable-offline-warn-if-use-proxy.patch
index 6537bb09..197a5386 100644
--- a/patches/server/0105-Disable-offline-warn-if-use-proxy.patch
+++ b/patches/server/0105-Disable-offline-warn-if-use-proxy.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Disable offline warn if use proxy
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-index fc45960fb9fe03323b5f6e9733bf9c7665dba5b7..18f053785df042c41f88341bb3c0f647beb52ebe 100644
+index 3e4d4c417904fba6aeba0dee2e01d3b5bd95ac20..85e7321c7b918db404fca1c75a16ec17bc6b0b2f 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-@@ -302,7 +302,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+@@ -303,7 +303,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
String proxyFlavor = (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) ? "Velocity" : "BungeeCord";
String proxyLink = (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) ? "https://docs.papermc.io/velocity/security" : "http://www.spigotmc.org/wiki/firewall-guide/";
// Paper end - Add Velocity IP Forwarding Support
diff --git a/patches/server/0118-Fast-resume.patch b/patches/server/0118-Fast-resume.patch
index 37dcd90c..9f4ead29 100644
--- a/patches/server/0118-Fast-resume.patch
+++ b/patches/server/0118-Fast-resume.patch
@@ -59,18 +59,18 @@ index 58d3d1a47e9f2423c467bb329c2d5f4b58a8b5ef..ea1ffe6b5e49ccf2b472829ed97e977b
return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier);
}
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 49e5749d838883a9a292c283daba136b9629b8eb..ca0badf478777d840ddf730e2684f76d9cc596a9 100644
+index a598fcf9d67ec29668b36f70d6980831f7de2fea..d6c5b8ee987ba73643a88e4a9337a54bfa7f792f 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -748,6 +748,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop