9
0
mirror of https://github.com/Winds-Studio/Leaf.git synced 2025-12-19 15:09:25 +00:00
Files
Leaf/patches/server/0161-Leaves-Fakeplayer-support.patch
2024-12-17 05:05:24 -05:00

5204 lines
204 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: violetc <58360096+s-yh-china@users.noreply.github.com>
Date: Thu, 3 Feb 2022 12:28:15 +0800
Subject: [PATCH] Leaves: Fakeplayer support
Original license: GPLv3
Original project: https://github.com/LeavesMC/Leaves
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
index 30b56382e9574004e344c1c8289d7dcbb177386b..a737304bdb1a34912dcfc95fadf3a7c1e45e469f 100644
--- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
@@ -57,9 +57,13 @@ class PaperEventManager {
throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously.");
}
// Leaves start - skip photographer
- if (event instanceof org.bukkit.event.player.PlayerEvent playerEvent && playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.Photographer) {
+ // Leaves start - skip bot
+ if (event instanceof org.bukkit.event.player.PlayerEvent playerEvent
+ && (playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.Photographer || playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.Bot)
+ ) {
return;
}
+ // Leaves end - skip bot
// Leaves end - skip photographer
for (RegisteredListener registration : listeners) {
diff --git a/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java b/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java
index 35772110e9318df46a2729dbc0b5879b290011b7..f26989a44cdda9baabf337d573436c6c115c9884 100644
--- a/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java
+++ b/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java
@@ -39,6 +39,7 @@ public abstract class SimpleCriterionTrigger<T extends SimpleCriterionTrigger.Si
}
protected void trigger(ServerPlayer player, Predicate<T> predicate) {
+ if (player instanceof org.leavesmc.leaves.bot.ServerBot) return; // Leaves - bot skip
PlayerAdvancements playerAdvancements = player.getAdvancements();
Set<CriterionTrigger.Listener<T>> 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 652c25272832cf80fbd05eb7ac1090246e8cc638..b3e778940e0067d54555550f2cc91a9d0d9cb666 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<Packet<?>> {
@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 6b9b3d366343a0bef76036b2b150691787f93db6..f3b473ebbc7ed11f41bf3b9171c04401311baacd 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -312,6 +312,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
public gg.pufferfish.pufferfish.util.AsyncExecutor mobSpawnExecutor = new gg.pufferfish.pufferfish.util.AsyncExecutor("Leaf Async Mob Spawn Thread"); // Pufferfish - optimize mob spawning // Leaf - Unify thread name
public final Set<Entity> entitiesWithScheduledTasks = java.util.concurrent.ConcurrentHashMap.newKeySet(); // SparklyPaper - skip EntityScheduler's executeTick checks if there isn't any tasks to be run (concurrent because plugins may schedule tasks async)
private boolean waitingForNextTick = false; // Leaf - Fix MC-183518
+ private org.leavesmc.leaves.bot.BotList botList; // Leaves - fakeplayer
public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
AtomicReference<S> atomicreference = new AtomicReference();
@@ -739,6 +740,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
// Paper end - Configurable player collision
+ this.getBotList().loadResume(); // Leaves - load resident bot
+
this.server.enablePlugins(org.bukkit.plugin.PluginLoadOrder.POSTWORLD);
this.server.spark.registerCommandBeforePlugins(this.server); // Paper - spark
this.server.spark.enableAfterPlugins(this.server); // Paper - spark
@@ -1024,6 +1027,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
MinecraftServer.LOGGER.info("Stopping server");
Commands.COMMAND_SENDING_POOL.shutdownNow(); // Paper - Perf: Async command map building; Shutdown and don't bother finishing
+ this.getBotList().removeAll(); // Leaves - save or remove bot
this.server.spark.disable(); // Paper - spark
// Purpur start
if (upnp) {
@@ -1842,6 +1846,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked
this.getConnection().tick();
+ this.botList.networkTick(); // Leaves - fakeplayer
this.playerList.tick();
if (SharedConstants.IS_RUNNING_IN_IDE && this.tickRateManager.runsNormally()) {
GameTestTicker.SINGLETON.tick();
@@ -2972,6 +2977,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
return ServerLinks.EMPTY;
}
+ // Leaves start - fakeplayer
+ protected void setBotList(org.leavesmc.leaves.bot.BotList botList) {
+ this.botList = botList;
+ }
+
+ public org.leavesmc.leaves.bot.BotList getBotList() {
+ return botList;
+ }
+ // Leaves end - fakeplayer
+
public static record ReloadableResources(CloseableResourceManager resourceManager, ReloadableServerResources managers) implements AutoCloseable {
public void close() {
diff --git a/src/main/java/net/minecraft/server/PlayerAdvancements.java b/src/main/java/net/minecraft/server/PlayerAdvancements.java
index 5e46c9b294699f56a9f6f9e9cf43f2390760b54d..48c9f0497caac8515fb070133bd7a42d3052902e 100644
--- a/src/main/java/net/minecraft/server/PlayerAdvancements.java
+++ b/src/main/java/net/minecraft/server/PlayerAdvancements.java
@@ -223,9 +223,11 @@ public class PlayerAdvancements {
public boolean award(AdvancementHolder advancement, String criterionName) {
boolean flag = false;
// Leaves start - photographer can't get advancement
- if (player instanceof org.leavesmc.leaves.replay.ServerPhotographer) {
+ // Leaves start - bot can't get advancement
+ if (player instanceof org.leavesmc.leaves.replay.ServerPhotographer || player instanceof org.leavesmc.leaves.bot.ServerBot) {
return false;
}
+ // Leaves end - bot can't get advancement
// Leaves end - photographer can't get advancement
AdvancementProgress advancementprogress = this.getOrStartProgress(advancement);
boolean flag1 = advancementprogress.isDone();
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
index 5a88fd8997d2703a1318df441a2789a7b1ab229c..d4e8458b047704fd106daa7d7694ee3da8b3a154 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
@@ -217,6 +217,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
this.setLocalIp(dedicatedserverproperties.serverIp);
}
// Spigot start
+ this.setBotList(new org.leavesmc.leaves.bot.BotList(this)); // Leaves - fakeplayer
this.setPlayerList(new DedicatedPlayerList(this, this.registries(), this.playerDataStorage));
org.spigotmc.SpigotConfig.init((java.io.File) this.options.valueOf("spigot-settings"));
org.spigotmc.SpigotConfig.registerCommands();
@@ -251,8 +252,9 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command
org.galemc.gale.command.GaleCommands.registerCommands(this); // Gale - Gale commands - register commands
this.server.spark.registerCommandBeforePlugins(this.server); // Paper - spark
- com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics
com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now
+ org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.registerBot(); // Leaves - Leaf - Fakeplayer Support
+ com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics // Leaves - move down
// Gale start - Pufferfish - SIMD support
// Initialize vectorization
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index 58e79417e3722ce73cbbc1f9c74cbc73178f762d..2780db5ee87649bdada81bc74c9bf59962b069b3 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1389,6 +1389,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
} else if (this.seenBy.remove(player.connection)) {
this.serverEntity.removePairing(player);
+ // Leaves start - render bot
+ if (entity instanceof org.leavesmc.leaves.bot.ServerBot bot) {
+ if (bot.needSendFakeData(player)) {
+ bot.sendFakeData(player.connection, false);
+ }
+ }
+ // Leaves end - render bot
}
}
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index b93823983183c4ae1467a6df4f0b1fcfe60c815c..01acff57808667334e86a49f43ca9b880903991c 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -2074,6 +2074,12 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.
return this.players;
}
+ // Leaves start - fakeplayer skip
+ public List<ServerPlayer> realPlayers() {
+ return this.realPlayers;
+ }
+ // Leaves end - fakeplayer skip
+
@Override
public void onBlockStateChange(BlockPos pos, BlockState oldBlock, BlockState newBlock) {
Optional<Holder<PoiType>> optional = PoiTypes.forState(oldBlock);
@@ -2546,7 +2552,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.
if (entity instanceof ServerPlayer entityplayer) {
ServerLevel.this.players.add(entityplayer);
// Leaves start - skip
- if (!(entityplayer instanceof org.leavesmc.leaves.replay.ServerPhotographer)) {
+ if (!(entityplayer instanceof org.leavesmc.leaves.replay.ServerPhotographer) && !(entityplayer instanceof org.leavesmc.leaves.bot.ServerBot)) {
ServerLevel.this.realPlayers.add(entityplayer);
}
// Leaves end - skip
@@ -2632,7 +2638,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.
if (entity instanceof ServerPlayer entityplayer) {
ServerLevel.this.players.remove(entityplayer);
// Leaves start - skip
- if (!(entityplayer instanceof org.leavesmc.leaves.replay.ServerPhotographer)) {
+ if (!(entityplayer instanceof org.leavesmc.leaves.replay.ServerPhotographer) && !(entityplayer instanceof org.leavesmc.leaves.bot.ServerBot)) {
ServerLevel.this.realPlayers.remove(entityplayer);
}
// Leaves end - skip
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
index f24f7c3adad0eb31aee059ce71e4e1ad122945ed..90675e05f7a9f723f6c8259f493f505e215f1626 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -782,16 +782,20 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple
--this.invulnerableTime;
}
- // Paper start - Configurable container update tick rate
- if (--containerUpdateDelay <= 0) {
- this.containerMenu.broadcastChanges();
- containerUpdateDelay = this.level().paperConfig().tickRates.containerUpdate;
- }
- // Paper end - Configurable container update tick rate
- if (!this.level().isClientSide && this.containerMenu != this.inventoryMenu && (this.isImmobile() || !this.containerMenu.stillValid(this))) { // Paper - Prevent opening inventories when frozen
- this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.CANT_USE); // Paper - Inventory close reason
- this.containerMenu = this.inventoryMenu;
+ // Leaves start - skip bot
+ if (!(this instanceof org.leavesmc.leaves.bot.ServerBot)) {
+ // Paper start - Configurable container update tick rate
+ if (--containerUpdateDelay <= 0) {
+ this.containerMenu.broadcastChanges();
+ containerUpdateDelay = this.level().paperConfig().tickRates.containerUpdate;
+ }
+ // Paper end - Configurable container update tick rate
+ if (!this.level().isClientSide && this.containerMenu != this.inventoryMenu && (this.isImmobile() || !this.containerMenu.stillValid(this))) { // Paper - Prevent opening inventories when frozen
+ this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.CANT_USE); // Paper - Inventory close reason
+ this.containerMenu = this.inventoryMenu;
+ }
}
+ // Leaves end - skip bot
Entity entity = this.getCamera();
@@ -807,7 +811,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple
}
}
- CriteriaTriggers.TICK.trigger(this);
+ if (!(this instanceof org.leavesmc.leaves.bot.ServerBot)) CriteriaTriggers.TICK.trigger(this); // Leaves - skip bot
if (this.levitationStartPos != null) {
CriteriaTriggers.LEVITATION.trigger(this, this.levitationStartPos, this.tickCount - this.levitationStartTime);
}
@@ -1449,6 +1453,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.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerSupport) {
+ this.server.getBotList().bots.forEach(bot -> bot.sendFakeDataIfNeed(this, true)); // Leaves - render bot
+ }
+ // Leaves end - bot support
+
// CraftBukkit start
PlayerChangedWorldEvent changeEvent = new PlayerChangedWorldEvent(this.getBukkitEntity(), worldserver1.getWorld());
this.level().getCraftServer().getPluginManager().callEvent(changeEvent);
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
index 00b9d244898ffdc1584eb254643557776bf4a76f..ea61ca9a11ec86bb6df01bb8cc8726fda34fd9b0 100644
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
@@ -123,6 +123,8 @@ import org.bukkit.event.player.PlayerRespawnEvent.RespawnReason;
import org.bukkit.event.player.PlayerSpawnChangeEvent;
// CraftBukkit end
+import org.leavesmc.leaves.bot.ServerBot;
+
public abstract class PlayerList {
public static final File USERBANLIST_FILE = new File("banned-players.json");
@@ -238,6 +240,19 @@ public abstract class PlayerList {
// org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol
+ // Leaves start - bot support
+ if (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerSupport) {
+ ServerBot bot = this.server.getBotList().getBotByName(player.getScoreboardName());
+ if (bot != null) {
+ this.server.getBotList().removeBot(bot, org.leavesmc.leaves.event.bot.BotRemoveEvent.RemoveReason.INTERNAL, player.getBukkitEntity(), false);
+ }
+ this.server.getBotList().bots.forEach(bot1 -> {
+ bot1.sendPlayerInfo(player);
+ bot1.sendFakeDataIfNeed(player, true);
+ }); // Leaves - render bot
+ }
+ // Leaves end - bot support
+
final List<ServerPlayer> onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1);
for (int i = 0; i < this.players.size(); ++i) {
ServerPlayer entityplayer1 = this.players.get(i);
@@ -470,6 +485,19 @@ public abstract class PlayerList {
org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol
+ // Leaves start - bot support
+ if (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerSupport) {
+ ServerBot bot = this.server.getBotList().getBotByName(player.getScoreboardName());
+ if (bot != null) {
+ this.server.getBotList().removeBot(bot, org.leavesmc.leaves.event.bot.BotRemoveEvent.RemoveReason.INTERNAL, player.getBukkitEntity(), false);
+ }
+ this.server.getBotList().bots.forEach(bot1 -> {
+ bot1.sendPlayerInfo(player);
+ bot1.sendFakeDataIfNeed(player, true);
+ }); // Leaves - render bot
+ }
+ // Leaves end - bot support
+
final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage();
if (org.dreeam.leaf.config.modules.misc.ConnectionMessage.joinEnabled && jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure // Leaf - Configurable connection message - join message
@@ -1116,6 +1144,12 @@ public abstract class PlayerList {
}
// Paper end - Add PlayerPostRespawnEvent
+ // Leaves start - bot support
+ if (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerSupport) {
+ this.server.getBotList().bots.forEach(bot -> bot.sendFakeDataIfNeed(entityplayer1, true)); // Leaves - render bot
+ }
+ // Leaves end - bot support
+
// CraftBukkit end
return entityplayer1;
@@ -1308,11 +1342,16 @@ public abstract class PlayerList {
}
public String[] getPlayerNamesArray() {
- String[] astring = new String[this.players.size()];
+ 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) this.server.getBotList().bots.get(i - this.players.size())).getGameProfile().getName();
+ }
+ // Leaves end - fakeplayer support
return astring;
}
@@ -1421,7 +1460,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<Level> worldKey, Packet<?> packet) {
@@ -1767,7 +1812,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
}
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 732f7229ff9ea32ecdde94a1c34d3e763d08dd15..86bb9572dbe4fc21c21ed9159535051800b5329c 100644
--- a/src/main/java/net/minecraft/world/entity/Entity.java
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
@@ -1585,7 +1585,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
return offsetFactor;
}
- private Vec3 collide(Vec3 movement) {
+ public Vec3 collide(Vec3 movement) { // Leaves - private -> public
// Paper start - optimise collisions
final boolean xZero = movement.x == 0.0;
final boolean yZero = movement.y == 0.0;
diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java
index 865c83af67fee690e4d3a62c7c145c308e619ed1..a26db6d426fcffadfe80c3fbd2ccba2fb9b24dee 100644
--- a/src/main/java/net/minecraft/world/entity/player/Player.java
+++ b/src/main/java/net/minecraft/world/entity/player/Player.java
@@ -184,7 +184,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<GlobalPos> lastDeathLocation;
@Nullable
@@ -374,6 +374,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();
@@ -682,7 +688,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 5df85ba4904a6b4b69ab584e9f30d34c68925a5c..4241448a89630e6fbd0fad386f8badf6750dd315 100644
--- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
+++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
@@ -63,7 +63,7 @@ public class FishingHook extends Projectile {
public static final EntityDataAccessor<Integer> DATA_HOOKED_ENTITY = SynchedEntityData.defineId(FishingHook.class, EntityDataSerializers.INT);
private static final EntityDataAccessor<Boolean> DATA_BITING = SynchedEntityData.defineId(FishingHook.class, EntityDataSerializers.BOOLEAN);
private int life;
- private int nibble;
+ public int nibble; // Leaves - private -> public
public int timeUntilLured;
public int timeUntilHooked;
public float fishAngle;
diff --git a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java
index f1b4adeeb4dad5178a5e52870f420beaa8e13034..c40a5cfdf8014c831c908c1742dd38a312a5ca35 100644
--- a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java
+++ b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java
@@ -407,6 +407,8 @@ public abstract class AbstractContainerMenu {
ItemStack itemstack1;
int l;
+ if (!doClickCheck(slotIndex, button, actionType, player)) return; // Leaves - doClick check
+
if (actionType == ClickType.QUICK_CRAFT) {
int i1 = this.quickcraftStatus;
@@ -681,6 +683,22 @@ public abstract class AbstractContainerMenu {
}
+ // Leaves start - doClick check
+ private boolean doClickCheck(int slotIndex, int button, ClickType actionType, Player player) {
+ if (slotIndex < 0) {
+ return true;
+ }
+
+ Slot slot = getSlot(slotIndex);
+ ItemStack itemStack = slot.getItem();
+ net.minecraft.world.item.component.CustomData customData = itemStack.get(net.minecraft.core.component.DataComponents.CUSTOM_DATA);
+ if (customData != null && customData.contains("Leaves.Gui.Placeholder")) {
+ return !customData.copyTag().getBoolean("Leaves.Gui.Placeholder");
+ }
+ return true;
+ }
+ // Leaves end - doClick check
+
private boolean tryItemClickBehaviourOverride(Player player, ClickAction clickType, Slot slot, ItemStack stack, ItemStack cursorStack) {
FeatureFlagSet featureflagset = player.level().enabledFeatures();
diff --git a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java
index 976ac286b4a6b8a843d275583dacb4ca2b0c4cb2..94a24a053aa521ef5779c86ff44d082f3e17573c 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 {
ServerStatsCounter serverstatisticmanager = entityplayer.getStats();
int j = Mth.clamp(serverstatisticmanager.getValue(Stats.CUSTOM.get(Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE);
boolean flag2 = true;
+ // Leaves start - fakeplayer spawn
+ if (entityplayer instanceof 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 e825d9e573a38531f5a3b3f9cdccc24570953015..bcfbc19825a0d33199693033d10475c6e0ee027b 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/CraftRegionAccessor.java b/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java
index c9ecec5da937bc5458f69736b68ff6ae50aa5ebc..decaea842c557adecb9d2d6e654376f0508721bd 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java
@@ -428,6 +428,7 @@ public abstract class CraftRegionAccessor implements RegionAccessor {
@SuppressWarnings("unchecked")
public <T extends Entity> T addEntity(T entity) {
Preconditions.checkArgument(!entity.isInWorld(), "Entity has already been added to a world");
+ Preconditions.checkState(!(entity instanceof org.leavesmc.leaves.entity.CraftBot), "[Leaves] Fakeplayers do not support changing world, Please use leaves fakeplayer-api instead!");
net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle();
if (nmsEntity.level() != this.getHandle().getLevel()) {
nmsEntity = nmsEntity.changeDimension(new DimensionTransition(this.getHandle().getLevel(), nmsEntity, DimensionTransition.DO_NOTHING));
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index c7d806f7e2ddef2226be1efbe794f0da4c331615..6c1d8123088cf7646fa57fdcab2d3131ec4f6670 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.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; // Leaves
// Paper start - Folia region threading API
private final io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler();
@@ -494,6 +495,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) {
@@ -1118,6 +1120,7 @@ public final class CraftServer implements Server {
playerMetadata.removeAll(plugin);
}
// Paper end
+ org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.registerBot(); // Leaves - Leaf - Fakeplayer Support
this.reloadData();
org.spigotmc.SpigotConfig.registerCommands(); // Spigot
io.papermc.paper.command.PaperCommands.registerCommands(this.console); // Paper
@@ -1493,7 +1496,7 @@ public final class CraftServer implements Server {
return false;
}
- if (handle.players().size() > 0) {
+ if (handle.realPlayers().size() > 0) { // Leaves - skip
return false;
}
@@ -3389,4 +3392,10 @@ public final class CraftServer implements Server {
return photographerManager;
}
// Leaves end - replay mod api
+ // Leaves start - Bot API
+ @Override
+ public org.leavesmc.leaves.entity.CraftBotManager getBotManager() {
+ return botManager;
+ }
+ // Leaves end - Bot API
}
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index 617c386290e525e2e81fe0e7b6d1f0b16ef8d109..64141a3f2b299537b200e125e38edc427ad9be98 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -245,7 +245,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public int getPlayerCount() {
- return world.players().size();
+ return world.realPlayers().size(); // Leaves - skip
}
@Override
@@ -1295,9 +1295,9 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public List<Player> getPlayers() {
- List<Player> list = new ArrayList<Player>(this.world.players().size());
+ List<Player> list = new ArrayList<Player>(this.world.realPlayers().size()); // Leaves - skip
- for (net.minecraft.world.entity.player.Player human : this.world.players()) {
+ for (net.minecraft.world.entity.player.Player human : this.world.realPlayers()) { // Leaves - skip
HumanEntity bukkitEntity = human.getBukkitEntity();
if ((bukkitEntity != null) && (bukkitEntity instanceof Player)) {
@@ -1982,7 +1982,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
public void playSound(final net.kyori.adventure.sound.Sound sound) {
org.spigotmc.AsyncCatcher.catchOp("play sound"); // Paper
final long seed = sound.seed().orElseGet(this.world.getRandom()::nextLong);
- for (ServerPlayer player : this.getHandle().players()) {
+ for (ServerPlayer player : this.getHandle().realPlayers()) { // Leaves - skip
player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player.getX(), player.getY(), player.getZ(), seed, null));
}
}
@@ -1998,7 +1998,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
org.spigotmc.AsyncCatcher.catchOp("play sound"); // Paper
final long seed = sound.seed().orElseGet(this.getHandle().getRandom()::nextLong);
if (emitter == net.kyori.adventure.sound.Sound.Emitter.self()) {
- for (ServerPlayer player : this.getHandle().players()) {
+ for (ServerPlayer player : this.getHandle().realPlayers()) { // Leaves - skip
player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player, seed, null));
}
} else if (emitter instanceof CraftEntity craftEntity) {
@@ -2219,7 +2219,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
Preconditions.checkArgument(particle.getDataType().isInstance(data), "data (%s) should be %s", data.getClass(), particle.getDataType());
}
this.getHandle().sendParticles(
- receivers == null ? this.getHandle().players() : receivers.stream().map(player -> ((CraftPlayer) player).getHandle()).collect(java.util.stream.Collectors.toList()), // Paper - Particle API
+ receivers == null ? this.getHandle().realPlayers() : receivers.stream().map(player -> ((CraftPlayer) player).getHandle()).collect(java.util.stream.Collectors.toList()), // Paper - Particle API // Leaves - skip
sender != null ? ((CraftPlayer) sender).getHandle() : null, // Sender // Paper - Particle API
CraftParticle.createParticleParam(particle, data), // Particle
x, y, z, // Position
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
index b0058d6895b00c10d28113ae7e37223c9cd107db..6bec5601e760caf8918cad0d7146847f9266dd29 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
@@ -113,6 +113,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
}
if (entity instanceof org.leavesmc.leaves.replay.ServerPhotographer photographer) { return new org.leavesmc.leaves.entity.CraftPhotographer(server, photographer); } // Leaves - replay mod api
+ if (entity instanceof org.leavesmc.leaves.bot.ServerBot bot) { return new org.leavesmc.leaves.entity.CraftBot(server, bot); }
// Special case complex part, since there is no extra entity type for them
if (entity instanceof EnderDragonPart complexPart) {
diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
index 858c6c860d9b8aaa1d3f9f77a9e410726239d7cc..2b4c54f56986ad03dc222c4fddc816f5e7b5d404 100644
--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
@@ -1036,7 +1036,10 @@ public class CraftEventFactory {
event.setKeepInventory(keepInventory);
event.setKeepLevel(victim.keepLevel); // SPIGOT-2222: pre-set keepLevel
populateFields(victim, event); // Paper - make cancellable
- Bukkit.getServer().getPluginManager().callEvent(event);
+ // Leaves start - disable bot death event
+ if (!(victim instanceof org.leavesmc.leaves.bot.ServerBot)) {
+ Bukkit.getServer().getPluginManager().callEvent(event);
+ } // Leaves end
// Paper start - make cancellable
if (event.isCancelled()) {
return event;
diff --git a/src/main/java/org/dreeam/leaf/config/modules/gameplay/FakePlayerSupport.java b/src/main/java/org/dreeam/leaf/config/modules/gameplay/FakePlayerSupport.java
new file mode 100644
index 0000000000000000000000000000000000000000..e1344ccc0a550f52d424b943fcc94939adda370f
--- /dev/null
+++ b/src/main/java/org/dreeam/leaf/config/modules/gameplay/FakePlayerSupport.java
@@ -0,0 +1,77 @@
+package org.dreeam.leaf.config.modules.gameplay;
+
+import net.minecraft.server.MinecraftServer;
+import org.dreeam.leaf.config.ConfigModules;
+import org.dreeam.leaf.config.EnumConfigCategory;
+
+import org.bukkit.command.Command;
+
+import java.util.List;
+import java.util.Locale;
+
+public class FakePlayerSupport extends ConfigModules {
+
+ public String getBasePath() {
+ return EnumConfigCategory.GAMEPLAY.getBaseKeyName() + ".fakeplayer";
+ }
+
+ public static boolean fakeplayerSupport = true;
+ public static List<String> unableFakeplayerNames = List.of("player-name");
+ public static int fakeplayerLimit = 10;
+ public static String fakeplayerPrefix = "";
+ public static String fakeplayerSuffix = "";
+ public static boolean alwaysSendFakeplayerData = true;
+ public static boolean fakeplayerResident = false;
+ public static boolean openFakeplayerInventory = false;
+ public static boolean fakeplayerSkipSleep = false;
+ public static boolean fakeplayerSpawnPhantom = false;
+ public static double fakeplayerRegenAmount = 0.0;
+ public static boolean fakeplayerUseAction = true;
+ public static boolean fakeplayerModifyConfig = false;
+ public static boolean fakeplayerManualSaveAndLoad = false;
+ public static boolean fakeplayerCacheSkin = false;
+
+ @Override
+ public void onLoaded() {
+ fakeplayerSupport = config.getBoolean(getBasePath() + ".enable", fakeplayerSupport);
+ unableFakeplayerNames = config.getList(getBasePath() + ".unable-fakeplayer-names", unableFakeplayerNames);
+ fakeplayerLimit = config.getInt(getBasePath() + ".limit", fakeplayerLimit);
+ fakeplayerPrefix = config.getString(getBasePath() + ".prefix", fakeplayerPrefix);
+ fakeplayerSuffix = config.getString(getBasePath() + ".suffix", fakeplayerSuffix);
+ alwaysSendFakeplayerData = config.getBoolean(getBasePath() + ".always-send-data", alwaysSendFakeplayerData);
+ fakeplayerResident = config.getBoolean(getBasePath() + ".resident-fakeplayer", fakeplayerResident);
+ openFakeplayerInventory = config.getBoolean(getBasePath() + ".open-fakeplayer-inventory", openFakeplayerInventory);
+ fakeplayerSkipSleep = config.getBoolean(getBasePath() + ".skip-sleep-check", fakeplayerSkipSleep);
+ fakeplayerSpawnPhantom = config.getBoolean(getBasePath() + ".spawn-phantom", fakeplayerSpawnPhantom);
+ fakeplayerRegenAmount = config.getDouble(getBasePath() + ".regen-amount", fakeplayerRegenAmount);
+ fakeplayerUseAction = config.getBoolean(getBasePath() + ".use-action", fakeplayerUseAction);
+ fakeplayerModifyConfig = config.getBoolean(getBasePath() + ".modify-config", fakeplayerModifyConfig);
+ fakeplayerManualSaveAndLoad = config.getBoolean(getBasePath() + ".manual-save-and-load", fakeplayerManualSaveAndLoad);
+ fakeplayerCacheSkin = config.getBoolean(getBasePath() + ".cache-skin", fakeplayerCacheSkin);
+
+ if (fakeplayerRegenAmount < 0.0) {
+ throw new IllegalArgumentException("regen-amount need >= 0.0");
+ }
+ }
+
+ public static void registerBot() {
+ if (fakeplayerSupport) {
+ registerCommand("bot", new org.leavesmc.leaves.bot.BotCommand("bot"));
+ org.leavesmc.leaves.bot.agent.Actions.registerAll();
+ } else {
+ unregisterCommand("bot");
+ }
+ }
+
+ private static void registerCommand(String name, Command command) {
+ MinecraftServer.getServer().server.getCommandMap().register(name, "leaves", command);
+ MinecraftServer.getServer().server.syncCommands();
+ }
+
+ private static void unregisterCommand(String name) {
+ name = name.toLowerCase(Locale.ENGLISH).trim();
+ MinecraftServer.getServer().server.getCommandMap().getKnownCommands().remove(name);
+ MinecraftServer.getServer().server.getCommandMap().getKnownCommands().remove("leaves:" + name);
+ MinecraftServer.getServer().server.syncCommands();
+ }
+}
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..82b258f962067bed9b6d3302b65a8b6cfe1ba2ad
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/BotCommand.java
@@ -0,0 +1,543 @@
+package org.leavesmc.leaves.bot;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.ConsoleCommandSender;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.bukkit.generator.WorldInfo;
+import org.bukkit.permissions.Permission;
+import org.bukkit.permissions.PermissionDefault;
+import org.bukkit.plugin.PluginManager;
+import org.jetbrains.annotations.NotNull;
+import 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.command.LeavesCommandUtil;
+import org.leavesmc.leaves.entity.Bot;
+import org.leavesmc.leaves.event.bot.BotActionStopEvent;
+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 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) {
+ pluginManager.addPermission(new Permission("bukkit.command.bot", PermissionDefault.OP));
+ }
+ }
+
+ @Override
+ public @NotNull List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, String @NotNull [] args, Location location) throws IllegalArgumentException {
+ List<String> list = new ArrayList<>();
+ BotList botList = BotList.INSTANCE;
+
+ if (args.length <= 1) {
+ list.add("create");
+ list.add("remove");
+ if (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerUseAction) {
+ list.add("action");
+ }
+ if (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerModifyConfig) {
+ list.add("config");
+ }
+ if (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerManualSaveAndLoad) {
+ list.add("save");
+ list.add("load");
+ }
+ list.add("list");
+ }
+
+ if (args.length == 2) {
+ switch (args[0]) {
+ case "create" -> list.add("<BotName>");
+ 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());
+ }
+ }
+
+ if (args.length == 3) {
+ switch (args[0]) {
+ case "action" -> {
+ list.add("list");
+ list.add("stop");
+ list.addAll(Actions.getNames());
+ }
+ case "create" -> list.add("<BotSkinName>");
+ case "config" -> list.addAll(acceptConfig);
+ case "remove" -> list.addAll(List.of("cancel", "[hour]"));
+ }
+ }
+
+ 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 >= 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("config")) {
+ Configs<?> config = Configs.getConfig(args[2]);
+ if (config != null) {
+ list.addAll(config.config.getArgument().tabComplete(args.length - 4));
+ }
+ }
+
+ return LeavesCommandUtil.getListMatchingLast(sender, args, list, "bukkit.command.bot.", "bukkit.command.bot");
+ }
+
+ @Override
+ public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, String[] args) {
+ if (!testPermission(sender) || !org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerSupport) return true;
+
+ if (args.length == 0) {
+ 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(unknownMessage);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private void onCreate(CommandSender sender, String @NotNull [] args) {
+ if (args.length < 2) {
+ sender.sendMessage(text("Use /bot create <name> [skin_name] to create a fakeplayer", NamedTextColor.RED));
+ 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);
+
+ if (args.length >= 3) {
+ builder.skinName(args[2]);
+ }
+
+ 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);
+ }
+ }
+ }
+
+ 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(text("This name is illegal", NamedTextColor.RED));
+ return false;
+ }
+
+ if (Bukkit.getPlayerExact(name) != null || botList.getBotByName(name) != null) {
+ sender.sendMessage(text("This player is in server", NamedTextColor.RED));
+ return false;
+ }
+
+ if (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.unableFakeplayerNames.contains(name)) {
+ sender.sendMessage(text("This name is not allowed", NamedTextColor.RED));
+ return false;
+ }
+
+ if (botList.bots.size() >= org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerLimit) {
+ sender.sendMessage(text("Fakeplayer limit is full", NamedTextColor.RED));
+ return false;
+ }
+
+ return true;
+ }
+
+ private void onRemove(CommandSender sender, String @NotNull [] args) {
+ if (args.length < 2 || args.length > 5) {
+ sender.sendMessage(text("Use /bot remove <name> [hour] [minute] [second] to remove 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;
+ }
+
+ if (args.length > 2) {
+ if (args[2].equals("cancel")) {
+ if (bot.removeTaskId == -1) {
+ sender.sendMessage(text("This fakeplayer is not scheduled to be removed", NamedTextColor.RED));
+ return;
+ }
+ Bukkit.getScheduler().cancelTask(bot.removeTaskId);
+ bot.removeTaskId = -1;
+ sender.sendMessage(text("Remove cancel"));
+ return;
+ }
+
+ long time = 0;
+ int h; // Preventing out-of-range
+ long s = 0;
+ long m = 0;
+
+ try {
+ h = Integer.parseInt(args[2]);
+ if (h < 0) {
+ throw new NumberFormatException();
+ }
+ time += ((long) h) * 3600 * 20;
+ if (args.length > 3) {
+ m = Long.parseLong(args[3]);
+ if (m > 59 || m < 0) {
+ throw new NumberFormatException();
+ }
+ time += m * 60 * 20;
+ }
+ if (args.length > 4) {
+ s = Long.parseLong(args[4]);
+ if (s > 59 || s < 0) {
+ throw new NumberFormatException();
+ }
+ time += s * 20;
+ }
+ } catch (NumberFormatException e) {
+ sender.sendMessage(text("This fakeplayer is not scheduled to be removed", NamedTextColor.RED));
+ return;
+ }
+
+ boolean isReschedule = bot.removeTaskId != -1;
+
+ if (isReschedule) {
+ Bukkit.getScheduler().cancelTask(bot.removeTaskId);
+ }
+ bot.removeTaskId = Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> {
+ bot.removeTaskId = -1;
+ 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)" : ""));
+
+ return;
+ }
+
+ botList.removeBot(bot, BotRemoveEvent.RemoveReason.COMMAND, sender, false);
+ }
+
+ private void onAction(CommandSender sender, String @NotNull [] args) {
+ if (!org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerUseAction) {
+ return;
+ }
+
+ if (args.length < 3) {
+ sender.sendMessage(text("Use /bot action <name> <action> to make fakeplayer do action", NamedTextColor.RED));
+ return;
+ }
+
+ ServerBot bot = BotList.INSTANCE.getBotByName(args[1]);
+ if (bot == null) {
+ 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 (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")) {
+ Set<BotAction<?>> forRemoval = new HashSet<>();
+ for (int i = 0; i < bot.getBotActions().size(); i++) {
+ BotAction<?> action = bot.getBotActions().get(i);
+ BotActionStopEvent event = new BotActionStopEvent(
+ bot.getBukkitEntity(), action.getName(), action.getUUID(), BotActionStopEvent.Reason.COMMAND, sender
+ );
+ event.callEvent();
+ if (!event.isCancelled()) {
+ forRemoval.add(action);
+ }
+ }
+ bot.getBotActions().removeAll(forRemoval);
+ 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().get(i);
+ BotActionStopEvent event = new BotActionStopEvent(
+ bot.getBukkitEntity(), action.getName(), action.getUUID(), BotActionStopEvent.Reason.COMMAND, sender
+ );
+ event.callEvent();
+ if (!event.isCancelled()) {
+ 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]);
+ if (action == null) {
+ sender.sendMessage(text("Invalid action", NamedTextColor.RED));
+ return;
+ }
+
+ CraftPlayer player;
+ if (sender instanceof CraftPlayer) {
+ player = (CraftPlayer) sender;
+ } else {
+ player = bot.getBukkitEntity();
+ }
+
+ String[] realArgs = new String[args.length - 3];
+ if (realArgs.length != 0) {
+ System.arraycopy(args, 3, realArgs, 0, realArgs.length);
+ }
+
+ BotAction<?> newAction;
+ 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 e) {
+ sender.sendMessage(text("Action create error, please check your arguments, " + e.getMessage(), NamedTextColor.RED));
+ return;
+ }
+
+ if (newAction == null) {
+ return;
+ }
+
+ if (bot.addBotAction(newAction, sender)) {
+ sender.sendMessage("Action " + action.getName() + " has been issued to " + bot.getName().getString());
+ }
+ }
+
+ private static final List<String> acceptConfig = Configs.getConfigs().stream().map(config -> config.config.getName()).toList();
+
+ private void onConfig(CommandSender sender, String @NotNull [] args) {
+ if (!org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerModifyConfig) {
+ return;
+ }
+
+ if (args.length < 3) {
+ sender.sendMessage(text("Use /bot config <name> <config> to modify fakeplayer's config", NamedTextColor.RED));
+ return;
+ }
+
+ ServerBot bot = BotList.INSTANCE.getBotByName(args[1]);
+ if (bot == null) {
+ sender.sendMessage(text("This fakeplayer is not in server", NamedTextColor.RED));
+ return;
+ }
+
+ if (!acceptConfig.contains(args[2])) {
+ sender.sendMessage(text("This config is not accept", NamedTextColor.RED));
+ return;
+ }
+
+ BotConfig<?> config = Objects.requireNonNull(Configs.getConfig(args[2])).config;
+ if (args.length < 4) {
+ config.getMessage().forEach(sender::sendMessage);
+ } else {
+ String[] realArgs = new String[args.length - 3];
+ System.arraycopy(args, 3, realArgs, 0, realArgs.length);
+
+ BotConfigModifyEvent event = new BotConfigModifyEvent(bot.getBukkitEntity(), config.getName(), realArgs, sender);
+ Bukkit.getPluginManager().callEvent(event);
+
+ if (event.isCancelled()) {
+ return;
+ }
+ CommandArgumentResult result = config.getArgument().parse(0, realArgs);
+
+ try {
+ config.setValue(result);
+ config.getChangeMessage().forEach(sender::sendMessage);
+ } catch (IllegalArgumentException e) {
+ sender.sendMessage(text(e.getMessage(), NamedTextColor.RED));
+ }
+ }
+ }
+
+ private void onSave(CommandSender sender, String @NotNull [] args) {
+ if (!org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerManualSaveAndLoad) {
+ return;
+ }
+
+ if (args.length < 2) {
+ sender.sendMessage(text("Use /bot save <name> 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;
+ }
+
+ if (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 (!org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerManualSaveAndLoad) {
+ return;
+ }
+
+ if (args.length < 2) {
+ sender.sendMessage(text("Use /bot save <name> 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<World, List<String>> botMap = new HashMap<>();
+ for (World world : Bukkit.getWorlds()) {
+ botMap.put(world, new ArrayList<>());
+ }
+
+ for (ServerBot bot : botList.bots) {
+ Bot bukkitBot = bot.getBukkitEntity();
+ botMap.get(bukkitBot.getWorld()).add(bukkitBot.getName());
+ }
+
+ sender.sendMessage("Total number: (" + botList.bots.size() + "/" + org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerLimit + ")");
+ for (World world : botMap.keySet()) {
+ sender.sendMessage(world.getName() + "(" + botMap.get(world).size() + "): " + formatPlayerNameList(botMap.get(world)));
+ }
+ } else {
+ World world = Bukkit.getWorld(args[1]);
+
+ if (world == null) {
+ sender.sendMessage(text("Unknown world", NamedTextColor.RED));
+ return;
+ }
+
+ List<String> snowBotList = new ArrayList<>();
+ for (ServerBot bot : botList.bots) {
+ Bot bukkitBot = bot.getBukkitEntity();
+ if (bukkitBot.getWorld() == world) {
+ snowBotList.add(bukkitBot.getName());
+ }
+ }
+
+ sender.sendMessage(world.getName() + "(" + botList.bots.size() + "): " + formatPlayerNameList(snowBotList));
+ }
+ }
+
+ @NotNull
+ private static String formatPlayerNameList(@NotNull List<String> list) {
+ if (list.isEmpty()) {
+ return "";
+ }
+ String string = list.toString();
+ return string.substring(1, string.length() - 1);
+ }
+}
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..480ee25e813cdbeacfb551e5e8f66272a77b77b6
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/BotCreateState.java
@@ -0,0 +1,119 @@
+package org.leavesmc.leaves.bot;
+
+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.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 java.util.Objects;
+import java.util.function.Consumer;
+
+public record BotCreateState(String realName, String name, String skinName, String[] skin, Location location, BotCreateEvent.CreateReason createReason, CommandSender creator) {
+
+ private static final MinecraftServer server = MinecraftServer.getServer();
+
+ public ServerBot createNow() {
+ return server.getBotList().createNewBot(this);
+ }
+
+ @NotNull
+ public static Builder builder(@NotNull String realName, @Nullable Location location) {
+ return new Builder(realName, location);
+ }
+
+ 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 = org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerPrefix + realName + org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerSuffix;
+ this.skinName = this.realName;
+ this.skin = null;
+ this.createReason = BotCreateEvent.CreateReason.UNKNOWN;
+ this.creator = null;
+ }
+
+ public Builder name(@NotNull String name) {
+ Objects.requireNonNull(name);
+ this.name = name;
+ return this;
+ }
+
+ public Builder skinName(@Nullable String skinName) {
+ this.skinName = skinName;
+ return this;
+ }
+
+ public Builder skin(@Nullable String[] skin) {
+ this.skin = skin;
+ return this;
+ }
+
+ public Builder mojangAPISkin() {
+ if (this.skinName != null) {
+ this.skin = MojangAPI.getSkin(this.skinName);
+ }
+ 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<Bot> 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<CompoundTag> load(Player player) {
+ return this.load(player.getScoreboardName(), player.getStringUUID()).map((nbt) -> {
+ player.load(nbt);
+ return nbt;
+ });
+ }
+
+ private Optional<CompoundTag> load(String name, String uuid) {
+ File file = new File(this.botDir, uuid + ".dat");
+
+ if (file.exists() && file.isFile()) {
+ try {
+ Optional<CompoundTag> 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<ItemStack> items;
+ public final NonNullList<ItemStack> armor;
+ public final NonNullList<ItemStack> offhand;
+ private final List<NonNullList<ItemStack>> compartments;
+ private final NonNullList<ItemStack> 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<NonNullList<ItemStack>, Integer> pair = getItemSlot(slot);
+ if (pair != null) {
+ return pair.getFirst().get(pair.getSecond());
+ } else {
+ return ItemStack.EMPTY;
+ }
+ }
+
+ public Pair<NonNullList<ItemStack>, Integer> getItemSlot(int slot) {
+ switch (slot) {
+ case 0 -> {
+ return new Pair<>(buttons, 0);
+ }
+ case 1, 2, 3, 4 -> {
+ return new Pair<>(armor, 4 - slot);
+ }
+ case 5, 6 -> {
+ return new Pair<>(buttons, slot - 4);
+ }
+ case 7 -> {
+ return new Pair<>(offhand, 0);
+ }
+ case 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 -> {
+ return new Pair<>(buttons, slot - 5);
+ }
+ case 18, 19, 20, 21, 22, 23, 24, 25, 26,
+ 27, 28, 29, 30, 31, 32, 33, 34, 35,
+ 36, 37, 38, 39, 40, 41, 42, 43, 44 -> {
+ return new Pair<>(items, slot - 9);
+ }
+ case 45, 46, 47, 48, 49, 50, 51, 52, 53 -> {
+ return new Pair<>(items, slot - 45);
+ }
+ default -> {
+ return null;
+ }
+ }
+ }
+
+ @Override
+ @Nonnull
+ public ItemStack removeItem(int slot, int amount) {
+ Pair<NonNullList<ItemStack>, Integer> pair = getItemSlot(slot);
+ NonNullList<ItemStack> list = null;
+ ItemStack itemStack = ItemStack.EMPTY;
+ if (pair != null) {
+ list = pair.getFirst();
+ slot = pair.getSecond();
+ }
+ if (list != null && !list.get(slot).isEmpty()) {
+ itemStack = ContainerHelper.removeItem(list, slot, amount);
+ player.detectEquipmentUpdatesPublic();
+ }
+ return itemStack;
+ }
+
+ @Override
+ @Nonnull
+ public ItemStack removeItemNoUpdate(int slot) {
+ Pair<NonNullList<ItemStack>, Integer> pair = getItemSlot(slot);
+ NonNullList<ItemStack> list = null;
+ if (pair != null) {
+ list = pair.getFirst();
+ slot = pair.getSecond();
+ }
+ if (list != null && !list.get(slot).isEmpty()) {
+ ItemStack itemStack = list.get(slot);
+ list.set(slot, ItemStack.EMPTY);
+ return itemStack;
+ }
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public void setItem(int slot, @Nonnull ItemStack stack) {
+ Pair<NonNullList<ItemStack>, Integer> pair = getItemSlot(slot);
+ NonNullList<ItemStack> list = null;
+ if (pair != null) {
+ list = pair.getFirst();
+ slot = pair.getSecond();
+ }
+ if (list != null) {
+ list.set(slot, stack);
+ player.detectEquipmentUpdatesPublic();
+ }
+ }
+
+ @Override
+ public void setChanged() {
+ }
+
+ @Override
+ public boolean stillValid(@Nonnull Player player) {
+ if (this.player.isRemoved()) {
+ return false;
+ }
+ return !(player.distanceToSqr(this.player) > 64.0);
+ }
+
+ @Override
+ public void clearContent() {
+ for (List<ItemStack> list : this.compartments) {
+ list.clear();
+ }
+ }
+
+ private void createButton() {
+ CompoundTag customData = new CompoundTag();
+ customData.putBoolean("Leaves.Gui.Placeholder", true);
+
+ DataComponentPatch patch = DataComponentPatch.builder()
+ .set(DataComponents.CUSTOM_NAME, Component.empty())
+ .set(DataComponents.CUSTOM_DATA, CustomData.of(customData))
+ .build();
+
+ for (int i = 0; i < 13; i++) {
+ ItemStack button = new ItemStack(Items.STRUCTURE_VOID);
+ button.applyComponents(patch);
+ buttons.set(i, button);
+ }
+ }
+}
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..859c233379a03e1ffb1aa1552996e558edc7bf1f
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/BotList.java
@@ -0,0 +1,339 @@
+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.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<ServerBot> bots = new CopyOnWriteArrayList<>();
+ private final BotDataStorage dataStorage;
+
+ private final Map<UUID, ServerBot> botsByUUID = Maps.newHashMap();
+ private final Map<String, ServerBot> 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<CompoundTag> optional = playerIO.load(bot);
+
+ if (optional.isEmpty()) {
+ return null;
+ }
+
+ ResourceKey<Level> 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<CompoundTag> 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<Entity> 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 boolean removeBot(@NotNull ServerBot bot, @NotNull BotRemoveEvent.RemoveReason reason, @Nullable CommandSender remover, boolean saved) {
+ return this.removeBot(bot, reason, remover, saved, this.dataStorage);
+ }
+
+ public boolean 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 event.isCancelled();
+ }
+
+ 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);
+ }
+ return true;
+ }
+
+ public void removeAll() {
+ for (ServerBot bot : this.bots) {
+ bot.resume = org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerResident;
+ this.removeBot(bot, BotRemoveEvent.RemoveReason.INTERNAL, null, org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerResident);
+ }
+ }
+
+ public void loadResume() {
+ if (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerSupport && org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerResident) {
+ CompoundTag savedBotList = this.getSavedBotList().copy();
+ for (String realName : savedBotList.getAllKeys()) {
+ CompoundTag nbt = savedBotList.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 (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.unableFakeplayerNames.contains(name)) {
+ return false;
+ }
+
+ return this.bots.size() < org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.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/BotUtil.java b/src/main/java/org/leavesmc/leaves/bot/BotUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..78414d1f53328cdc2963264ecb4f5a65e9783798
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/BotUtil.java
@@ -0,0 +1,73 @@
+package org.leavesmc.leaves.bot;
+
+import com.google.common.base.Charsets;
+import net.minecraft.core.NonNullList;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.item.ItemStack;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public class BotUtil {
+
+ public static void replenishment(@NotNull ItemStack itemStack, NonNullList<ItemStack> itemStackList) {
+ int count = itemStack.getMaxStackSize() / 2;
+ if (itemStack.getCount() <= 8 && count > 8) {
+ for (ItemStack itemStack1 : itemStackList) {
+ if (itemStack1 == ItemStack.EMPTY || itemStack1 == itemStack) {
+ continue;
+ }
+
+ if (ItemStack.isSameItemSameComponents(itemStack1, itemStack)) {
+ if (itemStack1.getCount() > count) {
+ itemStack.setCount(itemStack.getCount() + count);
+ itemStack1.setCount(itemStack1.getCount() - count);
+ } else {
+ itemStack.setCount(itemStack.getCount() + itemStack1.getCount());
+ itemStack1.setCount(0);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static void replaceTool(@NotNull EquipmentSlot slot, @NotNull ServerBot bot) {
+ ItemStack itemStack = bot.getItemBySlot(slot);
+ for (int i = 0; i < 36; i++) {
+ ItemStack itemStack1 = bot.getInventory().getItem(i);
+ if (itemStack1 == ItemStack.EMPTY || itemStack1 == itemStack) {
+ continue;
+ }
+
+ if (itemStack1.getItem().getClass() == itemStack.getItem().getClass() && !isDamage(itemStack1, 10)) {
+ ItemStack itemStack2 = itemStack1.copy();
+ bot.getInventory().setItem(i, itemStack);
+ bot.setItemSlot(slot, itemStack2);
+ return;
+ }
+ }
+
+ for (int i = 0; i < 36; i++) {
+ ItemStack itemStack1 = bot.getInventory().getItem(i);
+ if (itemStack1 == ItemStack.EMPTY && itemStack1 != itemStack) {
+ bot.getInventory().setItem(i, itemStack);
+ bot.setItemSlot(slot, ItemStack.EMPTY);
+ return;
+ }
+ }
+ }
+
+ public static boolean isDamage(@NotNull ItemStack item, int minDamage) {
+ return item.isDamageableItem() && (item.getMaxDamage() - item.getDamageValue()) <= minDamage;
+ }
+
+ @NotNull
+ public static UUID getBotUUID(@NotNull BotCreateState state) {
+ return getBotUUID(state.realName());
+ }
+
+ 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;
+
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.world.entity.player.Player;
+
+import java.util.Optional;
+
+public interface IPlayerDataStorage {
+
+ void save(Player player);
+
+ Optional<CompoundTag> 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..8c3dfb7e30b6bfae141fd0bbef666b48d4775915
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/MojangAPI.java
@@ -0,0 +1,38 @@
+package org.leavesmc.leaves.bot;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+public class MojangAPI {
+
+ private static final Map<String, String[]> CACHE = new HashMap<>();
+
+ public static String[] getSkin(String name) {
+ if (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerCacheSkin && CACHE.containsKey(name)) {
+ return CACHE.get(name);
+ }
+
+ String[] values = pullFromAPI(name);
+ CACHE.put(name, values);
+ return values;
+ }
+
+ // Laggggggggggggggggggggggggggggggggggggggggg
+ public static String[] pullFromAPI(String name) {
+ try {
+ String uuid = 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..b606a1b7f13a129089848f89a756f21ad8b81a61
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/ServerBot.java
@@ -0,0 +1,547 @@
+package org.leavesmc.leaves.bot;
+
+import com.google.common.collect.ImmutableMap;
+import com.mojang.authlib.GameProfile;
+import io.papermc.paper.adventure.PaperAdventure;
+import io.papermc.paper.event.entity.EntityKnockbackEvent;
+import net.minecraft.core.BlockPos;
+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.game.ClientboundPlayerInfoRemovePacket;
+import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
+import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket;
+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.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.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.phys.EntityHitResult;
+import net.minecraft.world.phys.Vec3;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.command.CommandSender;
+import org.bukkit.util.Vector;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+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.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.BotRemoveEvent;
+import org.leavesmc.leaves.plugin.MinecraftInternalPlugin;
+import org.leavesmc.leaves.util.MathUtils;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.function.Predicate;
+
+// TODO test
+public class ServerBot extends ServerPlayer {
+
+ private final Map<Configs<?>, BotConfig<?>> configs;
+ private final List<BotAction<?>> actions;
+
+ public boolean resume = false;
+ public BotCreateState createState;
+ public UUID createPlayer;
+
+ private final int tracingRange;
+ private final ServerStatsCounter stats;
+ private final BotInventoryContainer container;
+
+ public int notSleepTicks;
+
+ public int removeTaskId = -1;
+
+ private Vec3 knockback = Vec3.ZERO;
+
+ public ServerBot(MinecraftServer server, ServerLevel world, GameProfile profile) {
+ super(server, world, profile, ClientInformation.createDefault());
+ this.entityData.set(Player.DATA_PLAYER_MODE_CUSTOMISATION, (byte) -2);
+
+ this.gameMode = new ServerBotGameMode(this);
+ this.actions = new ArrayList<>();
+
+ ImmutableMap.Builder<Configs<?>, BotConfig<?>> configBuilder = ImmutableMap.builder();
+ for (Configs<?> config : Configs.getConfigs()) {
+ configBuilder.put(config, config.config.create(this));
+ }
+ this.configs = configBuilder.build();
+
+ this.stats = new BotStatsCounter(server);
+ this.container = new BotInventoryContainer(this);
+ this.tracingRange = world.spigotConfig.playerTrackingRange * world.spigotConfig.playerTrackingRange;
+
+ this.notSleepTicks = 0;
+ this.fauxSleeping = org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerSkipSleep;
+ }
+
+ public void sendPlayerInfo(ServerPlayer player) {
+ player.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED, ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME), List.of(this)));
+ }
+
+ public boolean needSendFakeData(ServerPlayer player) {
+ return this.getConfigValue(Configs.ALWAYS_SEND_DATA) && (player.level() == this.level() && player.position().distanceToSqr(this.position()) > this.tracingRange);
+ }
+
+ public void sendFakeDataIfNeed(ServerPlayer player, boolean login) {
+ if (needSendFakeData(player)) {
+ this.sendFakeData(player.connection, login);
+ }
+ }
+
+ public void sendFakeData(ServerPlayerConnection playerConnection, boolean login) {
+ ChunkMap.TrackedEntity entityTracker = ((ServerLevel) this.level()).getChunkSource().chunkMap.entityMap.get(this.getId());
+
+ if (entityTracker == null) {
+ LeavesLogger.LOGGER.warning("Fakeplayer cant get entity tracker for " + this.getId());
+ return;
+ }
+
+ playerConnection.send(this.getAddEntityPacket(entityTracker.serverEntity));
+ if (login) {
+ 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)));
+ }
+ }
+
+ public void renderAll() {
+ this.server.getPlayerList().getPlayers().forEach(
+ player -> {
+ this.sendPlayerInfo(player);
+ this.sendFakeDataIfNeed(player, false);
+ }
+ );
+ }
+
+ private void sendPacket(Packet<?> packet) {
+ this.server.getPlayerList().getPlayers().forEach(player -> player.connection.send(packet));
+ }
+
+ @Override
+ public void die(@NotNull DamageSource damageSource) {
+ boolean flag = this.level().getGameRules().getBoolean(GameRules.RULE_SHOWDEATHMESSAGES);
+ Component defaultMessage = this.getCombatTracker().getDeathMessage();
+
+ 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.gameEvent(GameEvent.ENTITY_DIE);
+
+ 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);
+ }
+
+ this.server.getBotList().removeBot(this, BotRemoveEvent.RemoveReason.DEATH, null, false);
+ }
+
+ public void removeTab() {
+ this.sendPacket(new ClientboundPlayerInfoRemovePacket(List.of(this.getUUID())));
+ }
+
+ @Nullable
+ @Override
+ public Entity changeDimension(@NotNull DimensionTransition teleportTarget) {
+ return this; // disable dimension change
+ }
+
+ @Override
+ public void handlePortal() {
+ }
+
+ @Override
+ public void tick() {
+ if (!this.isAlive()) {
+ return;
+ }
+ super.tick();
+
+ if (this.getConfigValue(Configs.SPAWN_PHANTOM)) {
+ notSleepTicks++;
+ }
+
+ if (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerRegenAmount > 0.0 && server.getTickCount() % 20 == 0) {
+ float health = getHealth();
+ float maxHealth = getMaxHealth();
+ float regenAmount = (float) (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerRegenAmount * 20);
+ float amount;
+
+ if (health < maxHealth - regenAmount) {
+ amount = health + regenAmount;
+ } else {
+ amount = maxHealth;
+ }
+
+ this.setHealth(amount);
+ }
+ }
+
+ @Override
+ public void onItemPickup(@NotNull ItemEntity item) {
+ super.onItemPickup(item);
+ this.updateItemInHand(InteractionHand.MAIN_HAND);
+ }
+
+ public void updateItemInHand(InteractionHand hand) {
+ ItemStack item = this.getItemInHand(hand);
+
+ if (!item.isEmpty()) {
+ BotUtil.replenishment(item, getInventory().items);
+ if (BotUtil.isDamage(item, 10)) {
+ BotUtil.replaceTool(hand == InteractionHand.MAIN_HAND ? EquipmentSlot.MAINHAND : EquipmentSlot.OFFHAND, this);
+ }
+ }
+ this.detectEquipmentUpdatesPublic();
+ }
+
+ @Override
+ public @NotNull InteractionResult interact(@NotNull Player player, @NotNull InteractionHand hand) {
+ if (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.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);
+ this.level().gameEvent(GameEvent.HIT_GROUND, this.position(), GameEvent.Context.of(this, this.mainSupportingBlockPos.map((blockposition1) -> {
+ return this.level().getBlockState(blockposition1);
+ }).orElse(state)));
+ }
+
+ this.resetFallDistance();
+ } else if (heightDifference < 0.0D) {
+ this.fallDistance -= (float) heightDifference;
+ }
+ }
+
+ @Override
+ public void doTick() {
+ this.absMoveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot());
+
+ if (this.takeXpDelay > 0) {
+ --this.takeXpDelay;
+ }
+
+ if (this.isSleeping()) {
+ ++this.sleepCounter;
+ if (this.sleepCounter > 100) {
+ this.sleepCounter = 100;
+ this.notSleepTicks = 0;
+ }
+
+ 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;
+ }
+ }
+
+ this.updateIsUnderwater();
+
+ this.addDeltaMovement(knockback);
+ this.knockback = Vec3.ZERO;
+
+ this.server.tell(this.server.wrapRunnable(this::runAction));
+
+ this.livingEntityTick();
+
+ this.foodData.tick(this);
+
+ ++this.attackStrengthTicker;
+ ItemStack itemstack = this.getMainHandItem();
+ if (!ItemStack.matches(this.lastItemInMainHand, itemstack)) {
+ if (!ItemStack.isSameItem(this.lastItemInMainHand, itemstack)) {
+ this.resetAttackStrengthTicker();
+ }
+
+ this.lastItemInMainHand = itemstack.copy();
+ }
+
+ this.getCooldowns().tick();
+ this.updatePlayerPose();
+
+ if (this.hurtTime > 0) {
+ this.hurtTime -= 1;
+ }
+ }
+
+ @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) {
+ 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);
+ }
+ }
+
+ @Override
+ public void setRot(float yaw, float pitch) {
+ this.getBukkitEntity().setRotation(yaw, pitch);
+ }
+
+ @Override
+ public void attack(@NotNull Entity target) {
+ super.attack(target);
+ this.swing(InteractionHand.MAIN_HAND);
+ }
+
+ @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));
+ }
+ createNbt.put("skin", skin);
+ }
+
+ nbt.put("createStatus", createNbt);
+
+ if (!this.actions.isEmpty()) {
+ ListTag actionNbt = new ListTag();
+ for (BotAction<?> action : this.actions) {
+ actionNbt.add(action.save(new CompoundTag()));
+ }
+ 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);
+ }
+ }
+
+ @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);
+ }
+ }
+
+ createBuilder.skinName(createNbt.getString("skinName")).skin(skin);
+ createBuilder.createReason(BotCreateEvent.CreateReason.INTERNAL).creator(null);
+
+ this.createState = createBuilder.build();
+ this.gameProfile = new BotList.CustomGameProfile(this.getUUID(), this.createState.name(), this.createState.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);
+ }
+ }
+ }
+
+ 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 void faceLocation(@NotNull Location loc) {
+ this.look(loc.toVector().subtract(getLocation().toVector()), false);
+ }
+
+ public void look(Vector dir, boolean keepYaw) {
+ float yaw, pitch;
+
+ if (keepYaw) {
+ yaw = this.getYHeadRot();
+ pitch = MathUtils.fetchPitch(dir);
+ } else {
+ float[] vals = MathUtils.fetchYawPitch(dir);
+ yaw = vals[0];
+ pitch = vals[1];
+
+ this.sendPacket(new ClientboundRotateHeadPacket(this, (byte) (yaw * 256 / 360f)));
+ }
+
+ this.setRot(yaw, pitch);
+ }
+
+ public Location getLocation() {
+ return this.getBukkitEntity().getLocation();
+ }
+
+ public Entity getTargetEntity(int maxDistance, Predicate<? super Entity> predicate) {
+ List<Entity> 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;
+ }
+
+ public void dropAll() {
+ this.getInventory().dropAll();
+ this.detectEquipmentUpdatesPublic();
+ }
+
+ private void runAction() {
+ if (org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerUseAction) {
+ this.actions.forEach(action -> action.tryTick(this));
+ this.actions.removeIf(BotAction::isCancelled);
+ }
+ }
+
+ public boolean addBotAction(BotAction<?> action, CommandSender sender) {
+ if (!org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.fakeplayerUseAction) {
+ return false;
+ }
+
+ if (!new BotActionScheduleEvent(this.getBukkitEntity(), action.getName(), action.getUUID(), sender).callEvent()) {
+ return false;
+ }
+
+ action.init();
+ this.actions.add(action);
+ return true;
+ }
+
+ public List<BotAction<?>> getBotActions() {
+ return actions;
+ }
+
+ @Override
+ public @NotNull ServerStatsCounter getStats() {
+ return stats;
+ }
+
+ @SuppressWarnings("unchecked")
+ public <E> BotConfig<E> getConfig(Configs<E> config) {
+ return (BotConfig<E>) Objects.requireNonNull(this.configs.get(config));
+ }
+
+ public <E> E getConfigValue(Configs<E> 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..bc1e29f6080c4783940848456620be8c06c32cce
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java
@@ -0,0 +1,138 @@
+package org.leavesmc.leaves.bot;
+
+import net.kyori.adventure.text.Component;
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.ServerPlayerGameMode;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.InteractionResult;
+import net.minecraft.world.ItemInteractionResult;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.context.UseOnContext;
+import net.minecraft.world.level.GameType;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.phys.BlockHitResult;
+import org.bukkit.event.player.PlayerGameModeChangeEvent;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class ServerBotGameMode extends ServerPlayerGameMode {
+
+ public ServerBotGameMode(ServerBot bot) {
+ super(bot);
+ super.setGameModeForPlayer(GameType.SURVIVAL, null);
+ }
+
+ @Override
+ public boolean changeGameModeForPlayer(@NotNull GameType gameMode) {
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public PlayerGameModeChangeEvent changeGameModeForPlayer(@NotNull GameType gameMode, PlayerGameModeChangeEvent.@NotNull Cause cause, @Nullable Component cancelMessage) {
+ return null;
+ }
+
+ @Override
+ protected void setGameModeForPlayer(@NotNull GameType gameMode, @Nullable GameType previousGameMode) {
+ }
+
+ @Override
+ public void tick() {
+ }
+
+ @Override
+ public void destroyAndAck(@NotNull BlockPos pos, int sequence, @NotNull String reason) {
+ this.destroyBlock(pos);
+ }
+
+ @Override
+ public boolean destroyBlock(@NotNull BlockPos pos) {
+ BlockState iblockdata = this.level.getBlockState(pos);
+ BlockEntity tileentity = this.level.getBlockEntity(pos);
+ Block block = iblockdata.getBlock();
+
+ if (this.player.blockActionRestricted(this.level, pos, this.getGameModeForPlayer())) {
+ return false;
+ } else {
+ this.level.captureDrops = null;
+ BlockState iblockdata1 = block.playerWillDestroy(this.level, pos, iblockdata, this.player);
+ boolean flag = this.level.removeBlock(pos, false);
+
+ if (flag) {
+ block.destroy(this.level, pos, iblockdata1);
+ }
+
+ ItemStack itemstack = this.player.getMainHandItem();
+ ItemStack itemstack1 = itemstack.copy();
+
+ boolean flag1 = this.player.hasCorrectToolForDrops(iblockdata1);
+
+ itemstack.mineBlock(this.level, iblockdata1, pos, this.player);
+ if (flag && flag1) {
+ Block.dropResources(iblockdata1, this.level, pos, tileentity, this.player, itemstack1, true);
+ }
+
+ if (flag) {
+ iblockdata.getBlock().popExperience(this.level, pos, block.getExpDrop(iblockdata, this.level, pos, itemstack, true), this.player);
+ }
+
+ return true;
+ }
+ }
+
+ @NotNull
+ @Override
+ public InteractionResult useItemOn(@NotNull ServerPlayer player, Level world, @NotNull ItemStack stack, @NotNull InteractionHand hand, BlockHitResult hitResult) {
+ BlockPos blockposition = hitResult.getBlockPos();
+ BlockState iblockdata = world.getBlockState(blockposition);
+ InteractionResult enuminteractionresult = InteractionResult.PASS;
+
+ if (!iblockdata.getBlock().isEnabled(world.enabledFeatures())) {
+ return InteractionResult.FAIL;
+ }
+
+ if (player.getCooldowns().isOnCooldown(stack.getItem())) {
+ return InteractionResult.PASS;
+ }
+
+ this.firedInteract = true;
+ this.interactResult = false;
+ this.interactPosition = blockposition.immutable();
+ this.interactHand = hand;
+ this.interactItemStack = stack.copy();
+
+ boolean flag = !player.getMainHandItem().isEmpty() || !player.getOffhandItem().isEmpty();
+ boolean flag1 = player.isSecondaryUseActive() && flag;
+
+ if (!flag1) {
+ ItemInteractionResult iteminteractionresult = iblockdata.useItemOn(player.getItemInHand(hand), world, player, hand, hitResult);
+
+ if (iteminteractionresult.consumesAction()) {
+ return iteminteractionresult.result();
+ }
+
+ if (iteminteractionresult == ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION && hand == InteractionHand.MAIN_HAND) {
+ enuminteractionresult = iblockdata.useWithoutItem(world, player, hitResult);
+ if (enuminteractionresult.consumesAction()) {
+ return enuminteractionresult;
+ }
+ }
+ }
+
+ if (!stack.isEmpty() && enuminteractionresult != InteractionResult.SUCCESS && !this.interactResult) {
+ UseOnContext itemactioncontext = new UseOnContext(player, hand, hitResult);
+ return stack.useOn(itemactioncontext);
+ }
+ return enuminteractionresult;
+ }
+
+ @Override
+ 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..a37513e1ba8443c702ab0c01fbe5e052e5f0f2ab
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/Actions.java
@@ -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;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class Actions {
+
+ private static final Map<String, BotAction<?>> actions = new HashMap<>();
+
+ public static void registerAll() {
+ register(new AttackAction());
+ register(new BreakBlockAction());
+ register(new DropAction());
+ register(new JumpAction());
+ register(new RotateAction());
+ register(new SneakAction());
+ register(new UseItemAction());
+ register(new UseItemOnAction());
+ register(new UseItemToAction());
+ register(new LookAction());
+ register(new FishAction());
+ register(new SwimAction());
+ register(new UseItemOffHandAction());
+ register(new UseItemOnOffhandAction());
+ register(new UseItemToOffhandAction());
+ register(new RotationAction());
+ }
+
+ public static boolean register(@NotNull BotAction<?> action) {
+ if (!actions.containsKey(action.getName())) {
+ actions.put(action.getName(), action);
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean unregister(@NotNull String name) {
+ if (actions.containsKey(name)) {
+ actions.remove(name);
+ return true;
+ }
+ return false;
+ }
+
+ @NotNull
+ @Contract(pure = true)
+ public static Collection<BotAction<?>> getAll() {
+ return actions.values();
+ }
+
+ @NotNull
+ public static Set<String> getNames() {
+ return actions.keySet();
+ }
+
+ @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
new file mode 100644
index 0000000000000000000000000000000000000000..3bd512b436b32f240466a406a101a051b4b07817
--- /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<E extends BotAction<E>> {
+
+ private final String name;
+ private final CommandArgument argument;
+ private final Supplier<E> 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<E> 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, null).callEvent();
+ this.setCancelled(true);
+ }
+
+ public CommandArgument getArgument() {
+ return this.argument;
+ }
+
+ @SuppressWarnings("unchecked")
+ public E setTabComplete(int index, List<String> 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..c889a2409d8b9f5979a10b61c98638054bd8f5bc
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/BotConfig.java
@@ -0,0 +1,62 @@
+package org.leavesmc.leaves.bot.agent;
+
+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 BotConfig<E> {
+
+ private final String name;
+ private final CommandArgument argument;
+ private final Supplier<BotConfig<E>> creator;
+ protected ServerBot bot;
+
+ public BotConfig(String name, CommandArgument argument, Supplier<BotConfig<E>> creator) {
+ this.name = name;
+ this.argument = argument;
+ this.creator = creator;
+ }
+
+ public BotConfig<E> setBot(ServerBot bot) {
+ this.bot = bot;
+ return this;
+ }
+
+ public abstract E getValue();
+
+ public abstract void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException;
+
+ public List<String> getMessage() {
+ return List.of(this.bot.getScoreboardName() + "'s " + this.getName() + ": " + this.getValue());
+ }
+
+ public List<String> getChangeMessage() {
+ return List.of(this.bot.getScoreboardName() + "'s " + this.getName() + " changed: " + this.getValue());
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public CommandArgument getArgument() {
+ return argument;
+ }
+
+ @NotNull
+ public BotConfig<E> create(ServerBot bot) {
+ return this.creator.get().setBot(bot);
+ }
+
+ @NotNull
+ public CompoundTag save(@NotNull CompoundTag nbt) {
+ nbt.putString("configName", this.name);
+ return nbt;
+ }
+
+ 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<E> {
+
+ private static final Map<String, Configs<?>> configs = new HashMap<>();
+
+ public static final Configs<Boolean> SKIP_SLEEP = register(new SkipSleepConfig());
+ public static final Configs<Boolean> ALWAYS_SEND_DATA = register(new AlwaysSendDataConfig());
+ public static final Configs<Boolean> SPAWN_PHANTOM = register(new SpawnPhantomConfig());
+ public static final Configs<Integer> SIMULATION_DISTANCE = register(new SimulationDistanceConfig());
+
+ public final BotConfig<E> config;
+
+ private Configs(BotConfig<E> config) {
+ this.config = config;
+ }
+
+ @NotNull
+ @Contract(pure = true)
+ public static Collection<Configs<?>> getConfigs() {
+ return configs.values();
+ }
+
+ @Nullable
+ public static Configs<?> getConfig(String name) {
+ return configs.get(name);
+ }
+
+ @NotNull
+ private static <T> Configs<T> register(BotConfig<T> botConfig) {
+ Configs<T> config = new Configs<>(botConfig);
+ configs.put(botConfig.getName(), config);
+ return config;
+ }
+}
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..be55a3085a53542c08e7f0209883a4f3f72602e7
--- /dev/null
+++ 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 org.jetbrains.annotations.NotNull;
+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 abstract class AbstractTimerAction<E extends AbstractTimerAction<E>> extends BotAction<E> {
+
+ public AbstractTimerAction(String name, Supplier<E> creator) {
+ super(name, CommandArgument.of(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER), creator);
+ this.setTabComplete(0, List.of("[TickDelay]")).setTabComplete(1, List.of("[DoNumber]"));
+ }
+
+ @Override
+ 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<AttackAction> {
+
+ public AttackAction() {
+ super("attack", AttackAction::new);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ Entity entity = bot.getTargetEntity(3, Entity::isAttackable);
+ if (entity != null) {
+ bot.attack(entity);
+ return true;
+ }
+ return false;
+ }
+}
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..bf7d20374cd7bff7cb7e09d209c6da5d297fe1f1
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java
@@ -0,0 +1,75 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import net.minecraft.core.BlockPos;
+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;
+
+public class BreakBlockAction extends AbstractTimerAction<BreakBlockAction> {
+
+ public BreakBlockAction() {
+ super("break", BreakBlockAction::new);
+ }
+
+ private BlockPos lastPos = null;
+ private int destroyProgressTime = 0;
+ private int lastSentState = -1;
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ Block block = bot.getBukkitEntity().getTargetBlockExact(5);
+ if (block != null) {
+ BlockPos pos = ((CraftBlock) block).getPosition();
+
+ if (lastPos == null || !lastPos.equals(pos)) {
+ lastPos = pos;
+ destroyProgressTime = 0;
+ lastSentState = -1;
+ }
+
+ BlockState iblockdata = bot.level().getBlockState(pos);
+ if (!iblockdata.isAir()) {
+ bot.swing(InteractionHand.MAIN_HAND);
+
+ if (iblockdata.getDestroyProgress(bot, bot.level(), pos) >= 1.0F) {
+ bot.gameMode.destroyAndAck(pos, 0, "insta mine");
+ bot.level().destroyBlockProgress(bot.getId(), pos, -1);
+ bot.updateItemInHand(InteractionHand.MAIN_HAND);
+ finalBreak();
+ return true;
+ }
+
+ float damage = this.incrementDestroyProgress(bot, iblockdata, pos);
+ if (damage >= 1.0F) {
+ bot.gameMode.destroyAndAck(pos, 0, "destroyed");
+ bot.level().destroyBlockProgress(bot.getId(), pos, -1);
+ bot.updateItemInHand(InteractionHand.MAIN_HAND);
+ finalBreak();
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private void finalBreak() {
+ lastPos = null;
+ destroyProgressTime = 0;
+ lastSentState = -1;
+ }
+
+ private float incrementDestroyProgress(ServerBot bot, @NotNull BlockState state, BlockPos pos) {
+ float f = state.getDestroyProgress(bot, bot.level(), pos) * (float) (++destroyProgressTime);
+ int k = (int) (f * 10.0F);
+
+ if (k != lastSentState) {
+ bot.level().destroyBlockProgress(bot.getId(), pos, k);
+ lastSentState = k;
+ }
+
+ return f;
+ }
+}
diff --git a/src/main/java/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..7b149243b08a44f1181e82217a8645ccab7732d7
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java
@@ -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<CraftCustomBotAction> {
+
+ private final CustomBotAction realAction;
+
+ public CraftCustomBotAction(String name, @NotNull CustomBotAction realAction) {
+ super(name, CommandArgument.of().setAllTabComplete(realAction.getTabComplete()), null);
+ this.realAction = realAction;
+ }
+
+ @Override
+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ public CraftCustomBotAction createCraft(@Nullable Player player, String[] args) {
+ CustomBotAction newRealAction = realAction.getNew(player, args);
+ if (newRealAction != null) {
+ return new CraftCustomBotAction(this.getName(), newRealAction);
+ }
+ return null;
+ }
+
+ @Override
+ public int getNumber() {
+ return realAction.getNumber();
+ }
+
+ @Override
+ public int getTickDelay() {
+ return realAction.getTickDelay();
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ return realAction.doTick(bot.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..c71e483e8938ef3b181c95d8e297e54203b5b914
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/DropAction.java
@@ -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.command.CommandArgumentResult;
+
+public class DropAction extends AbstractTimerAction<DropAction> {
+
+ public DropAction() {
+ super("drop", DropAction::new);
+ }
+
+ @Override
+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) {
+ this.setTickDelay(result.readInt(100)).setNumber(result.readInt(1));
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ bot.dropAll();
+ return true;
+ }
+}
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..3a13f8ac73e042d939496fb5602e4aa4ea368e0d
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/FishAction.java
@@ -0,0 +1,73 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+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;
+
+public class FishAction extends AbstractTimerAction<FishAction> {
+
+ public FishAction() {
+ super("fish", FishAction::new);
+ }
+
+ private int delay = 0;
+ private int nowDelay = 0;
+
+ @Override
+ public FishAction setTickDelay(int tickDelay) {
+ super.setTickDelay(0);
+ this.delay = tickDelay;
+ return this;
+ }
+
+ @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 (this.nowDelay > 0) {
+ this.nowDelay--;
+ return false;
+ }
+
+ ItemStack mainHand = bot.getMainHandItem();
+ if (mainHand == ItemStack.EMPTY || mainHand.getItem().getClass() != FishingRodItem.class) {
+ return false;
+ }
+
+ FishingHook fishingHook = bot.fishing;
+ if (fishingHook != null) {
+ if (fishingHook.currentState == FishingHook.FishHookState.HOOKED_IN_ENTITY) {
+ mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND);
+ this.nowDelay = 20;
+ return false;
+ }
+ if (fishingHook.nibble > 0) {
+ mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND);
+ this.nowDelay = this.delay;
+ return true;
+ }
+ } else {
+ mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND);
+ }
+
+ return false;
+ }
+}
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..6fc9ba9bf94cb19ed32cfafa3a44fad0201b14a6
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/JumpAction.java
@@ -0,0 +1,21 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.bot.ServerBot;
+
+public class JumpAction extends AbstractTimerAction<JumpAction> {
+
+ public JumpAction() {
+ super("jump", JumpAction::new);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ if (bot.onGround()) {
+ bot.jumpFromGround();
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
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..8be962cf7dc273ccb6a6754684a9be8353865225
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/LookAction.java
@@ -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;
+import org.leavesmc.leaves.command.CommandArgumentResult;
+import org.leavesmc.leaves.command.CommandArgumentType;
+
+import java.util.List;
+
+public class LookAction extends BotAction<LookAction> {
+
+ public LookAction() {
+ super("look", CommandArgument.of(CommandArgumentType.DOUBLE, CommandArgumentType.DOUBLE, CommandArgumentType.DOUBLE), LookAction::new);
+ this.setTabComplete(0, List.of("<X>"));
+ this.setTabComplete(1, List.of("<Y>"));
+ this.setTabComplete(2, List.of("<Z>"));
+ }
+
+ private Vector pos;
+
+ @Override
+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) throws IllegalArgumentException {
+ Vector pos = result.readVector();
+ if (pos != null) {
+ this.setPos(pos).setTickDelay(0).setNumber(1);
+ } else {
+ throw new IllegalArgumentException("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) {
+ this.pos = pos;
+ return this;
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ bot.look(pos.subtract(bot.getLocation().toVector()), false);
+ return true;
+ }
+}
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..84eb7bd727a0085d005a6ee518dfbb8b44fce991
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotateAction.java
@@ -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<RotateAction> {
+
+ public RotateAction() {
+ super("rotate", CommandArgument.EMPTY, RotateAction::new);
+ }
+
+ private ServerPlayer player;
+
+ @Override
+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) {
+ this.setPlayer(player).setTickDelay(0).setNumber(1);
+ }
+
+ 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;
+ }
+}
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..6f6ea32fd78c634467e431572957711034aa6529
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotationAction.java
@@ -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;
+import org.leavesmc.leaves.command.CommandArgumentResult;
+import org.leavesmc.leaves.command.CommandArgumentType;
+
+import java.util.List;
+
+public class RotationAction extends BotAction<RotationAction> {
+
+ public RotationAction() {
+ super("rotation", CommandArgument.of(CommandArgumentType.FLOAT, CommandArgumentType.FLOAT), RotationAction::new);
+ this.setTabComplete(0, List.of("<yaw>"));
+ this.setTabComplete(1, List.of("<pitch>"));
+ }
+
+ 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;
+ }
+
+ public RotationAction setPitch(float pitch) {
+ this.pitch = pitch;
+ return this;
+ }
+
+ @Override
+ @NotNull
+ public CompoundTag save(@NotNull CompoundTag nbt) {
+ super.save(nbt);
+ nbt.putFloat("yaw", this.yaw);
+ nbt.putFloat("pitch", this.pitch);
+ return nbt;
+ }
+
+ @Override
+ 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.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..923cf55d81fce5cf9db9a1c7adc6f3aed5753b16
--- /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 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 SneakAction extends BotAction<SneakAction> {
+
+ public SneakAction() {
+ super("sneak", CommandArgument.EMPTY, SneakAction::new);
+ }
+
+ @Override
+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) {
+ this.setTickDelay(0).setNumber(1);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ 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..b5ccedee17857bc955301512ee965d81fd12017f
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/SwimAction.java
@@ -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<SwimAction> {
+
+ public SwimAction() {
+ super("swim", CommandArgument.EMPTY, SwimAction::new);
+ }
+
+ @Override
+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) {
+ this.setTickDelay(0).setNumber(-1);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ 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..c511ed17e9d5df6d2b961c24ca6f8c157a2baf07
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemAction.java
@@ -0,0 +1,26 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import net.minecraft.world.InteractionHand;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.bot.ServerBot;
+
+public class UseItemAction extends AbstractTimerAction<UseItemAction> {
+
+ public UseItemAction() {
+ super("use", UseItemAction::new);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ if (bot.isUsingItem()) {
+ return false;
+ }
+
+ boolean flag = bot.gameMode.useItem(bot, bot.level(), bot.getItemInHand(InteractionHand.MAIN_HAND), InteractionHand.MAIN_HAND).consumesAction();
+ if (flag) {
+ bot.swing(InteractionHand.MAIN_HAND);
+ bot.updateItemInHand(InteractionHand.MAIN_HAND);
+ }
+ return flag;
+ }
+}
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..26d7286fe9ca9885a02f4f13a8d648d59c7550cd
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java
@@ -0,0 +1,26 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import net.minecraft.world.InteractionHand;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.bot.ServerBot;
+
+public class UseItemOffHandAction extends AbstractTimerAction<UseItemOffHandAction> {
+
+ public UseItemOffHandAction() {
+ super("use_offhand", UseItemOffHandAction::new);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ if (bot.isUsingItem()) {
+ return false;
+ }
+
+ boolean flag = bot.gameMode.useItem(bot, bot.level(), bot.getItemInHand(InteractionHand.OFF_HAND), InteractionHand.OFF_HAND).consumesAction();
+ if (flag) {
+ bot.swing(InteractionHand.OFF_HAND);
+ bot.updateItemInHand(InteractionHand.OFF_HAND);
+ }
+ return flag;
+ }
+}
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..232d0abecb871d3e48c6833f839c921f405b7be7
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java
@@ -0,0 +1,45 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.level.ClipContext;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.entity.TrappedChestBlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.phys.BlockHitResult;
+import net.minecraft.world.phys.HitResult;
+import org.bukkit.Bukkit;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.bot.ServerBot;
+import org.leavesmc.leaves.plugin.MinecraftInternalPlugin;
+
+public class UseItemOnAction extends AbstractTimerAction<UseItemOnAction> {
+
+ public UseItemOnAction() {
+ super("use_on", UseItemOnAction::new);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ HitResult result = bot.getRayTrace(5, ClipContext.Fluid.NONE);
+ if (result instanceof BlockHitResult blockHitResult) {
+ BlockState state = bot.serverLevel().getBlockState(blockHitResult.getBlockPos());
+ if (state.isAir()) {
+ return false;
+ }
+ bot.swing(InteractionHand.MAIN_HAND);
+ if (state.getBlock() == Blocks.TRAPPED_CHEST) {
+ BlockEntity entity = bot.serverLevel().getBlockEntity(blockHitResult.getBlockPos());
+ if (entity instanceof TrappedChestBlockEntity chestBlockEntity) {
+ chestBlockEntity.startOpen(bot);
+ Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> chestBlockEntity.stopOpen(bot), 1);
+ return true;
+ }
+ } else {
+ bot.updateItemInHand(InteractionHand.MAIN_HAND);
+ return bot.gameMode.useItemOn(bot, bot.level(), bot.getItemInHand(InteractionHand.MAIN_HAND), InteractionHand.MAIN_HAND, (BlockHitResult) result).consumesAction();
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/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..3616802c37908372cb7e30c61d6d343bcd3c1cc8
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java
@@ -0,0 +1,45 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.level.ClipContext;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.entity.TrappedChestBlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.phys.BlockHitResult;
+import net.minecraft.world.phys.HitResult;
+import org.bukkit.Bukkit;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.bot.ServerBot;
+import org.leavesmc.leaves.plugin.MinecraftInternalPlugin;
+
+public class UseItemOnOffhandAction extends AbstractTimerAction<UseItemOnOffhandAction> {
+
+ public UseItemOnOffhandAction() {
+ super("use_on_offhand", UseItemOnOffhandAction::new);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ HitResult result = bot.getRayTrace(5, ClipContext.Fluid.NONE);
+ if (result instanceof BlockHitResult blockHitResult) {
+ BlockState state = bot.serverLevel().getBlockState(blockHitResult.getBlockPos());
+ if (state.isAir()) {
+ return false;
+ }
+ bot.swing(InteractionHand.OFF_HAND);
+ if (state.getBlock() == Blocks.TRAPPED_CHEST) {
+ BlockEntity entity = bot.serverLevel().getBlockEntity(blockHitResult.getBlockPos());
+ if (entity instanceof TrappedChestBlockEntity chestBlockEntity) {
+ chestBlockEntity.startOpen(bot);
+ Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> chestBlockEntity.stopOpen(bot), 1);
+ return true;
+ }
+ } else {
+ bot.updateItemInHand(InteractionHand.OFF_HAND);
+ return bot.gameMode.useItemOn(bot, bot.level(), bot.getItemInHand(InteractionHand.OFF_HAND), InteractionHand.OFF_HAND, (BlockHitResult) result).consumesAction();
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/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..05be3dd5ca71a7cd81cd150b9588c60e86b54b73
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToAction.java
@@ -0,0 +1,27 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.entity.Entity;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.bot.ServerBot;
+
+public class UseItemToAction extends AbstractTimerAction<UseItemToAction> {
+
+ public UseItemToAction() {
+ super("use_to", UseItemToAction::new);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ Entity entity = bot.getTargetEntity(3, null);
+ if (entity != null) {
+ boolean flag = bot.interactOn(entity, InteractionHand.MAIN_HAND).consumesAction();
+ if (flag) {
+ bot.swing(InteractionHand.MAIN_HAND);
+ bot.updateItemInHand(InteractionHand.MAIN_HAND);
+ }
+ return flag;
+ }
+ return false;
+ }
+}
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..f8334858a7a0572d1c3dcf5f04696fbbec552a84
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java
@@ -0,0 +1,27 @@
+package org.leavesmc.leaves.bot.agent.actions;
+
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.entity.Entity;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.bot.ServerBot;
+
+public class UseItemToOffhandAction extends AbstractTimerAction<UseItemToOffhandAction> {
+
+ public UseItemToOffhandAction() {
+ super("use_to_offhand", UseItemToOffhandAction::new);
+ }
+
+ @Override
+ public boolean doTick(@NotNull ServerBot bot) {
+ Entity entity = bot.getTargetEntity(3, null);
+ if (entity != null) {
+ boolean flag = bot.interactOn(entity, InteractionHand.OFF_HAND).consumesAction();
+ if (flag) {
+ bot.swing(InteractionHand.OFF_HAND);
+ bot.updateItemInHand(InteractionHand.OFF_HAND);
+ }
+ return flag;
+ }
+ 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..a7f8ed6c81f8309e83feb574ec406956657be285
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/configs/AlwaysSendDataConfig.java
@@ -0,0 +1,44 @@
+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.List;
+
+public class AlwaysSendDataConfig extends BotConfig<Boolean> {
+
+ private boolean value;
+
+ public AlwaysSendDataConfig() {
+ super("always_send_data", CommandArgument.of(CommandArgumentType.BOOLEAN).setTabComplete(0, List.of("ture", "false")), AlwaysSendDataConfig::new);
+ this.value = org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.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<Integer> {
+
+ public SimulationDistanceConfig() {
+ super("simulation_distance", CommandArgument.of(CommandArgumentType.INTEGER).setTabComplete(0, List.of("2", "10", "<INT 2 - 32>")), 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<Boolean> {
+
+ 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..b079cd05be35402c573f7acdb62676ef99b62a2e
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/bot/agent/configs/SpawnPhantomConfig.java
@@ -0,0 +1,50 @@
+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.List;
+
+public class SpawnPhantomConfig extends BotConfig<Boolean> {
+
+ private boolean value;
+
+ public SpawnPhantomConfig() {
+ super("spawn_phantom", CommandArgument.of(CommandArgumentType.BOOLEAN).setTabComplete(0, List.of("ture", "false")), SpawnPhantomConfig::new);
+ this.value = org.dreeam.leaf.config.modules.gameplay.FakePlayerSupport.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<String> 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/command/CommandArgument.java b/src/main/java/org/leavesmc/leaves/command/CommandArgument.java
new file mode 100644
index 0000000000000000000000000000000000000000..0bccbf7816ef621316f0da4911ec112f4753f88e
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/command/CommandArgument.java
@@ -0,0 +1,55 @@
+package org.leavesmc.leaves.command;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class CommandArgument {
+
+ public static final CommandArgument EMPTY = new CommandArgument();
+
+ private final List<CommandArgumentType<?>> argumentTypes;
+ private final List<List<String>> tabComplete;
+
+ private CommandArgument(CommandArgumentType<?>... argumentTypes) {
+ this.argumentTypes = List.of(argumentTypes);
+ this.tabComplete = new ArrayList<>();
+ for (int i = 0; i < argumentTypes.length; i++) {
+ tabComplete.add(new ArrayList<>());
+ }
+ }
+
+ public static CommandArgument of(CommandArgumentType<?>... argumentTypes) {
+ return new CommandArgument(argumentTypes);
+ }
+
+ public List<String> tabComplete(int n) {
+ if (tabComplete.size() > n) {
+ return tabComplete.get(n);
+ } else {
+ return List.of();
+ }
+ }
+
+ public CommandArgument setTabComplete(int index, List<String> list) {
+ tabComplete.set(index, list);
+ return this;
+ }
+
+ public CommandArgument setAllTabComplete(List<List<String>> tabComplete) {
+ this.tabComplete.clear();
+ this.tabComplete.addAll(tabComplete);
+ return this;
+ }
+
+ public CommandArgumentResult parse(int index, String @NotNull [] args) {
+ Object[] result = new Object[argumentTypes.size()];
+ Arrays.fill(result, null);
+ for (int i = index, j = 0; i < args.length && j < result.length; i++, j++) {
+ result[j] = argumentTypes.get(j).pasre(args[i]);
+ }
+ return new CommandArgumentResult(new ArrayList<>(Arrays.asList(result)));
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/command/CommandArgumentResult.java b/src/main/java/org/leavesmc/leaves/command/CommandArgumentResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..46aa6eaa75b65aad6bdbe4a5f517b42e87bcca77
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/command/CommandArgumentResult.java
@@ -0,0 +1,69 @@
+package org.leavesmc.leaves.command;
+
+import net.minecraft.core.BlockPos;
+import org.bukkit.util.Vector;
+
+import java.util.List;
+import java.util.Objects;
+
+public class CommandArgumentResult {
+
+ private final List<Object> result;
+
+ public CommandArgumentResult(List<Object> result) {
+ this.result = result;
+ }
+
+ public int readInt(int def) {
+ return Objects.requireNonNullElse(read(Integer.class), def);
+ }
+
+ public double readDouble(double def) {
+ return Objects.requireNonNullElse(read(Double.class), def);
+ }
+
+ public float readFloat(float def) {
+ return Objects.requireNonNullElse(read(Float.class), def);
+ }
+
+ public String readString(String def) {
+ 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) {
+ if (po == null) {
+ return null;
+ }
+ }
+ return new BlockPos(pos[0], pos[1], pos[2]);
+ }
+
+ public Vector readVector() {
+ Double[] pos = {read(Double.class), read(Double.class), read(Double.class)};
+ for (Double po : pos) {
+ if (po == null) {
+ return null;
+ }
+ }
+ return new Vector(pos[0], pos[1], pos[2]);
+ }
+
+ public <T> T read(Class<T> tClass) {
+ if (result.isEmpty()) {
+ return null;
+ }
+
+ Object obj = result.remove(0);
+ if (tClass.isInstance(obj)) {
+ return tClass.cast(obj);
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/command/CommandArgumentType.java b/src/main/java/org/leavesmc/leaves/command/CommandArgumentType.java
new file mode 100644
index 0000000000000000000000000000000000000000..4ca3508475bbd9771768704e300fe12b717489d6
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/command/CommandArgumentType.java
@@ -0,0 +1,55 @@
+package org.leavesmc.leaves.command;
+
+import org.jetbrains.annotations.NotNull;
+
+public abstract class CommandArgumentType<E> {
+
+ public static final CommandArgumentType<Integer> INTEGER = new CommandArgumentType<>() {
+ @Override
+ public Integer pasre(@NotNull String arg) {
+ try {
+ return Integer.parseInt(arg);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+ };
+
+ public static final CommandArgumentType<Double> DOUBLE = new CommandArgumentType<>() {
+ @Override
+ public Double pasre(@NotNull String arg) {
+ try {
+ return Double.parseDouble(arg);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+ };
+
+ public static final CommandArgumentType<Float> FLOAT = new CommandArgumentType<>() {
+ @Override
+ public Float pasre(@NotNull String arg) {
+ try {
+ return Float.parseFloat(arg);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+ };
+
+ public static final CommandArgumentType<String> STRING = new CommandArgumentType<>() {
+ @Override
+ public String pasre(@NotNull String arg) {
+ return arg;
+ }
+ };
+
+ public static final CommandArgumentType<Boolean> 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/LeavesCommandUtil.java b/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..33143ede35e4a08ebd3211d5535392942f02e2dc
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java
@@ -0,0 +1,82 @@
+package org.leavesmc.leaves.command;
+
+import com.google.common.base.Functions;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import io.papermc.paper.command.PaperCommand;
+import net.minecraft.resources.ResourceLocation;
+import org.bukkit.command.CommandSender;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+@DefaultQualifier(NonNull.class)
+public class LeavesCommandUtil {
+
+ private LeavesCommandUtil() {
+ }
+
+ // Code from Mojang - copyright them
+ public static List<String> getListMatchingLast(
+ final CommandSender sender,
+ final String[] args,
+ final String... matches
+ ) {
+ return getListMatchingLast(sender, args, Arrays.asList(matches));
+ }
+
+ public static boolean matches(final String s, final String s1) {
+ return s1.regionMatches(true, 0, s, 0, s.length());
+ }
+
+ public static List<String> getListMatchingLast(
+ final CommandSender sender,
+ final String[] strings,
+ final Collection<?> collection
+ ) {
+ return getListMatchingLast(sender, strings, collection, "bukkit.command.leaves.", "bukkit.command.leaves");
+ }
+
+ public static List<String> getListMatchingLast(
+ final CommandSender sender,
+ final String[] strings,
+ final Collection<?> collection,
+ final String basePermission,
+ final String overridePermission
+ ) {
+ String last = strings[strings.length - 1];
+ ArrayList<String> results = Lists.newArrayList();
+
+ if (!collection.isEmpty()) {
+ Iterator iterator = Iterables.transform(collection, Functions.toStringFunction()).iterator();
+
+ while (iterator.hasNext()) {
+ String s1 = (String) iterator.next();
+
+ if (matches(last, s1) && (sender.hasPermission(basePermission + s1) || sender.hasPermission(overridePermission))) {
+ results.add(s1);
+ }
+ }
+
+ if (results.isEmpty()) {
+ iterator = collection.iterator();
+
+ while (iterator.hasNext()) {
+ Object object = iterator.next();
+
+ if (object instanceof ResourceLocation && matches(last, ((ResourceLocation) object).getPath())) {
+ results.add(String.valueOf(object));
+ }
+ }
+ }
+ }
+
+ return results;
+ }
+ // end copy stuff
+}
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..b33a3ca0e2b6a06668252048ce064dd294c28bee
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/entity/CraftBot.java
@@ -0,0 +1,102 @@
+package org.leavesmc.leaves.entity;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.entity.TeleportFlag;
+import net.minecraft.world.level.portal.DimensionTransition;
+import net.minecraft.world.phys.Vec3;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.craftbukkit.CraftServer;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.craftbukkit.util.CraftLocation;
+import org.bukkit.event.player.PlayerTeleportEvent;
+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.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.Set;
+import java.util.UUID;
+
+public class CraftBot extends CraftPlayer implements Bot {
+
+ public CraftBot(CraftServer server, ServerBot entity) {
+ super(server, entity);
+ }
+
+ @Override
+ public String getSkinName() {
+ return this.getHandle().createState.skinName();
+ }
+
+ @Override
+ public @NotNull String getRealName() {
+ return this.getHandle().createState.realName();
+ }
+
+ @Override
+ public @Nullable UUID getCreatePlayerUUID() {
+ return this.getHandle().createPlayer;
+ }
+
+ @Override
+ public void addAction(@NotNull LeavesBotAction action) {
+ this.getHandle().addBotAction(CraftBotAction.asInternalCopy(action), null);
+ }
+
+ @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);
+ }
+ }
+
+ @Override
+ public boolean remove(boolean save) {
+ BotList.INSTANCE.removeBot(this.getHandle(), BotRemoveEvent.RemoveReason.PLUGIN, null, save);
+ return true;
+ }
+
+ @Override
+ public boolean teleport(Location location, PlayerTeleportEvent.TeleportCause cause, io.papermc.paper.entity.TeleportFlag... flags) {
+ // Paper end
+ Preconditions.checkArgument(location != null, "location cannot be null");
+ Preconditions.checkState(location.getWorld().equals(this.getWorld()), "[Leaves] Fakeplayers do not support changing world, Please use leaves fakeplayer-api instead!");
+ return super.teleport(location, cause, flags);
+ }
+
+ @Override
+ public ServerBot getHandle() {
+ return (ServerBot) entity;
+ }
+
+ public void setHandle(final ServerBot entity) {
+ super.setHandle(entity);
+ }
+
+ @Override
+ public String toString() {
+ return "CraftBot{" + "name=" + getName() + '}';
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/entity/CraftBotManager.java b/src/main/java/org/leavesmc/leaves/entity/CraftBotManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..422640df346ccae612b2d3492780efa59d8b4d17
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/entity/CraftBotManager.java
@@ -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 java.util.Collection;
+import java.util.Collections;
+import java.util.UUID;
+
+public class CraftBotManager implements BotManager {
+
+ private final BotList botList;
+ private final Collection<Bot> botViews;
+
+ public CraftBotManager() {
+ this.botList = MinecraftServer.getServer().getBotList();
+ this.botViews = Collections.unmodifiableList(Lists.transform(botList.bots, new Function<ServerBot, CraftBot>() {
+ @Override
+ public CraftBot apply(ServerBot bot) {
+ return bot.getBukkitEntity();
+ }
+ }));
+ }
+
+ @Override
+ public @Nullable Bot getBot(@NotNull UUID uuid) {
+ ServerBot bot = botList.getBot(uuid);
+ if (bot != null) {
+ return bot.getBukkitEntity();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public @Nullable Bot getBot(@NotNull String name) {
+ ServerBot bot = botList.getBotByName(name);
+ if (bot != null) {
+ return bot.getBukkitEntity();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public Collection<Bot> getBots() {
+ return botViews;
+ }
+
+ @Override
+ public boolean registerCustomBotAction(String name, CustomBotAction action) {
+ return Actions.register(new CraftCustomBotAction(name, action));
+ }
+
+ @Override
+ public boolean unregisterCustomBotAction(String 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..de06c854a9a5242cf632b38806e8e710496b7e4e
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java
@@ -0,0 +1,151 @@
+package org.leavesmc.leaves.plugin;
+
+import org.bukkit.Server;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.generator.BiomeProvider;
+import org.bukkit.generator.ChunkGenerator;
+import org.bukkit.plugin.PluginBase;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.bukkit.plugin.PluginLoader;
+import org.bukkit.plugin.PluginLogger;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.List;
+
+public class MinecraftInternalPlugin extends PluginBase {
+
+ public static final MinecraftInternalPlugin INSTANCE = new MinecraftInternalPlugin();
+
+ private boolean enabled = true;
+
+ private final PluginDescriptionFile pdf;
+
+ public MinecraftInternalPlugin() {
+ String pluginName = "Minecraft";
+ pdf = new PluginDescriptionFile(pluginName, "1.0", "nms");
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @Override
+ public File getDataFolder() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public PluginDescriptionFile getDescription() {
+ return pdf;
+ }
+
+ @Override
+ public io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() {
+ return pdf;
+ }
+
+ @Override
+ public FileConfiguration getConfig() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public InputStream getResource(String filename) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void saveConfig() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void saveDefaultConfig() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void saveResource(String resourcePath, boolean replace) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void reloadConfig() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public PluginLogger getLogger() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public PluginLoader getPluginLoader() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public Server getServer() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ @Override
+ public void onDisable() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void onLoad() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void onEnable() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public boolean isNaggable() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void setNaggable(boolean canNag) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public @Nullable BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public @NotNull io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager<org.bukkit.plugin.Plugin> getLifecycleManager() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/util/MathUtils.java b/src/main/java/org/leavesmc/leaves/util/MathUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..a6a4fd61644815a7fb01ab1a5844a34f39e57e6d
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/util/MathUtils.java
@@ -0,0 +1,100 @@
+package org.leavesmc.leaves.util;
+
+import org.bukkit.util.NumberConversions;
+import org.bukkit.util.Vector;
+
+import java.util.regex.Pattern;
+
+public class MathUtils {
+ // Lag ?
+ public static void clean(Vector vector) {
+ if (!NumberConversions.isFinite(vector.getX())) vector.setX(0);
+ if (!NumberConversions.isFinite(vector.getY())) vector.setY(0);
+ if (!NumberConversions.isFinite(vector.getZ())) vector.setZ(0);
+ }
+
+ private static final Pattern numericPattern = Pattern.compile("^-?[1-9]\\d*$|^0$");
+
+ public static boolean isNumeric(String str) {
+ return numericPattern.matcher(str).matches();
+ }
+
+ public static float[] fetchYawPitch(Vector dir) {
+ double x = dir.getX();
+ double z = dir.getZ();
+
+ float[] out = new float[2];
+
+ if (x == 0.0D && z == 0.0D) {
+ out[1] = (float) (dir.getY() > 0.0D ? -90 : 90);
+ } else {
+ double theta = Math.atan2(-x, z);
+ out[0] = (float) Math.toDegrees((theta + 6.283185307179586D) % 6.283185307179586D);
+
+ double x2 = NumberConversions.square(x);
+ double z2 = NumberConversions.square(z);
+ double xz = Math.sqrt(x2 + z2);
+ out[1] = (float) Math.toDegrees(Math.atan(-dir.getY() / xz));
+ }
+
+ return out;
+ }
+
+ public static float fetchPitch(Vector dir) {
+ double x = dir.getX();
+ double z = dir.getZ();
+
+ float result;
+
+ if (x == 0.0D && z == 0.0D) {
+ result = (float) (dir.getY() > 0.0D ? -90 : 90);
+ } else {
+ double x2 = NumberConversions.square(x);
+ double z2 = NumberConversions.square(z);
+ double xz = Math.sqrt(x2 + z2);
+ result = (float) Math.toDegrees(Math.atan(-dir.getY() / xz));
+ }
+
+ return result;
+ }
+
+ public static Vector getDirection(double rotX, double rotY) {
+ Vector vector = new Vector();
+
+ rotX = Math.toRadians(rotX);
+ rotY = Math.toRadians(rotY);
+
+ double xz = Math.abs(Math.cos(rotY));
+
+ vector.setX(-Math.sin(rotX) * xz);
+ vector.setZ(Math.cos(rotX) * xz);
+ vector.setY(-Math.sin(rotY));
+
+ return vector;
+ }
+
+ private static final int[] MULTIPLY_DE_BRUIJN_BIT_POSITION = new int[]{0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9};
+
+ public static int floorLog2(int value) {
+ return ceilLog2(value) - (isPowerOfTwo(value) ? 0 : 1);
+ }
+
+ public static int ceilLog2(int value) {
+ value = isPowerOfTwo(value) ? value : smallestEncompassingPowerOfTwo(value);
+ return MULTIPLY_DE_BRUIJN_BIT_POSITION[(int) ((long) value * 125613361L >> 27) & 31];
+ }
+
+ public static boolean isPowerOfTwo(int value) {
+ return value != 0 && (value & value - 1) == 0;
+ }
+
+ public static int smallestEncompassingPowerOfTwo(int value) {
+ int i = value - 1;
+ i |= i >> 1;
+ i |= i >> 2;
+ i |= i >> 4;
+ i |= i >> 8;
+ i |= i >> 16;
+ return i + 1;
+ }
+}