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:
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
After
OldPlayerMover will now have settings and a GUI:
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:
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.
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.
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:
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: