From c7ba5e5fa352985574dafda8b13da1dfd0759d1a Mon Sep 17 00:00:00 2001 From: Milutinke Date: Fri, 9 Sep 2022 15:39:41 +0200 Subject: [PATCH] Implemented the Follow Player chat bot. --- MinecraftClient/ChatBots/FollowPlayer.cs | 175 ++++++++++++++++++ MinecraftClient/McClient.cs | 7 +- .../Resources/config/MinecraftClient.ini | 11 ++ MinecraftClient/Resources/lang/en.ini | 20 ++ MinecraftClient/Settings.cs | 16 +- 5 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 MinecraftClient/ChatBots/FollowPlayer.cs diff --git a/MinecraftClient/ChatBots/FollowPlayer.cs b/MinecraftClient/ChatBots/FollowPlayer.cs new file mode 100644 index 00000000..92af0b9c --- /dev/null +++ b/MinecraftClient/ChatBots/FollowPlayer.cs @@ -0,0 +1,175 @@ +using System; +using System.Linq; +using MinecraftClient.Mapping; + +namespace MinecraftClient.ChatBots +{ + public class FollowPlayer : ChatBot + { + private string? _playerToFollow = null; + private int _updateCounter = 0; + private int _updateLimit; + private int _stopAtDistance; + private bool _unsafeEnabled = false; + + public FollowPlayer(int updateLimit = 15, int stopAtDistance = 3) + { + _updateLimit = updateLimit; + _stopAtDistance = stopAtDistance; + } + + public override void Initialize() + { + if (!GetEntityHandlingEnabled()) + { + LogToConsoleTranslated("extra.entity_required"); + LogToConsoleTranslated("general.bot_unload"); + UnloadBot(); + return; + } + + if (!GetTerrainEnabled()) + { + LogToConsoleTranslated("extra.terrainandmovement_required"); + LogToConsoleTranslated("general.bot_unload"); + UnloadBot(); + return; + } + + RegisterChatBotCommand("follow", "cmd.follow.desc", "follow ", OnFollowCommand); + } + + private string OnFollowCommand(string cmd, string[] args) + { + if (args.Length > 0) + { + if (args[0].Equals("stop", StringComparison.OrdinalIgnoreCase)) + { + if (_playerToFollow == null) + return Translations.TryGet("cmd.follow.already_stopped"); + + _playerToFollow = null; + return Translations.TryGet("cmd.follow.stopping"); + } + else + { + if (!IsValidName(args[0])) + return Translations.TryGet("cmd.follow.invalid_name"); + + Entity? player = GetEntities().Values.ToList().Find(entity => + entity.Type == EntityType.Player && !string.IsNullOrEmpty(entity.Name) && entity.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + + if (player == null) + return Translations.TryGet("cmd.follow.invalid_player"); + + if (!CanMoveThere(player.Location)) + return Translations.TryGet("cmd.follow.cant_reach_player"); + + if (_playerToFollow != null && _playerToFollow.Equals(args[0], StringComparison.OrdinalIgnoreCase)) + return Translations.TryGet("cmd.follow.already_following", _playerToFollow); + + string result = Translations.TryGet(_playerToFollow != null ? "cmd.follow.switched" : "cmd.follow.started", player.Name!); + _playerToFollow = args[0].Trim().ToLower(); + + LogToConsoleTranslated("cmd.follow.note"); + + if (args.Length == 2 && args[1].Equals("-f", StringComparison.OrdinalIgnoreCase)) + { + _unsafeEnabled = true; + LogToConsoleTranslated("cmd.follow.unsafe_enabled"); + } + + return result; + } + } + + return Translations.TryGet("cmd.follow.desc") + ": " + Translations.TryGet("cmd.follow.usage"); + } + + public override void Update() + { + _updateCounter++; + } + + public override void OnEntityMove(Entity entity) + { + + if (_updateCounter < _updateLimit) + return; + + _updateCounter = 0; + + if (entity.Type != EntityType.Player) + return; + + if (_playerToFollow == null || string.IsNullOrEmpty(entity.Name)) + return; + + if (_playerToFollow != entity.Name.ToLower()) + return; + + if (!CanMoveThere(entity.Location)) + return; + + // Stop at specified distance from plater (prevents pushing player around) + double distance = Distance(entity.Location, GetCurrentLocation()); + + if (distance < _stopAtDistance) + return; + + MoveToLocation(entity.Location, _unsafeEnabled); + } + + public override void OnEntitySpawn(Entity entity) + { + if (entity.Type != EntityType.Player) + return; + + if (_playerToFollow != null && !string.IsNullOrEmpty(entity.Name) && _playerToFollow.Equals(entity.Name, StringComparison.OrdinalIgnoreCase)) + { + LogToConsoleTranslated("cmd.follow.player_came_to_the_range", _playerToFollow); + LogToConsoleTranslated("cmd.follow.resuming"); + } + } + + public override void OnEntityDespawn(Entity entity) + { + if (entity.Type != EntityType.Player) + return; + + if (_playerToFollow != null && !string.IsNullOrEmpty(entity.Name) && _playerToFollow.Equals(entity.Name, StringComparison.OrdinalIgnoreCase)) + { + LogToConsoleTranslated("cmd.follow.player_left_the_range", _playerToFollow); + LogToConsoleTranslated("cmd.follow.pausing"); + } + } + + public override void OnPlayerLeave(Guid uuid, string? name) + { + if (_playerToFollow != null && !string.IsNullOrEmpty(name) && _playerToFollow.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + LogToConsoleTranslated("cmd.follow.player_left", _playerToFollow); + LogToConsoleTranslated("cmd.follow.stopping"); + _playerToFollow = null; + } + } + + private double Distance(Location location1, Location location2) + { + return Math.Sqrt( + ((location1.X - location2.X) * (location1.X - location2.X)) + + ((location1.Y - location2.Y) * (location1.Y - location2.Y)) + + ((location1.Z - location2.Z) * (location1.Z - location2.Z))); + } + + private bool CanMoveThere(Location location) + { + ChunkColumn? chunkColumn = GetWorld().GetChunkColumn(location); + + if (chunkColumn == null || chunkColumn.FullyLoaded == false) + return false; + + return true; + } + } +} \ No newline at end of file diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 3ef7fbf1..eaf6b63a 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -195,6 +195,7 @@ namespace MinecraftClient if (Settings.AutoCraft_Enabled) { BotLoad(new AutoCraft(Settings.AutoCraft_configFile)); } if (Settings.AutoDrop_Enabled) { BotLoad(new AutoDrop(Settings.AutoDrop_Mode, Settings.AutoDrop_items)); } if (Settings.ReplayMod_Enabled) { BotLoad(new ReplayCapture(Settings.ReplayMod_BackupInterval)); } + if (Settings.FollowPlayer_Enabled) { BotLoad(new FollowPlayer(Settings.FollowPlayer_UpdateLimit, Settings.FollowPlayer_UpdateLimit)); } //Add your ChatBot here by uncommenting and adapting //BotLoad(new ChatBots.YourBot()); @@ -316,7 +317,7 @@ namespace MinecraftClient { if (e is not ThreadAbortException) Log.Warn("Update: Got error from " + bot.ToString() + ": " + e.ToString()); - else + else throw; //ThreadAbortException should not be caught } } @@ -1658,8 +1659,8 @@ namespace MinecraftClient upper2backpack = true; lowerStartSlot = 1; } - else if (item != null && item.Count == 1 && (item.Type == ItemType.NetheriteIngot || - item.Type == ItemType.Emerald || item.Type == ItemType.Diamond || item.Type == ItemType.GoldIngot || + else if (item != null && item.Count == 1 && (item.Type == ItemType.NetheriteIngot || + item.Type == ItemType.Emerald || item.Type == ItemType.Diamond || item.Type == ItemType.GoldIngot || item.Type == ItemType.IronIngot) && !inventory.Items.ContainsKey(0)) { lower2upper = true; diff --git a/MinecraftClient/Resources/config/MinecraftClient.ini b/MinecraftClient/Resources/config/MinecraftClient.ini index 332c00be..d2a8bb72 100644 --- a/MinecraftClient/Resources/config/MinecraftClient.ini +++ b/MinecraftClient/Resources/config/MinecraftClient.ini @@ -249,3 +249,14 @@ items= # separate each item name with a comma ',': It # /!\ You SHOULD use /replay stop or exit the program gracefully with /quit OR THE REPLAY FILE MAY GET CORRUPT! enabled=false backupinterval=300 # How long should replay file be auto-saved, in seconds. Use -1 to disable + +[FollowPlayer] +# Enabled you to make the bot follow you +# NOTE: This is an experimental feature, the bot can be slow at times, you need to walk with a normal speed and to sometimes stop for it to be able to keep up with you +# It's similar to making animals follow you when you're holding food in your hand. +# This is due to a slow pathfinding algorithm, we're working on getting a better one +# You can tweak the update limit and find what works best for you. (NOTE: Do not but a very low one, because you might achieve the opposite, +# this might clog the thread for terain handling) and thus slow the bot even more. +enabled=false +update_limit=10 # The rate at which the bot does calculations (10 = 1s) (Default: 5) (You can tweak this if you feel the bot is too slow) +stop_at_distance=3 # Do not follow the player if he is in the range of 3 blocks (prevents the bot from pushing a player in an infinite loop) \ No newline at end of file diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 1d0cc4ad..c13717fb 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -515,6 +515,26 @@ bot.replayCapture.created=Replay file created. bot.replayCapture.stopped=Record stopped. bot.replayCapture.restart=Record was stopped. Restart the program to start another record. +# Follow player +cmd.follow.desc=Makes the bot follow a specified player +cmd.follow.usage=follow [-f] (Use -f to enable un-safe walking) +cmd.follow.already_stopped=Already stopped +cmd.follow.stopping=Stopped following! +cmd.follow.invalid_name=Invalid or empty player name provided! +cmd.follow.invalid_player=The specified player is either not connected out out of the range! +cmd.follow.cant_reach_player=Can\'t reach the player, he is either in chunks that are not loaded, too far away or not reachable by a bot due to obstacles like gaps or water bodies! +cmd.follow.already_following=Already following {0}! +cmd.follow.switched=Switched to following {0}! +cmd.follow.started=Started following {0}! +cmd.follow.unsafe_enabled=Enabled us-safe walking (NOTE: The bot might die or get hurt!) +cmd.follow.note=NOTE: The bot is quite slow, you need to walk slowly and at a close distance for it to be able to keep up, kinda like when you make animals follow you by holding food in your hand. This is a limitation due to a pathfinding algorithm, we are working to get a better one. +cmd.follow.player_came_to_the_range=The player {0} came back to the range! +cmd.follow.resuming=Resuming to follow! +cmd.follow.player_left_the_range=The player {0} has left the range! +cmd.follow.pausing=Pausing! +cmd.follow.player_left=The player {0} left the server! +cmd.follow.stopping=Stopped! + # Script bot.script.not_found=§8[MCC] [{0}] Cannot find script file: {1} bot.script.file_not_found=File not found: '{0}' diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index db493953..3eeb8070 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -230,6 +230,11 @@ namespace MinecraftClient public static bool ReplayMod_Enabled = false; public static int ReplayMod_BackupInterval = 3000; + // Follow Player + public static bool FollowPlayer_Enabled = false; + public static int FollowPlayer_UpdateLimit = 10; + public static int FollowPlayer_StopAtDistance = 3; + //Custom app variables and Minecraft accounts private static readonly Dictionary AppVars = new Dictionary(); private static readonly Dictionary> Accounts = new Dictionary>(); @@ -239,7 +244,7 @@ namespace MinecraftClient private static string ServerAliasTemp = null; //Mapping for settings sections in the INI file - private enum Section { Default, Main, AppVars, Proxy, MCSettings, AntiAFK, Hangman, Alerts, ChatLog, AutoRelog, ScriptScheduler, RemoteControl, ChatFormat, AutoRespond, AutoAttack, AutoFishing, AutoEat, AutoCraft, AutoDrop, Mailer, ReplayMod, Logging, Signature }; + private enum Section { Default, Main, AppVars, Proxy, MCSettings, AntiAFK, Hangman, Alerts, ChatLog, AutoRelog, ScriptScheduler, RemoteControl, ChatFormat, AutoRespond, AutoAttack, AutoFishing, AutoEat, AutoCraft, AutoDrop, Mailer, ReplayMod, FollowPlayer, Logging, Signature }; /// /// Get settings section from name @@ -816,6 +821,15 @@ namespace MinecraftClient case "backupinterval": ReplayMod_BackupInterval = str2int(argValue); return true; } break; + + case Section.FollowPlayer: + switch (ToLowerIfNeed(argName)) + { + case "enabled": FollowPlayer_Enabled = str2bool(argValue); return true; + case "update_limit": FollowPlayer_UpdateLimit = str2int(argValue); return true; + case "stop_at_distance": FollowPlayer_StopAtDistance = str2int(argValue); return true; + } + break; } return false; }