What's new
  • Visit Rebornbuddy
  • Visit Panda Profiles
  • Visit LLamamMagic
  • Visit Resources
  • Visit Downloads
  • Visit Portal

[NOTICE] Upcoming PlayerMover Breaking Changes

pushedx

Well-Known Member
Joined
Sep 24, 2013
Messages
4,252
Reaction score
290
NOTE: These are internal changes, not plugin feature/functionality related, so most users do not have to care about this unless they are using a custom player mover, or are a developer of such player mover!

This thread is a heads up for users/developers that Exilebuddy will receive some breaking API in the near future. These types of changes are avoided as much as possible, but sometimes they are unavoidable due to how old Exilebuddy is, and how some systems it is using simply need to be rewritten to offer more ease of use, flexibility, and power.

As mentioned in the "Exilebuddy Capability Information" there's a number of longer term changes we have had planned, but unfortunately due to how frequently the game changes, there's usually not enough time to redesign and recode legacy systems that have been holding Exilebuddy back. As the game keeps moving forward, we have to as well, and as a result, a lot of old code that is "good enough" sticks around longer than intended.

As our Bestiary API updates progress, we're currently in a good position to take care of a few of our longer term plans, so this thread will explain what is changing and why. Bestiary updates will continue, but we feel it's a good time to also address some older issues that we've mentioned will be taken care of eventually.

The first of the breaking changes will be with the PlayerMover system. As mentioned in the previously linked thread:
Player Mover - This system will be updated most likely along with the routine system, since people want better skill based handling, so it ties together. We do have plans to revamp this system in the near future as well, but for the initial 3.0 release, we just stuck with something that was 'working' still, because trying to have something that is more efficient is just a QoL upgrade that can be focused on later.

The Player Mover system is getting updated first, because it's very small, we know exactly what needs to be done with it, and the changes won't be too traumatic. We still intend to update the Routine system and the Item Filter system, but the Player Mover system is easiest right now.

First, I will cover why the Player Mover system is being changed.

The current player mover system is very old, and is a legacy system that was being used before we started including the source to all of our content. When we made that transition, we could not included the player mover in the same way as routines, because the underlying system was hardcoded into the bot itself.

This hardcoded system has created what represents the biggest problems with player movers - the default one is coded into the bot (we do include the source code in the Help folder, but it's not changeable) so when users want to switch between them or try a community one, the process is different than using bots, routines, plugins.

Next, the design of the player mover is from an older version of EB before the concept of messages and logic requests was added, which greatly improved the flexibility and power of EB. Right now, the player mover is designed to accept a message (a newer addition around the last big API change) but that's not enough.

Customizing your player mover or changing settings is impossible since there's no concept of settings, just sending messages. It would be very nice if the player movers supported GUIs and settings that could be shared among users.

Fortunately, the solution to the player mover issues are easy to solve - treat them the same way we treat routines.

That's exactly what the changes being made will do. A new MoverManager class will be added that mirrors RoutineManager, and users will be able to switch player movers through the UI or code just as they can with a routine. Likewise, the IMover interface will get processed like the other content interfaces (IBot, IPlugin, IRoutine, IContent) so sharing player mover code will work the same way as everything else finally. The last used routine is saved in gui settings, so the last used mover will be as well, and so on.

Lastly, the addition of skill based player movers has always been requested, and while there are a few community versions, our support of them has been more limited due to how the legacy player mover system worked.

With the new changes, the player mover system will mirror routines, so a lot more will be possible on that front, and community developers for player movers will be much simpler than before. These changes are essentially changes that need to be made to the Item Filter system as well, but that system is much more complicated to deal with, so we'll address that later. The routine system only needs implementation updates, not design updates , so that's why the player mover system is being changed from the way it is.

To give some basic code examples:
Before
Code:
// #1
PlayerMover.Message(new Message("SetNetworkingMode", this, LokiPoe.ConfigManager.NetworkingMode)); // TODO: Replace

// #2
PlayerMover.Message(new Loki.Bot.Message("SetDoAdjustments", null, false));

// #3
PlayerMover.MoveTowards(boss.Position)

After
Code:
// #1
// The code now takes place in IMover.Start, so there is no need to send a SetNetworkingMode message

// #2
// All default implementation message code has been removed, as it was only being used in Legacy OGB code, which is no longer being developed or supported. However, the messaging API still works fine if your customer player mover supports it.

// #3
MoverManager.Current.MoveTowards(boss.Position)
// Just as the RoutineManager.Current is used, the same system for IMover will be too

OldPlayerMover will now have settings and a GUI:
0093e3d300b70d525bb7f56ab92f08b4.webp


To allow devs to make and test new IMovers in the mean time, the PlayerMover.UseMoverManager bool property has been added. When set to true via code, the PlayerMover class will forward requests to the MoveManager class instead, allowing both systems to coexist, without breaking anything for now.

Here's that class, for reference to understand:
Code:
using System;
using log4net;
using Loki.Common;

namespace Loki.Bot
{
    /// <summary>
    /// A class that holds an IPlayerMover to use to handle movement.
    /// </summary>
    [Obsolete("NOTICE: The PlayerMover will be removed in the future and the MoverManager will be used instead. However, it's not possible to fully switch to MoverManager yet without breaking everyone's code.")]
    public static class PlayerMover
    {
        private static readonly ILog Log = Logger.GetLoggerInstanceForType();

        private static bool _useMoverManager = false;

        /// <summary>
        /// Should the new MoverManager system be used instead?
        /// </summary>
        public static bool UseMoverManager
        {
            get
            {
                return _useMoverManager;
            }
            set
            {
                _useMoverManager = value;
                Log.InfoFormat("[PlayerMover.UseMoverManager] {0}", _useMoverManager);
            }
        }

        /// <summary>
        /// The current IPlayerMover to use.
        /// </summary>
        public static IPlayerMover Instance { get; set; }

        static PlayerMover()
        {
            Instance = new DefaultPlayerMover();
        }

        /// <summary>
        /// Implements logic to handle a message passed through the system.
        /// </summary>
        /// <param name="message">The message to be processed.</param>
        /// <returns>A tuple of a MessageResult and object.</returns>
        public static MessageResult Message(Message message)
        {
            if (UseMoverManager)
            {
                return MoverManager.Current.Message(message);
            }

            if (Instance == null)
            {
                Log.ErrorFormat("[PlayerMover::Message] Instance == null");
                return MessageResult.Unprocessed;
            }

            return Instance.Message(message);
        }

        /// <summary>
        /// An interface for moving a player.
        /// </summary>
        /// <param name="position">The position to move towards.</param>
        /// <param name="user">A user object to pass though IPlayerMover.</param>
        /// <returns>true if the position was moved towards, and false if there was an error.</returns>
        public static bool MoveTowards(Vector2i position, object user = null)
        {
            if(UseMoverManager)
            {
                return MoverManager.Current.MoveTowards(position, user);
            }

            if (Instance == null)
            {
                Log.ErrorFormat("[PlayerMover::MoveTowards] Instance == null");
                return false;
            }

            return Instance.MoveTowards(position, user);
        }
    }
}

Since it is not ideal to have two different systems in place though, the legacy system will be removed in the future. NOTE: Any user code that uses PlayerMover.Instance directly to do things, will not work with the new flag, because the assumption is all code is going through the PlayerMover static class in the first place! However, it's not a huge deal since new player mover code is needed to use the new system anyways.

The current code for OldPlayerMover is as follows, but might get minor changes, the logic is functionally the same as the current DefaultPlayerMover code though.
Code:
using System.Diagnostics;
using System.Linq;
using log4net;
using Loki.Bot.Pathfinding;
using Loki.Common;
using Loki.Game;
using Loki.Bot;
using System.Windows.Controls;
using System.Threading.Tasks;

namespace Legacy.OldPlayerMover
{
    internal class OldPlayerMover : IMover
    {
        private static readonly ILog Log = Logger.GetLoggerInstanceForType();

        private Gui _instance;

        #region Implementation of IAuthored

        /// <summary> The name of the plugin. </summary>
        public string Name => "OldPlayerMover";

        /// <summary>The author of the plugin.</summary>
        public string Author => "Bossland GmbH";

        /// <summary> The description of the plugin. </summary>
        public string Description => "The old legacy player mover for Exilebuddy.";

        /// <summary>The version of the plugin.</summary>
        public string Version => "0.0.1.1";

        #endregion

        private PathfindingCommand _cmd;
        private readonly Stopwatch _sw = new Stopwatch();
        private LokiPoe.ConfigManager.NetworkingType _networkingMode = LokiPoe.ConfigManager.NetworkingType.Unknown;
        private int _pathRefreshRate = 1000;
        private Vector2i _lastPoint = Vector2i.Zero;

        /// <summary>
        /// These are areas that always have issues with stock pathfinding, so adjustments will be made.
        /// </summary>
        private readonly string[] _forcedAdjustmentAreas = new[]
        {
            "The City of Sarn",
            "The Slums",
        };

        private LokiPoe.TerrainDataEntry[,] _tgts;
        private uint _tgtSeed;

        private LokiPoe.TerrainDataEntry TgtUnderPlayer
        {
            get
            {
                var myPos = LokiPoe.LocalData.MyPosition;
                return _tgts[myPos.X / 23, myPos.Y / 23];
            }
        }

        #region Implementation of IBase

        /// <summary>Initializes this plugin.</summary>
        public void Initialize()
        {
        }

        /// <summary>Deinitializes this object. This is called when the object is being unloaded from the bot.</summary>
        public void Deinitialize()
        {
        }

        #endregion

        #region Implementation of ITickEvents / IStartStopEvents

        /// <summary> The mover start callback. Do any initialization here. </summary>
        public void Start()
        {
            _networkingMode = LokiPoe.ConfigManager.NetworkingMode; // Now this can be done cleanly!
            if (_networkingMode == LokiPoe.ConfigManager.NetworkingType.Predictive)
            {
                // Generate new paths in predictive more frequently to avoid back and forth issues from the new movement model
                _pathRefreshRate = 16;
            }
            else
            {
                _pathRefreshRate = 1000;
            }
        }

        /// <summary> The mover tick callback. Do any update logic here. </summary>
        public void Tick()
        {
        }

        /// <summary> The mover stop callback. Do any pre-dispose cleanup here. </summary>
        public void Stop()
        {
        }

        #endregion


        #region Implementation of IConfigurable

        /// <summary>The settings object. This will be registered in the current configuration.</summary>
        public JsonSettings Settings => OldPlayerMoverSettings.Instance;

        /// <summary> The plugin's settings control. This will be added to the Exilebuddy Settings tab.</summary>
        public UserControl Control => (_instance ?? (_instance = new Gui()));

        #endregion

        #region Implementation of ILogicHandler

        /// <summary>
        /// Implements the ability to handle a logic passed through the system.
        /// </summary>
        /// <param name="logic">The logic to be processed.</param>
        /// <returns>A LogicResult that describes the result..</returns>
        public async Task<LogicResult> Logic(Logic logic)
        {
            return LogicResult.Unprovided;
        }

        #endregion

        #region Implementation of IMessageHandler

        /// <summary>
        /// Implements logic to handle a message passed through the system.
        /// </summary>
        /// <param name="message">The message to be processed.</param>
        /// <returns>A tuple of a MessageResult and object.</returns>
        public MessageResult Message(Message message)
        {
            return MessageResult.Unprocessed;
        }

        #endregion

        #region Implementation of IEnableable

        /// <summary> The plugin is being enabled.</summary>
        public void Enable()
        {
        }

        /// <summary> The plugin is being disabled.</summary>
        public void Disable()
        {
        }

        #endregion

        #region Override of Object

        /// <summary>Returns a string that represents the current object.</summary>
        /// <returns>A string that represents the current object.</returns>
        public override string ToString()
        {
            return Name + ": " + Description;
        }

        #endregion

        #region Override of IMover

        /// <summary>
        /// Returns the player mover's current PathfindingCommand being used.
        /// </summary>
        public PathfindingCommand CurrentCommand => _cmd;

        /// <summary>
        /// Attempts to move towards a position. This function will perform pathfinding logic and take into consideration move distance
        /// to try and smoothly move towards a point.
        /// </summary>
        /// <param name="position">The position to move towards.</param>
        /// <param name="user">A user object passed.</param>
        /// <returns>true if the position was moved towards, and false if there was a pathfinding error.</returns>
        public bool MoveTowards(Vector2i position, params dynamic[] user)
        {
            Log.WarnFormat("[OldPlayerMover.MoveTowards] {0}", position);

            var myPosition = LokiPoe.MyPosition;
            if (
                _cmd == null || // No command yet
                _cmd.Path == null ||
                _cmd.EndPoint != position || // Moving to a new position
                LokiPoe.CurrentWorldArea.IsTown || // In town, always generate new paths
                (_sw.IsRunning && _sw.ElapsedMilliseconds > _pathRefreshRate) || // New paths on interval
                _cmd.Path.Count <= 2 || // Not enough points
                _cmd.Path.All(p => myPosition.Distance(p) > 7))
            // Try and find a better path to follow since we're off course
            {
                _cmd = new PathfindingCommand(myPosition, position, 3, OldPlayerMoverSettings.Instance.AvoidWallHugging);
                if (!ExilePather.FindPath(ref _cmd))
                {
                    _sw.Restart();
                    Log.ErrorFormat("[OldPlayerMover.MoveTowards] ExilePather.FindPath failed from {0} to {1}.",
                        myPosition, position);
                    return false;
                }
                //Log.InfoFormat("[OldPlayerMover.MoveTowards] Finding new path.");
                _sw.Restart();
                //_originalPath = new IndexedList<Vector2i>(_cmd.Path);
            }

            // Eliminate points until we find one within a good moving range.
            while (_cmd.Path.Count > 1)
            {
                if (_cmd.Path[0].Distance(myPosition) < OldPlayerMoverSettings.Instance.MoveRange)
                {
                    _cmd.Path.RemoveAt(0);
                }
                else
                {
                    break;
                }
            }

            var point = _cmd.Path[0];
            point += new Vector2i(LokiPoe.Random.Next(-2, 3), LokiPoe.Random.Next(-2, 3));

            var cwa = LokiPoe.CurrentWorldArea;
            if (!cwa.IsTown && !cwa.IsHideoutArea && _forcedAdjustmentAreas.Contains(cwa.Name))
            {
                var negX = 0;
                var posX = 0;

                var tmp1 = point;
                var tmp2 = point;

                for (var i = 0; i < 10; i++)
                {
                    tmp1.X--;
                    if (!ExilePather.IsWalkable(tmp1))
                    {
                        negX++;
                    }

                    tmp2.X++;
                    if (!ExilePather.IsWalkable(tmp2))
                    {
                        posX++;
                    }
                }

                if (negX > 5 && posX == 0)
                {
                    point.X += 15;
                    Log.InfoFormat("[OldPlayerMover.MoveTowards] Adjustments being made!");
                    _cmd.Path[0] = point;
                }
                else if (posX > 5 && negX == 0)
                {
                    point.X -= 15;
                    Log.InfoFormat("[OldPlayerMover.MoveTowards] Adjustments being made!");
                    _cmd.Path[0] = point;
                }
            }

            // Le sigh...
            if (cwa.IsTown && cwa.Act == 3)
            {
                var seed = LokiPoe.LocalData.AreaHash;
                if (_tgtSeed != seed || _tgts == null)
                {
                    Log.InfoFormat("[OldPlayerMover.MoveTowards] Now building TGT info.");
                    _tgts = LokiPoe.TerrainData.TgtEntries;
                    _tgtSeed = seed;
                }
                if (TgtUnderPlayer.TgtName.Equals("Art/Models/Terrain/Act3Town/Act3_town_01_01_c16r7.tgt"))
                {
                    Log.InfoFormat("[OldPlayerMover.MoveTowards] Act 3 Town force adjustment being made!");
                    point.Y += 5;
                }
            }

            var move = LokiPoe.InGameState.SkillBarHud.LastBoundMoveSkill;
            if (move == null)
            {
                Log.ErrorFormat("[OldPlayerMover.MoveTowards] Please assign the \"Move\" skill to your skillbar!");
                return false;
            }

            if ((LokiPoe.ProcessHookManager.GetKeyState(move.BoundKeys.Last()) & 0x8000) != 0 &&
                LokiPoe.Me.HasCurrentAction)
            {
                if (myPosition.Distance(position) < OldPlayerMoverSettings.Instance.SingleUseDistance)
                {
                    LokiPoe.ProcessHookManager.ClearAllKeyStates();
                    LokiPoe.InGameState.SkillBarHud.UseAt(move.Slots.Last(), false, point);
                    if (OldPlayerMoverSettings.Instance.DebugInputApi)
                    {
                        Log.WarnFormat("[SkillBarHud.UseAt] {0}", point);
                    }
                    _lastPoint = point;
                }
                else
                {
                    if (OldPlayerMoverSettings.Instance.UseMouseSmoothing)
                    {
                        var d = _lastPoint.Distance(point);
                        if (d >= OldPlayerMoverSettings.Instance.MouseSmoothDistance)
                        {
                            LokiPoe.Input.SetMousePos(point, false);
                            if (OldPlayerMoverSettings.Instance.DebugInputApi)
                            {
                                Log.WarnFormat("[Input.SetMousePos] {0} [{1}]", point, d);
                            }
                            _lastPoint = point;
                        }
                        else
                        {
                            if (OldPlayerMoverSettings.Instance.DebugInputApi)
                            {
                                Log.WarnFormat("[Input.SetMousePos] Skipping moving mouse to {0} because [{1}] < [{2}]", point, d, OldPlayerMoverSettings.Instance.MouseSmoothDistance);
                            }
                        }
                    }
                    else
                    {
                        LokiPoe.Input.SetMousePos(point, false);
                    }
                }
            }
            else
            {
                LokiPoe.ProcessHookManager.ClearAllKeyStates();
                if (myPosition.Distance(position) < OldPlayerMoverSettings.Instance.SingleUseDistance)
                {
                    LokiPoe.InGameState.SkillBarHud.UseAt(move.Slots.Last(), false, point);
                    if (OldPlayerMoverSettings.Instance.DebugInputApi)
                    {
                        Log.WarnFormat("[SkillBarHud.UseAt] {0}", point);
                    }
                }
                else
                {
                    LokiPoe.InGameState.SkillBarHud.BeginUseAt(move.Slots.Last(), false, point);
                    if (OldPlayerMoverSettings.Instance.DebugInputApi)
                    {
                        Log.WarnFormat("[BeginUseAt] {0}", point);
                    }
                }
            }

            return true;
        }

        #endregion
    }
}


Hopefully that makes sense, and users can understand why these changes are being made.

The next Beta/Release will contain these updates, but nothing should break yet.


In order to move Exilebuddy forward, we often have to break old things to pave the way for new better things. This thread is being left open for any discussion, so please don't hesitate to take part of the discussion. There is no ETA yet on the breaking changes though, but we expect perhaps a few weeks at least so everyone has time to prepare and understand the changes.
 
Last edited:
Rip in pepperonis.

Edit, how do I report a thread for breaking my comfort zone. /s
 
Hi. Any chance to make MoveTowards async Task<bool> in future? Would be nice to use await FinishCurrentAction for skill based movers or some other await operations.
 
Hi. Any chance to make MoveTowards async Task<bool> in future? Would be nice to use await FinishCurrentAction for skill based movers or some other await operations.

It's a big change that I'm not against just yet, but what do you think you can do with a coroutine that you can't currently do with the non-coroutine setup? FinishCurrentAction logic is simple without a coroutine - you just don't execute since PlayerMover is pulsed to move, so what did you have in mind being able to await other operations exactly?

It's not already a coroutine because the system was in place before we had coroutines in Exilebuddy, back when we used TreeSharp. I just tested the changes, and all code calling MoveTowards is already coroutine logic, so I don't think anything would be lost except calling MoveTowards from a non-coroutine context, which is still possible I believe just extra code. Biggest inconvenience is the degree of code breaking changes for devs, but we're willing to do things like that if the new system results in a better long term prospect for the project.

Basically though, it's a lot easier to convince people of major breaking changes like that if you have something concrete to show them of what will be possible with the new system vs how itd have to be done currently. I do think there's room for a lot of improvements with, especially when it comes to dodging, but it's not feasible under any system because of how pathfinding works in this game and major changes we need on that front still.
 
As for just not execute logic, if you has current action, its easy agreed. Just thought if you decided to make big changes, it would be nice to implement that.

As far as I know that Alcor75's mover, which is used by 80%+ of people I think,
uses some mindbreaking logic for delays and checks if your character stuck in half-way.

Any way its up to you. I only shared some thoughts.
 
As for just not execute logic, if you has current action, its easy agreed. Just thought if you decided to make big changes, it would be nice to implement that.

As far as I know that Alcor75's mover, which is used by 80%+ of people I think,
uses some mindbreaking logic for delays and checks if your character stuck in half-way.

Any way its up to you. I only shared some thoughts.

Next Beta will include an update to our bot bases to call Start/Tick/Stop of MoverManager (current users need to make the change themselves) and in addition, I'll include an example of using coroutines in the new system. Using coroutines before was possible, but did require some extra hoops to be jumped through, but now it's much more straight forward.

So far, I've not had anyone give me any convincing arguments for making MoveTowards a Task, but I don't have any good arguments for not changing it yet either, past just breaking code inconveniences, so right now there seems not to be any reason to change it.

Attached is the example of coroutine support using the new system, if you want to play with it or see if you can do what Alcor did but better or not. We will eventually have our own skill based player mover I think, but we cannot just use anyone else's. Remember you need to add MoverManager.Start/Tick/Stop events in the respective locations in QuestBot/MapBot for it to work with the current Beta/Release.

Also, the logic doesn't actually use the coroutine yet, it just shows exactly what can be done! I only did brief testing, but it should work, as I just ran QB with it a bit. Make sure to enable the new system from the Managers setting window.

I'm still open to hearing ideas from anyone about potentials with player mover design changes.
 

Attachments

Also, the logic doesn't actually use the coroutine yet, it just shows exactly what can be done! I only did brief testing, but it should work, as I just ran QB with it a bit. Make sure to enable the new system from the Managers setting window.

Oh. Never thought you'll add this to Bot so didn't tell my thought about "OldCoroutinePlayerMover".

Because of realization it allways returns false until task handeled. This cause some "issues". Explorer often adds coords to blacklist and it makes bot to move strange.... Besides I had some code in some of my plugins that was expecting MoveTowards return false only if no path exists. Sure Its only for test and I can add check before moving. Just saying its hard to test in this condition ))

As for Coroutine logic for mover at all, don't think anyone interested in it now. No sense to work on it.

Anyway thx! You are great! Glad you try to make your product better.
 
I think I saw your PM, but I'll have to get back to you later on specifics of what you asked.

Adding the coroutine based example wasn't a problem, since it's in Legacy, and the mover system isn't fully being used yet. But, it's there for people to play with if they think they can do more with the design or maybe someone can get an idea of something not done yet.

The side effects of what you mentioned are one thing you have to indeed consider with such a change. Remember the way PlayerMover is currently used is in context of the Legacy design, so any code that isn't using the player mover as intended would certainly want to be updated. While I don't foresee the task change to MoveTowards, the new setup is pretty solid and I'm happy with that, and it at least makes both the old style of moving as well as the potential for coroutine moving more accessible, so that won't change.

I'll have another thread posted in the Community Developer section coming soon about 3.3 updates and I'll mention all this stuff again.
 
Back
Top