I am a software developer but have never messed too much with the EB code (only very simple changes like changing a conditional statement). I'd like to build an auto-flask plugin that supports all the major flask affixes, like burning removal, bleeding removal, etc..., either by reading the affixes on the flasks (possible?) or by letting a user specify one or multiple flasks for various effects and then using them when the effect is found on my character.
Once I have this done, do you know if I can run that logic w/o the bot controlling the rest of my character, so that I could play manually but have it flask for me?
I tend to learn best by reading code examples, so is there anything similar already out there that I could read to get a start on this?
If you are a software developer, any of the buddy code should be a breeze to you.
Just open up the current autoflask plugin in notepad++ and check out what pushedx has done so far and just take it from there.
As far as playing by hand and the bot flasking for you.. you would just need to make a combatroutine that does not force any use of combat spells and just loads the basic bot settings and plugins.
Again just use exampleroutine as a guide and edit out anything that has to do with combat or movement.
Actually you should edit out the tasks instead of editing the combat routine. There are some explanations on the Guides sub-forum for devs. Sorry I'm on mobile, can't give more details.
Once I have this done, do you know if I can run that logic w/o the bot controlling the rest of my character, so that I could play manually but have it flask for me?
Not without losing control during the time you need to call API functions that perform input actions.
The API is entirely passive memory reading now (aside from a few safe client function calls that only get data, and don't generate any actions) and input emulated actions for the mouse and keyboard.
There's no way to use API functions that send emulated input/mouse actions while the user has control, because of how the game works (constantly updating, replacing old data with new). The API has to have input control via ProcessHookManager in order to work correctly, because the API is designed to detect when it doesn't have input control to avoid issues where the client doesn't do what is asked. This is why it's possible to still use your PC while running multiple copies of EB, since we don't take global control over system input, just local process.
For flasks, you can get by not using the API by simply using the Input.PressActionKey function (pass ActionKey.UseFlaskInSlot1-ActionKey.UseFlaskInSlot5) and it should work the same, since it simply generates a key press and doesn't actually need ProcessHookManager (unlike anything that works with the mouse). This is true of calling any of the lower level Input class functions to generate input events,
However, it will not work for anything mouse based, because the client requires the mouse to be in a valid position before it'll perform an action. For example, if you're mouse is too far away from the character, the game thinks you're trying to cast a skill at that location, and there is a limit to how far you can perform such actions, so the client won't cast it. When it comes to GUI interactions, the mouse position needs to be over the GUI element itself, which is not possible when the user has control, since the mouse will be where the user places it.
User input can currently interfere with the bot, since there's no way to block it, because the bot works the same way a user does. This is the downside to having an input emulated design, but it's the only possible way for a bot to work correctly in this game without causing all sorts of other issues with bans/flags and just the general things we've seen for the past year. You can still use external programs to send input to the client for the same exact reason. That's why external auto-pots work alongside EB, because they are sending global input the client still processes.
Here's the example I posted for the older API version that shows the idea behind using EB as a helper rather than fully automated. It won't compile as-is, but the concept it's using should be the same:
Code:
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Windows.Controls;
using Buddy.Coroutines;
using log4net;
using Loki.Bot;
using Loki.Bot.Logic.Bots.ExilebuddyBot;
using Loki.Bot.Logic.Bots.ExilebuddyBot.Evaluators;
using Loki.Bot.Logic.Bots.ExilebuddyBot.Logic;
using Loki.Bot.Pathfinding;
using Loki.Bot.v2;
using Loki.Bot.v2.Settings;
using Loki.Game;
using Loki.Game.GameData;
using Loki.Game.Objects;
using Loki.Utilities;
using System.Windows.Markup;
using System.IO;
using System;
using System.Threading.Tasks;
using System.Windows.Data;
namespace PickitBot
{
/// <summary> </summary>
public class OnLootEventArgs : EventArgs
{
/// <summary>The name of the item.</summary>
public string Name { get; set; }
/// <summary>The type of the item.</summary>
public string Type { get; set; }
/// <summary>The ilvl of the item.</summary>
public int ItemLevel { get; set; }
/// <summary>The rarity of the item.</summary>
public Rarity Rarity { get; set; }
internal OnLootEventArgs(string name, string type, int itemLevel, Rarity rarity)
{
Name = name;
Type = type;
ItemLevel = itemLevel;
Rarity = rarity;
}
}
/// <summary> </summary>
public class PickitBot : IBot
{
private static readonly ILog Log = Logger.GetLoggerInstanceForType();
/// <summary> </summary>
public PickitBot()
{
RequestStopEvent = new AutoResetEvent(false);
}
private Coroutine _coroutine;
/// <summary>An event handler for when the bot loots an item.</summary>
public static event EventHandler<OnLootEventArgs> OnLoot;
/// <summary>The item evaluator the bot should use.</summary>
public IItemEvaluator CurrentItemEvaluator { get; set; }
#region Implementation of IBot
/// <summary> Gets the name of this bot. </summary>
public string Name
{
get
{
return "PickitBot";
}
}
/// <summary> Gets the description of this bot. </summary>
public string Description
{
get
{
return "A bot that only does pickit logic.";
}
}
/// <summary>The event object used to stop the bot.</summary>
public AutoResetEvent RequestStopEvent { get; private set; }
/// <summary>Initializes this object. This is called when the object is loaded into the bot.</summary>
public void Initialize()
{
// Register the settings manager for this bot with the configuration manager.
Configuration.Instance.AddSettings(PickitBotSettings.Instance);
CurrentItemEvaluator = new DefaultItemEvaluator();
BotManager.OnBotChanged += BotManagerOnOnBotChanged;
}
/// <summary> The bot start callback. Do any initialization here. </summary>
public void OnStart()
{
Log.DebugFormat("[PickitBot] OnStart");
// We need to set this to avoid some hard dependencies.
ItemEvaluator.GetItemEvaluator = () => CurrentItemEvaluator;
ItemEvaluator.OnSaved += ItemEvaluatorOnOnSaved;
// Reset the default MsBetweenTicks on start.
MainSettings.Instance.MsBetweenTicks = 10;
Log.DebugFormat("[OnStart] MsBetweenTicks: {0}.", MainSettings.Instance.MsBetweenTicks);
// Since this bot will be performing client actions, we need to enable the process hook manager.
//LokiPoe.ProcessHookManager.Enable();
PluginManager.Start();
_coroutine = new Coroutine(() => MainCoroutine());
ExilePather.Enabled = false;
ExilePather.Locked = true;
AreaStateCache.Start();
OnLoot += OnOnLoot;
}
private void OnOnLoot(object sender, OnLootEventArgs onLootEventArgs)
{
Log.InfoFormat("[OnLoot] {0}:{1} {2} {3}", onLootEventArgs.Type, onLootEventArgs.Name,
onLootEventArgs.ItemLevel, onLootEventArgs.Rarity);
}
private void ItemEvaluatorOnOnSaved(object sender, ItemEvaluatorSavedEventArgs itemEvaluatorSavedEventArgs)
{
var items = BlacklistedItems;
foreach (var id in items)
{
AreaStateCache.Current.RemoveBlacklisting(id);
}
items.Clear();
}
/// <summary> The bot tick callback. Do any update logic here. </summary>
public void OnTick()
{
// Check to see if the coroutine is finished. If it is, stop the bot.
if (_coroutine.IsFinished)
{
var msg = String.Format("The bot coroutine has finished in a state of {0}", _coroutine.Status);
Log.DebugFormat(msg);
BotManager.Stop();
return;
}
PluginManager.Tick();
AreaStateCache.Tick();
_coroutine.Resume();
}
/// <summary> The bot stop callback. Do any pre-dispose cleanup here. </summary>
public void OnStop()
{
Log.DebugFormat("[PickitBot] OnStop");
PluginManager.Stop();
AreaStateCache.Stop();
ItemEvaluator.OnSaved -= ItemEvaluatorOnOnSaved;
OnLoot -= OnOnLoot;
// When the bot is stopped, we want to remove the process hook manager.
LokiPoe.ProcessHookManager.Disable();
// Cleanup the coroutine.
if (_coroutine != null)
{
_coroutine.Dispose();
_coroutine = null;
}
ExilePather.Locked = false;
ExilePather.Enabled = true;
}
/// <summary> The bot's settings control. This will be added to the Exilebuddy Settings tab.</summary>
public UserControl SettingsControl
{
get
{
using (var fs = new FileStream(@"Bots\PickitBot\SettingsGui.xaml", FileMode.Open))
{
var root = (UserControl)XamlReader.Load(fs);
// TODO: Your settings binding here.
if (!Wpf.SetupTextBoxBinding(root, "PickitRangeTextBox", "PickitRange",
BindingMode.TwoWay, PickitBotSettings.Instance))
{
Log.DebugFormat("[SettingsControl] SetupTextBoxBinding failed for 'PickitRangeTextBox'.");
throw new Exception("The SettingsControl could not be created.");
}
if (!Wpf.SetupTextBoxBinding(root, "MinimalMonsterRangeTextBox", "MinimalMonsterRange",
BindingMode.TwoWay, PickitBotSettings.Instance))
{
Log.DebugFormat("[SettingsControl] SetupTextBoxBinding failed for 'MinimalMonsterRangeTextBox'.");
throw new Exception("The SettingsControl could not be created.");
}
// TODO: Your settings event handlers here.
return root;
}
}
}
#endregion
#region Implementation of IDisposable
/// <summary> </summary>
public void Dispose()
{
// Unregister the settings manager for this bot with the configuration manager.
Configuration.Instance.RemoveSettings(PickitBotSettings.Instance);
}
#endregion
#region Override of Object
/// <summary>
///
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Name + ": " + Description;
}
#endregion
private void BotManagerOnOnBotChanged(object sender, BotChangedEventArgs botChangedEventArgs)
{
if (botChangedEventArgs.New == this)
{
// We need to set this to avoid some hard dependencies.
ItemEvaluator.GetItemEvaluator = () => CurrentItemEvaluator;
}
}
#region Coroutine Logic
private static readonly Dictionary<uint, List<int>> BlacklistedItemManager = new Dictionary<uint, List<int>>();
private static List<int> BlacklistedItems
{
get
{
var seed = LokiPoe.LocalData.TileHash;
List<int> list;
if (!BlacklistedItemManager.TryGetValue(seed, out list))
{
list = new List<int>();
BlacklistedItemManager.Add(seed, list);
}
return list;
}
}
private async Task MainCoroutine()
{
while (true)
{
if (LokiPoe.IsInGame)
{
// We don't need to execute in town.
if (!LokiPoe.Me.IsInTown)
{
// If we don't have an item evaluator, we cannot match items.
var eval = ItemEvaluator.GetItemEvaluator();
if (eval != null)
{
// Only execute when there are no active monster within 2x the picking range.
if (
!LokiPoe.ObjectManager.Objects.OfType<Monster>()
.Any(m => m.IsActive && m.Distance < PickitBotSettings.Instance.MinimalMonsterRange))
{
// Loop through all items we know about.
foreach (
var wi in
LokiPoe.ObjectManager.Objects.OfType<WorldItem>()
.Where(
wi =>
!AreaStateCache.Current.IsBlacklisted(wi) && !wi.IsAllocatedToOther)
)
{
// Check to see if we should loot the item based on filters.
ItemEvaluator.Filter filter;
if (!eval.Match(wi.Item, ItemEvaluator.EvaluationType.PickUp, out filter))
{
BlacklistedItems.Add(wi.Id);
AreaStateCache.Current.Blacklist(wi.Id, TimeSpan.FromHours(1),
"Does not match item evaluator.", false);
continue;
}
// If the item is within picking range, attempt to loot it.
if (wi.Distance < PickitBotSettings.Instance.PickitRange)
{
if (!LokiPoe.PlayerInventory.Main.CanFitItem(wi.Item.Size))
{
Log.DebugFormat("[MainCoroutine] Your inventory is full!");
BotManager.Stop();
break;
}
var id = wi.Id;
var position = wi.Position;
var evt = new OnLootEventArgs(wi.Name, wi.Type, wi.Item.ItemLevel,
wi.Item.Rarity);
// Enable the process hook manager so we can take over input and clear client key/mouse state.
LokiPoe.ProcessHookManager.Enable();
LokiPoe.ProcessHookManager.ResetCursor();
LokiPoe.ProcessHookManager.ClearAllKeyStates();
await Coroutine.Sleep(25);
// Try to stop the character from moving by sending another move action around the item's position.
await Coroutines.ClickToMove(position);
// Now try to interact. Blacklist the object so it doesn't get stuck on it.
var res = await Coroutines.InteractWithObject(id);
Log.DebugFormat("[MainCoroutine] InteractWithObject returned {0}.", res);
AreaStateCache.Current.Blacklist(id, TimeSpan.FromMilliseconds(500),
"Attempted to loot.", false);
// Finally, clear the input state and return input to the user,
LokiPoe.ProcessHookManager.ResetCursor();
LokiPoe.ProcessHookManager.ClearAllKeyStates();
LokiPoe.ProcessHookManager.Disable();
if (LokiPoe.ObjectManager.GetObjectById(id) == null)
{
// Trigger the OnLoot event. We assume we're the one that looted the item, but there's no real way to verify it.
LokiPoe.InvokeEvent(OnLoot, null, evt);
}
}
}
}
}
}
}
else
{
// Most likely in a loading screen, which will cause us to block on the executor,
// but just in case we hit something else that would cause us to execute...
await Coroutine.Sleep(1000);
continue;
}
// End of the tick.
await Coroutine.Yield();
}
// ReSharper disable once FunctionNeverReturns
}
#endregion
}
}
Basically, you'd be making a new IBot implementation where you don't globally enable the ProcessHookManager, so you keep control. Before you use API functions, you need to enable the ProcessHookManager and reset it so nothing persists into the API call. Once the API calls are done, you can disable it to regain control. There are some issues with this though, as if there's any exceptions or the like, you'll lose control briefly longer unless you press the hotkey to disable the PHM (alt+shift+d).
Take a look at the MainCoroutine logic for the actual PHM stuff. For flasks, you'd only need to do that if you want to go though the actual API and not manage the input events yourself though Input. However, you have to keep in mind the looming issue of control loss, which might make this less than appealing. There's no other way around it though.
As for the flask specific stuff, here's some example code that you can start with that should point you in the right direction:
Code:
public static IEnumerable<InventoryItem> GetFlasksByStat(StatType stat)
{
var inv = LokiPoe.PlayerInventory.Flasks.Items;
return from item in inv
let flask = item.Flask
where flask != null && item.Item.ExplicitStats.ContainsKey(stat) && flask.CanUse
select item;
}
// <summary> Returns usable flasks that can dispel bleeding. </summary>
public static IEnumerable<InventoryItem> BleedingFlasks
{
get { return GetFlasksByStat(StatType.LocalFlaskRemoveBleedingOnUse); }
}
/// <summary> Returns usable flasks that can dispel shock. </summary>
public static IEnumerable<InventoryItem> ShockFlasks
{
get { return GetFlasksByStat(StatType.LocalFlaskRemoveShockOnUse); }
}
/// <summary> Returns usable flasks that can dispel burning. </summary>
public static IEnumerable<InventoryItem> BurningFlasks
{
get { return GetFlasksByStat(StatType.LocalFlaskDispelsBurning); }
}
/// <summary> Returns usable flasks that can dispel frozen. </summary>
public static IEnumerable<InventoryItem> FrozenFlasks
{
get { return GetFlasksByStat(StatType.LocalFlaskDispelsFreezeAndChill); }
}
/// <summary> Returns usable flasks that can dispel frozen. </summary>
public static IEnumerable<InventoryItem> ChilledFlasks
{
get { return GetFlasksByStat(StatType.LocalFlaskDispelsFreezeAndChill); }
}
...
private bool FlaskHelper(Stopwatch sw, int flaskCdMs, IEnumerable<InventoryItem> flasks)
{
var useFlask = false;
if (!sw.IsRunning)
{
sw.Start();
useFlask = true;
}
else if (sw.ElapsedMilliseconds > Utility.LatencySafeValue(flaskCdMs))
{
sw.Restart();
useFlask = true;
}
if (useFlask)
{
var flask = flasks.FirstOrDefault();
if (flask != null)
{
flask.Use();
return true;
}
}
return false;
}
// Frozen
if (AutoFlaskSettings.Instance.UseFrozenFlasks && !AreaStateCache.Current.HasIceGround && LokiPoe.Me.IsFrozen)
{
if (FlaskHelper(_frozenFlaskCd, AutoFlaskSettings.Instance.FrozenFlaskCooldownMs, Utility.FrozenFlasks))
{
return;
}
}
// Bleeding
if (AutoFlaskSettings.Instance.UseBleedingFlasks && LokiPoe.Me.IsBleeding)
{
if (FlaskHelper(_bleedingFlaskCd, AutoFlaskSettings.Instance.BleedingFlaskCooldownMs, Utility.BleedingFlasks))
{
return;
}
}
// Lightning
if (AutoFlaskSettings.Instance.UseShockedFlasks && !AreaStateCache.Current.HasLightningGround && LokiPoe.Me.IsShocked)
{
if (FlaskHelper(_shockedFlaskCd, AutoFlaskSettings.Instance.ShockedFlaskCooldownMs, Utility.ShockFlasks))
{
return;
}
}
// Burning
if (AutoFlaskSettings.Instance.UseBurningFlasks && !AreaStateCache.Current.HasBurningGround && LokiPoe.Me.IsBurning)
{
if (FlaskHelper(_burningFlaskCd, AutoFlaskSettings.Instance.BurningFlaskCooldownMs, Utility.BurningFlasks))
{
return;
}
}
That's some code from the previous flask plugin before it was simplified for the current version. Basically, you just find the stats approporiate for the flask you want to handle, then setup the logic to trigger them based on the current area. Take into account areas that might have stats that cause something to trigger more than expected, as otherwise you'll burn though pots.
You can use the Inventory Explorer to help track down stats and stuff as well. make sure to read the How to Setup a Project for Exilebuddy guide as well, since you'll want to use the Object Browser.
hey all
i'm working on something like this, but with bot control. hand-levelling is the devil
changed the order of flask use, prioritizes those with more charges remaining. this is so flasks are used evenly instead of using a single flask fully then uses the next. helps with recharge
when using life flasks, it checks your status and uses appropriate status fixing flasks if available (dispels burning flask if you are burning etc)
removing curses triggers on frostbite, flammability, conductivity, elemental weakness & vulnerability
split life flask usage into two types, immediate heal (bubbling & seething) and duration, flasks which only heal over time
using a duration heal flask is hardcoded on below 90% life currently
using an immediate heal flask goes by the % set in autoflask options
this is to get around situations where you take damage, it uses a flask, you take more damage and it refuses to use an immediate heal flask, as the over time flask is still ongoing
next planned change is checking when life flasks are empty, and taking a portal to town to refill
anyone want to help with the logic on checking if all life flask are empty
or on making a portal, going to town so flasks refill, and taking the portal back to the grind zone?
next planned change is checking when life flasks are empty, and taking a portal to town to refill
anyone want to help with the logic on checking if all life flask are empty
or on making a portal, going to town so flasks refill, and taking the portal back to the grind zone?
For BasicGrindBot (LifeFlasks is the one in AutoFlask.cs):
Code:
if (LifeFlasks.Count() == 0)
{
BasicGrindBotSettings.Instance.NeedsTownRun = 1;
}
That's it! It should be easy to test as well, but that should work. You can add more logic like making sure to only run when not in town, etc... to avoid other issues, but that's the basics.
but queues up a town run for after combat
is there a way to do a town run instantly, or alternatively, to pause the combat routine temporarily?
this is so it can refill flasks during a boss fight etc
tried using things like Loki.Bot.Logic.Bots.BasicGrindBot.CombatTask.Stop() without success so far
That's correct. The current Task system, covered here, executes CombatTask and then TownRunTask.
The easy fix, without breaking everything would be to modify your CR to check BasicGrindBotSettings.Instance.NeedsTownRun and return false if it's not zero. That would force an instant Town Run, but you might notice side effects if you're being attacked and trying to take a portal. That is why combat is executed before the town run logic, as-is.