//-----------------------------------------------------------------------
//
// This code file, and this entire plugin, is uncopyrighted. This means
// I've put them in the public domain, and released my copyright on all
// these works. There is no need to email me for permission -- use my
// content however you want! Email it, share it, reprint it with or
// without credit. Change it around, break it, and attribute it to me.
// It's okay. Attribution is appreciated, but not required.
//
// Zane McFate
//-----------------------------------------------------------------------
namespace GatherAssist
{
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Timers;
using System.Windows.Forms;
using System.Windows.Media;
using System.Xml.Linq;
using ff14bot;
using ff14bot.Enums;
using ff14bot.Helpers;
using ff14bot.Interfaces;
using ff14bot.Managers;
using ff14bot.NeoProfiles;
using ff14bot.Objects;
using ff14bot.Settings;
using Settings;
using Action = TreeSharp.Action;
///
/// RebornBuddy plugin for allowing the gathering of multiple counts of multiple items, from various gathering classes.
///
public class GatherAssist : IBotPlugin
{
///
/// A code indicating that an item record slot number means nothing, and should not be used.
///
public const int NOSLOT = -1;
///
/// The maximum number of gear sets possible in FFXIV. May need to adjust this as new classes are added.
///
private const int MaxGearSets = 35;
///
/// This is a required value for profile building, and does not appear to need much adjusting for gathering profiles, so it is
/// static for all profiles generated by this plugin.
///
private const int KillRadius = 50;
///
/// A code indicating that the current gather item was unable to complete and should be skipped.
///
private const int BADITEM = 99999;
///
/// Settings for this plugin which should be saved for later use.
///
private static GatherAssistSettings settings = GatherAssistSettings.Instance;
///
/// The timer used to periodically check on the gathering status and guide the engine in the proper direction.
///
private static System.Timers.Timer gatherAssistTimer = new System.Timers.Timer();
///
/// The list of all current gather requests. Populated by the plugin entry form and status maintained during plugin execution.
///
private List requestList;
///
/// The current gather request. Used for quick reference to current execution parameters.
///
private GatherRequest currentGatherRequest = null;
///
/// A table containing all possible maps, organized by aetheryte ID. Entries should be unique.
///
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Aetheryte is a FFXIV term.")]
private DataTable mapsTable;
///
/// A table containing all items which can be gathered by this plugin. Includes values necessary to construct and execute
/// gathering profiles.
///
private DataTable itemsTable;
///
/// The form for user-provided settings.
///
private GatherAssist_Form form;
///
/// The number of iterations the timer has made so far.
///
private int timerIterations = 0;
///
/// Whether or not to reload the profile on the next pulse. Used for adjusting the current gather spell.
///
private bool reloadFlag = false;
///
/// Gets the author of this plugin.
///
public string Author
{
get { return " Zane McFate"; }
}
///
/// Gets the description of the plugin.
///
public string Description
{
get { return "Extends OrderBot gathering functionality to seek multiple items with a single command."; }
}
///
/// Gets the current plugin version.
///
public Version Version
{
get { return new Version(1, 0, 1); }
}
///
/// Gets the plugin name.
///
public string Name
{
get { return "GatherAssist"; }
}
///
/// Gets a value indicating whether we want a settings button. True because we do want a button.
///
public bool WantButton
{
get { return true; }
}
///
/// Gets the text value for the plugin's requisite button.
///
public string ButtonText
{
get { return this.Name; }
}
///
/// Gets the color used for log messages which are meant to be visible and important.
///
private static Color LogMajorColor
{
get { return Colors.SkyBlue; }
}
///
/// Gets the color used for log message which are less important. Also for debug messages.
///
private static Color LogMinorColor
{
get { return Colors.Teal; }
}
///
/// Gets the color used for log message indicating problems with the plugin.
///
private static Color LogErrorColor
{
get { return Colors.Red; }
}
///
/// Gets or sets a custom gathering spell override to the blindly-selected spell.
/// This can only be determined during a pulse, so every new profile starts with
/// this value set as null.
///
private string GatheringSpellOverride { get; set; }
///
/// Allows the program to send key strokes directly to FFXIV.
///
/// Handle for the FFXIV window.
/// The instruction. Key down or key up for this purpose.
/// The key which should be sent.
/// Not used.
/// The result of the message. Not used in this program.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1306:FieldNamesMustBeginWithLowerCaseLetter", Justification = "Using Win32 naming for consistency.")]
[DllImport("user32.dll")]
public static extern IntPtr PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
///
/// Escapes special characters in data table query syntax.
///
/// The field value to be fixed.
/// The number of times values should be escaped. This allows use in double escape situations.
/// The updated field value.
public static string FixQueryField(string field, int numberTimes)
{
string returnValue = field;
for (int i = 0; i < numberTimes; i++)
{
returnValue = returnValue.Replace("'", "''");
}
return returnValue;
}
///
/// Handles the IBotPlugin.OnButtonPress event. Code executed when the user pushes the requisite button for this plugin.
/// Initializes the settings form to gather required parameters for the next gathering attempt.
///
public void OnButtonPress()
{
try
{
if (this.form == null || this.form.IsDisposed || this.form.Disposing)
{
this.form = new GatherAssist_Form(this.itemsTable);
}
this.form.ShowDialog();
// don't alter anything if the user cancelled the form
if (this.form.DialogResult == DialogResult.OK)
{
this.InitializeRequestList(this.form.RequestTable); // reinitialize from updated settings
gatherAssistTimer.Interval = settings.UpdateIntervalMinutes * 60000;
gatherAssistTimer.Start();
this.ElapseTimer(); // immediately elapse timer to check item counts and set correct profile
}
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Used to compare this plugin to other plugins. Not currently implemented.
///
/// The parameter is not used.
/// The parameter is not used.
public bool Equals(IBotPlugin other)
{
throw new NotImplementedException();
}
///
/// Handles the IBotPlugin.OnInitialize event. Initializes data tables, initializes required settings, and prepares the timer for
/// future execution.
///
public void OnInitialize()
{
try
{
this.InitializeItems();
this.InitializeMaps();
// Initialize all settings to default values if necessary
if (settings.UpdateIntervalMinutes == 0)
{
settings.UpdateIntervalMinutes = 1;
}
if (settings.AutoSkipInterval < 1)
{
settings.AutoSkipInterval = 1;
}
gatherAssistTimer.Elapsed += this.GatherAssistTimer_Elapsed;
gatherAssistTimer.Interval = settings.UpdateIntervalMinutes * 60000;
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Handles the IBotPlugin.OnShutdown event. Currently does nothing.
///
public void OnShutdown()
{
}
///
/// Handles the IBotPlugin.OnEnabled event. Current shows the plugin version, but does nothing else.
///
public void OnEnabled()
{
try
{
this.Log(LogMajorColor, " v" + Version.ToString() + " Enabled");
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Handles the IBotPlugin.OnDisabled event. Reports the plugin version and stops iteration of the gather timer.
///
public void OnDisabled()
{
try
{
this.Log(LogMajorColor, " v" + Version.ToString() + " Disabled");
gatherAssistTimer.Stop();
//// TODO: Assess whether stopping the bot is the best idea here. Perhaps we should see whether this plugin was executing logic?
this.BotStop();
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Handles the IBotPlugin.OnPulse event. Currently does nothing.
///
public void OnPulse()
{
if (settings.SummonChocobo && !Chocobo.Summoned && Chocobo.CanSummon)
{
Chocobo.Summon();
}
if (this.reloadFlag && !GatheringManager.WindowOpen)
{
this.LoadProfile(true);
return;
}
// only run logic if no gathering spell override has been selected. Only works if the gather window is open.
if (GatheringManager.WindowOpen && string.IsNullOrEmpty(this.GatheringSpellOverride))
{
// get the correct target gathering item
GatheringItem targetGatheringItem = null;
foreach (GatheringItem gatheringItem in GatheringManager.GatheringWindowItems)
{
if (gatheringItem.ItemData.EnglishName == this.currentGatherRequest.ItemName)
{
targetGatheringItem = gatheringItem;
break;
}
}
if (targetGatheringItem == null)
{
if (!this.currentGatherRequest.ItemName.Contains("Crystal"))
{
this.Log(LogErrorColor, "The target gathering item could not be ascertained from this window; is there a problem with the profile?");
}
return;
}
if (targetGatheringItem.Chance <= 50 && Core.Me.ClassLevel >= 10)
{
this.GatheringSpellOverride = Core.Me.CurrentJob.ToString() == "Miner" ? "Sharp Vision III" : "Field Mastery III";
}
else if (targetGatheringItem.Chance <= 85 && Core.Me.ClassLevel >= 5)
{
this.GatheringSpellOverride = Core.Me.CurrentJob.ToString() == "Miner" ? "Sharp Vision II" : "Field Mastery II";
}
else if (targetGatheringItem.Chance <= 95 && Core.Me.ClassLevel >= 4)
{
this.GatheringSpellOverride = Core.Me.CurrentJob.ToString() == "Miner" ? "Sharp Vision" : "Field Mastery I";
}
else if (targetGatheringItem.Chance == 100)
{
if (Core.Me.ClassLevel >= 40)
{
this.GatheringSpellOverride = Core.Me.CurrentJob.ToString() == "Miner" ? "King's Yield II" : "Blessed Harvest II";
}
else if (Core.Me.ClassLevel >= 30)
{
this.GatheringSpellOverride = Core.Me.CurrentJob.ToString() == "Miner" ? "King's Yield" : "Blessed Harvest II";
}
else if (Core.Me.ClassLevel >= 20)
{
this.GatheringSpellOverride = Core.Me.CurrentJob.ToString() == "Miner" ? "Solid Reason" : "Ageless Words";
}
}
// break logic if a new spell has been selected
if (!string.IsNullOrEmpty(this.GatheringSpellOverride))
{
this.Log(LogMajorColor, string.Format("Overriding current gathering spell with {0}", this.GatheringSpellOverride));
this.reloadFlag = true; // don't do anything on this pulse, as it would create a paradox while waiting for a window to close
}
}
}
///
/// Handles the gatherAssistTimer.Elapsed event. Runs the ElapseTimer function.
///
/// The parameter is not used.
/// The parameter is not used.
private void GatherAssistTimer_Elapsed(object sender, ElapsedEventArgs e)
{
try
{
this.ElapseTimer();
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Performs periodic actions to monitor and adjust the engine to complete the overall gathering task. Checks the current gather
/// request status and loads the next profile if necessary. If all gather requests have been fulfilled, moves plugin to finished
/// state.
///
private void ElapseTimer()
{
try
{
this.timerIterations += 1;
int lastCount = this.currentGatherRequest == null ? 0 : this.currentGatherRequest.CurrentCount;
string lastRequest = this.currentGatherRequest == null ? string.Empty : this.currentGatherRequest.ItemName;
this.UpdateRequestedItemCounts();
this.ReportGatheringStatus();
if (
this.currentGatherRequest != null
&& settings.AutoSkip
&& this.timerIterations % settings.AutoSkipInterval == 0
&& lastCount == this.currentGatherRequest.CurrentCount)
{
// this section reached if auto skip is on and the current request has been running for too long without results.
this.Log(LogErrorColor, string.Format("AutoSkip - Item {0} has been running too long and nothing has been gathered, flagging and moving on.", this.currentGatherRequest.ItemName));
this.FlagBadItem(this.currentGatherRequest);
this.UpdateRequestedItemCounts();
this.ReportGatheringStatus();
}
// if no valid gather requests remain, stop the plugin execution
if (this.currentGatherRequest == null)
{
var obj = this.requestList.FirstOrDefault(x => x.CurrentCount == BADITEM);
if (obj != null)
{
this.Log(LogErrorColor, "AutoSkip passed up requests which took too long to complete; these profiles may be bad, or you may not have the appropriate skills to gather these items:");
foreach (GatherRequest currentRequest in this.requestList)
{
if (currentRequest.CurrentCount == BADITEM)
{
this.Log(LogErrorColor, currentRequest.ItemName);
}
}
}
this.Log(LogMajorColor, "Gather requests complete! GatherAssist will stop now.");
gatherAssistTimer.Stop();
this.BotStop();
if (settings.SoundWhenFinished)
{
Console.Beep(1000, 500);
Console.Beep(1500, 500);
Console.Beep(1000, 1000);
}
if (settings.LogoutWhenFinished)
{
const uint WM_KEYDOWN = 0x100;
const uint WM_KEYUP = 0x0101;
IntPtr handle = Core.Memory.Process.MainWindowHandle;
ChatManager.SendChat("/shutdown");
Thread.Sleep(1000);
PostMessage(handle, WM_KEYDOWN, (IntPtr)Keys.NumPad4, IntPtr.Zero);
PostMessage(handle, WM_KEYUP, (IntPtr)Keys.NumPad4, IntPtr.Zero);
Thread.Sleep(1000);
PostMessage(handle, WM_KEYDOWN, (IntPtr)Keys.NumPad4, IntPtr.Zero);
PostMessage(handle, WM_KEYUP, (IntPtr)Keys.NumPad4, IntPtr.Zero);
Thread.Sleep(1000);
PostMessage(handle, WM_KEYDOWN, (IntPtr)Keys.NumPad0, IntPtr.Zero);
PostMessage(handle, WM_KEYUP, (IntPtr)Keys.NumPad0, IntPtr.Zero);
Thread.Sleep(1000);
}
return;
}
else if (this.currentGatherRequest.ItemName != lastRequest)
{
// Only load a profle if the item name has changed; this keeps profile from needlessly reloading.
this.LoadProfile(false);
}
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Converts a supplied request table into the gather request array for profile execution.
///
/// The data table holding all gather requests. Should contain the ItemName and Count fields.
private void InitializeRequestList(DataTable requestTable)
{
try
{
// acquire all possible item/aetheryteID combos for all items
this.itemsTable.DefaultView.Sort = "AetheryteId ASC, ItemName ASC";
DataTable allItemCombos = this.itemsTable.DefaultView.ToTable(true, "ItemName", "AetheryteId");
DataTable itemCombos = allItemCombos.Clone();
itemCombos.Columns.Add(new DataColumn() { ColumnName = "Count", DefaultValue = 0 });
foreach (DataRow curItemCombo in allItemCombos.Rows)
{
foreach (DataRow curRequest in requestTable.Rows)
{
if (curItemCombo["ItemName"] == curRequest["ItemName"])
{
itemCombos.Rows.Add(curItemCombo["ItemName"], curItemCombo["AetheryteId"], curRequest["Count"]);
}
}
}
int maxAetheryteID = 56;
int[] aetheryteCount = new int[maxAetheryteID + 1];
for (uint i = 1; i <= maxAetheryteID; i++)
{
// remove records from Aetheryte IDs that the user does not have available
DataRow[] rows = itemCombos.Select(string.Format("AetheryteId = '{0}'", i));
if (!WorldManager.HasAetheryteId(i))
{
foreach (DataRow row in rows)
{
itemCombos.Rows.Remove(row);
}
itemCombos.AcceptChanges();
}
aetheryteCount[i] = rows.Length;
}
// get number of item records for each aethernet ID
for (int i = 1; i <= maxAetheryteID; i++)
{
DataRow[] rows = itemCombos.Select(string.Format("AetheryteId = '{0}'", i));
aetheryteCount[i] = rows.Length;
}
int bestId;
int bestIdCount;
do
{
// find aetheryte with the highest number of item requests
bestId = 0;
bestIdCount = 0;
for (int i = 1; i <= maxAetheryteID; i++)
{
if (aetheryteCount[i] > bestIdCount)
{
bestIdCount = aetheryteCount[i];
bestId = i;
}
}
// for all item names in the current "best Aethernet ID", select all item records for those names that have other Aethernet IDs, and remove them from the preferred list.
if (bestId != 0)
{
DataRow[] selectedItems = itemCombos.Select(string.Format("AetheryteId = '{0}'", bestId));
foreach (DataRow row in selectedItems)
{
if (row.RowState == DataRowState.Deleted)
{
MessageBox.Show("Deleted");
}
DataRow[] deleteItems = itemCombos.Select(string.Format("ItemName = '{0}' AND AetheryteId <> '{1}'", FixQueryField(Convert.ToString(row["ItemName"]), 1), bestId));
foreach (DataRow deleteRow in deleteItems)
{
itemCombos.Rows.Remove(deleteRow);
}
itemCombos.AcceptChanges();
}
aetheryteCount[bestId] = 0; // clear count so this doesn't cycle again
}
}
while (bestId != 0);
// TODO: validate parameter requestTable to fit the parameter description.
this.requestList = new List();
foreach (DataRow dataRow in itemCombos.Rows)
{
this.Log(LogMajorColor, "Adding " + dataRow["ItemName"] + " to request list", true);
this.requestList.Add(new GatherRequest(Convert.ToString(dataRow["ItemName"]), Convert.ToUInt32(dataRow["AetheryteId"]), Convert.ToInt32(dataRow["Count"])));
}
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Flags the specified gather request as unable to complete, allowing the bot to continue to the next request.
///
/// The gather requests referring to the trouble item.
private void FlagBadItem(GatherRequest gatherRequest)
{
foreach (GatherRequest curRequest in this.requestList)
{
if (gatherRequest.ItemName == curRequest.ItemName)
{
curRequest.CurrentCount = BADITEM;
}
}
}
///
/// Updates item counts for all requested items.
///
private void UpdateRequestedItemCounts()
{
try
{
this.currentGatherRequest = null; // reset current gather request, will be set to first valid request below
foreach (GatherRequest curRequest in this.requestList)
{
if (curRequest.CurrentCount != BADITEM)
{
curRequest.CurrentCount = 0;
}
else
{
continue;
}
if (settings.HqOnly)
{
curRequest.CurrentCount = ConditionParser.HqItemCount(curRequest.ItemName);
}
else
{
curRequest.CurrentCount = ConditionParser.ItemCount(curRequest.ItemName);
}
}
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Lists the gathering status of all requested items. Assigns a valid gather request for continuing work.
///
private void ReportGatheringStatus()
{
try
{
foreach (GatherRequest curRequest in this.requestList)
{
Color logColor;
if (curRequest.RequestedTotal == BADITEM)
{
logColor = LogErrorColor;
}
else if (curRequest.RequestedTotal <= curRequest.CurrentCount)
{
logColor = LogMinorColor;
}
else
{
logColor = LogMajorColor;
}
this.Log(logColor, string.Format("Item: {0}, Count: {1}, Requested: {2}", curRequest.ItemName, curRequest.CurrentCount, curRequest.RequestedTotal), true);
if (this.currentGatherRequest == null && curRequest.CurrentCount < curRequest.RequestedTotal && curRequest.CurrentCount != BADITEM)
{
this.Log(LogMajorColor, string.Format("Updating gather request to {0}", curRequest.ItemName), true);
this.currentGatherRequest = curRequest;
}
}
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Loads a profile to handle the current gather request.
///
/// Whether or not to use the gathering spell override when building this profile. This value should only be set after the first profile load for a particular item.
private void LoadProfile(bool spellOverride)
{
try
{
this.reloadFlag = false; // reset reload flag, whether or not the override is being triggered
this.timerIterations = 0; // reset iterations for AutoSkip feature
bool isValid = true;
if (this.currentGatherRequest == null)
{
this.Log(LogErrorColor, string.Format("Error: LoadProfile was executed without an active gather request; this should not be done. Shutting down {0} plugin.", this.Name));
isValid = false;
}
this.Log(LogMajorColor, string.Format("Current Gather Request is {0}", this.currentGatherRequest.ItemName), true);
ItemRecord itemRecord = this.GetItemRecord(this.currentGatherRequest.ItemName, this.currentGatherRequest.AetheryteId);
if (itemRecord == null)
{
this.Log(LogErrorColor, string.Format("Error: item {0} cannot be located. A new items entry must be created for this gather request to function properly.", this.currentGatherRequest.ItemName));
isValid = false;
}
if (!isValid)
{
gatherAssistTimer.Stop();
this.BotStop();
}
else
{
// stop the bot temporarily to allow for possible class changes. Also required for a profile load workaround, as the
// bot does not update item names properly during a "live" profile swap.
this.BotStop();
this.SetClass(itemRecord.ClassName); // switch class if necessary
string gatheringSpell;
if (spellOverride)
{
gatheringSpell = this.GatheringSpellOverride;
}
else
{
gatheringSpell = this.GetGatheringSpell(itemRecord); // get a gathering spell appropriate for this class
// don't allow shard/HQ spells to be overridden; automatic determination is already the best spell
if (itemRecord.ItemName.Contains("Shard") || itemRecord.ItemName.Contains("Crystal") || settings.HqOnly)
{
this.GatheringSpellOverride = gatheringSpell;
}
this.GatheringSpellOverride = null; // reset this value, since a new profile is being loaded
}
int timesToCast = (gatheringSpell == "Prospect" || gatheringSpell == "Triangulate") ? 2 : 1;
string nameSlotSection = string.Empty; // to store the item name / slot portion of the profile
if (itemRecord.ItemName.Contains("Crystal"))
{
// special handling for crystal profiles, since crystal/shard position moves
nameSlotSection = string.Format("{0}{1}", itemRecord.ItemName, itemRecord.ItemName.Replace("Crystal", "Shard"));
}
else if (itemRecord.SlotNumber == NOSLOT)
{
// if there is no valid slot number, use item naming logic instead
nameSlotSection = string.Format("{0}", itemRecord.ItemName);
}
else
{
// standard definitions, explicit slot number
nameSlotSection = string.Format("{0}", itemRecord.SlotNumber);
}
// construct profile using the chosen item record
string xmlContent = string.Format(
"{1}{2}{6}" +
"{9}" +
"",
settings.AutoEquip ? "1" : "0",
string.Format("{0}: {1}", itemRecord.ClassName, itemRecord.ItemName),
KillRadius,
itemRecord.MapNumber,
itemRecord.AetheryteName,
itemRecord.AetheryteId,
itemRecord.GatherObject,
itemRecord.HotspotRadius,
itemRecord.Location,
nameSlotSection,
gatheringSpell,
timesToCast);
string targetXmlFile = Path.Combine(GlobalSettings.Instance.PluginsPath, "GatherAssist/Temp/gaCurrentProfile.xml");
FileInfo profileFile = new FileInfo(targetXmlFile);
profileFile.Directory.Create(); // If the directory already exists, this method does nothing.
File.WriteAllText(profileFile.FullName, xmlContent);
while (ff14bot.Managers.GatheringManager.WindowOpen)
{
this.Log(LogMinorColor, "waiting for a window to close...", true);
Thread.Sleep(1000);
}
NeoProfileManager.Load(targetXmlFile, true); // profile will automatically switch to the new gathering profile at this point
Thread.Sleep(1000);
TreeRoot.Start();
}
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Populates map records for aetheryte teleporting.
///
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Aetheryte is a FFXIV term.")]
private void InitializeMaps()
{
try
{
this.mapsTable = Content.CreateMapsTable();
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Populates the items table with gatherable items and various required values on where/how to obtain them.
///
private void InitializeItems()
{
try
{
this.itemsTable = Content.CreateItemsTable();
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Retrieves the full item record for the supplied item name.
///
/// The name of the item being searched.
/// The ItemRecord for the supplied item name. Null if no item name can be found in the item table.
/// The preferred aetheryte ID where this item should be gathered.
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Aethernet is a FFXIV term.")]
private ItemRecord GetItemRecord(string itemName, uint aetheryteId)
{
try
{
bool isValid = true;
DataRow[] itemRows = this.itemsTable.Select(string.Format("ItemName = '{0}' AND AetheryteId = '{1}'", FixQueryField(itemName, 1), aetheryteId));
int itemCount = itemRows.Count();
if (itemCount == 0)
{
this.Log(LogErrorColor, string.Format("CONTACT DEVELOPER! Requested item name {0} does not exist in the item table; plesae create a record for this item before continuing.", itemName));
gatherAssistTimer.Stop();
this.BotStop();
}
else
{
foreach (DataRow curRow in itemRows)
{
if (WorldManager.HasAetheryteId(Convert.ToUInt32(curRow["AetheryteId"])))
{
DataRow itemRow = itemRows[0];
ItemRecord itemRecord = new ItemRecord();
itemRecord.ItemName = Convert.ToString(itemRow["ItemName"]);
itemRecord.ClassName = Convert.ToString(itemRow["ClassName"]);
itemRecord.AetheryteId = Convert.ToInt32(itemRow["AetheryteId"]);
itemRecord.GatherObject = Convert.ToString(itemRow["GatherObject"]);
itemRecord.ObjectLevel = Convert.ToInt32(itemRow["ObjectLevel"]);
itemRecord.HotspotRadius = Convert.ToInt32(itemRow["HotspotRadius"]);
itemRecord.Location = Convert.ToString(itemRow["Location"]);
itemRecord.SlotNumber = Convert.ToInt32(itemRow["SlotNumber"]);
DataRow[] mapRows = this.mapsTable.Select(string.Format("AetheryteId = '{0}'", itemRecord.AetheryteId));
int mapCount = mapRows.Count();
if (mapCount > 1)
{
this.Log(LogErrorColor, string.Format("CONTACT DEVELOPER! Requested Aetheryte ID {0} exists in {1} records; remove duplicates for this aetheryte before continuing.", itemRecord.AetheryteId, mapCount));
isValid = false;
}
else if (mapCount == 0)
{
this.Log(LogErrorColor, string.Format("CONTACT DEVELOPER! Requested Aetheryte ID {0} does not exist in the maps table; please create a record for this aetheryte before continuing.", itemRecord.AetheryteId));
isValid = false;
}
if (!isValid)
{
gatherAssistTimer.Stop();
this.BotStop();
}
else
{
DataRow mapRow = mapRows[0];
itemRecord.AetheryteName = Convert.ToString(mapRow["AetheryteName"]);
itemRecord.MapNumber = Convert.ToInt32(mapRow["MapNumber"]);
return itemRecord; // return completed itemRow
}
}
else
{
this.Log(LogMinorColor, string.Format("Aetheryte ID {0} is not available, skipping this item record"), true);
}
}
this.Log(LogErrorColor, string.Format("No items records existed that could be gathered by this user. Please check available Aetheryte IDs and update before continuing.", itemName));
gatherAssistTimer.Stop();
this.BotStop();
}
}
catch (Exception ex)
{
this.LogException(ex);
}
return null; // if valid ItemRecord was not returned or error was thrown, return null here
}
///
/// Safely stops the bot. Used for "pause" the bot to perform actions which are difficult or impossible to perform
/// while a profile is executing.
///
private void BotStop()
{
while (ff14bot.Managers.GatheringManager.WindowOpen)
{
this.Log(LogMinorColor, "waiting for a window to close...", true);
Thread.Sleep(1000);
}
TreeRoot.Stop(); // stop the bot
Thread.Sleep(1000); // give time for the change to register
}
///
/// Logs any exceptions encountered during plugin functions. Stops the plugin timer and the bot.
///
/// The exception which should be communicated in the log.
private void LogException(Exception ex)
{
this.Log(LogErrorColor, string.Format("Exception in plugin {0}: {1} {2}", this.Name, ex.Message, ex.StackTrace));
gatherAssistTimer.Stop();
this.BotStop();
}
///
/// Logs a message from the plugin. Attaches the plugin name to the message.
///
/// The color to use in the log.
/// The message to log.
private void Log(Color color, string message)
{
this.Log(color, message, false);
}
///
/// Logs a message from the plugin. Attaches the plugin name to the message.
///
/// The color to use in the log.
/// The message to log.
/// When true, appends a DEBUG: tag to the log message.
private void Log(Color color, string message, bool debug)
{
// don't output debug messages only Debug is on.
if (debug && !settings.Debug)
{
return;
}
if (debug)
{
message = "DEBUG: " + message;
}
Logging.Write(color, string.Format("[{0}] {1}", this.Name, message));
}
///
/// Changes the current class to the supplied class, if the character is not already that class.
///
/// The class to change to.
private void SetClass(string newClass)
{
try
{
if (Core.Me.CurrentJob.ToString() == newClass)
{
this.Log(LogMajorColor, string.Format("Class {0} is already chosen, bypassing SetClass logic", newClass), true);
return;
}
bool gearSetsUpdated = false;
// make sure gear sets exist
if (settings.GearSets == null)
{
this.Log(LogMinorColor, "Gear sets are null, updating for initial list", true);
this.UpdateGearSets();
gearSetsUpdated = true; // make sure gear sets are not updated again in this script
}
int targetGearSet = 0;
while (true)
{
for (int i = 0; i < settings.GearSets.Length; i++)
{
////this.Log(LogMinorColor, string.Format("TROUBLESHOOTING: Comparing desired class {0} to current GearSet class {1}", newClass, settings.GearSets[i]), true); // remove
if (newClass == settings.GearSets[i])
{
this.Log(LogMinorColor, string.Format("Choosing gear set {0}", i + 1), true);
targetGearSet = i + 1;
break; // otherwise, it will pick the buggy last gear set
}
}
if (targetGearSet != 0)
{
////this.Log(LogMinorColor, string.Format("TROUBLESHOOTING: changing gear sets"), true);
ChatManager.SendChat(string.Format("/gs change {0}", targetGearSet));
Thread.Sleep(3000); // give the system time to register the class change
// if the class change didn't work, update gear sets; assuming the sets have been adjusted
////this.Log(LogMinorColor, string.Format("TROUBLESHOOTING: Comparing desired class {0} to current class {1}", newClass, Core.Me.CurrentJob.ToString()), true); // remove
if (newClass != Core.Me.CurrentJob.ToString())
{
this.Log(LogMajorColor, "Gear sets appear to have been adjusted, scanning gear sets for changes...");
this.UpdateGearSets();
gearSetsUpdated = true;
}
break;
}
if (gearSetsUpdated)
{
throw new ApplicationException(string.Format("No gear set is available for the specified job class {0}; please check your gear sets.", newClass));
}
else
{
// update gear sets, reloop to check again
this.UpdateGearSets();
gearSetsUpdated = true;
}
}
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Updates the list of gear sets for the current character. Do not overuse, as this requires changing into all gear sets and logging
/// the class type of the gear set.
///
private void UpdateGearSets()
{
try
{
int maxClasses = 20;
string[] gearSets = new string[maxClasses];
for (int i = 0; i < maxClasses; i++)
{
ChatManager.SendChat(string.Format("/gs change {0}", i + 1));
Thread.Sleep(3000); // give the system time to register the class change
gearSets[i] = Core.Me.CurrentJob.ToString();
// if current gear set is the same class type as the previous set, exit loop
if (i != 0 && gearSets[i] == gearSets[i - 1])
{
break;
}
}
settings.GearSets = gearSets; // save gear sets
this.Log(LogMajorColor, "Gear sets acquired:");
for (int i = 0; i < maxClasses; i++)
{
if (gearSets[i] != null)
{
this.Log(LogMajorColor, string.Format("{0}: {1}", i + 1, gearSets[i]));
}
}
}
catch (Exception ex)
{
this.LogException(ex);
}
}
///
/// Supplies an appropriate gathering spell for the current class and specified gather item.
///
/// The record for the gather item desired.
/// A single spell that will work for the specified class name.
private string GetGatheringSpell(ItemRecord itemRecord)
{
try
{
// handle shard-specific spells. Allows for any class, if the cross-class skills are enabled.
switch (itemRecord.ItemName)
{
case "Fire Shard":
case "Fire Crystal":
case "Fire Cluster":
if (Actionmanager.HasSpell("Nald'thal's Ward"))
{
return "Nald'thal's Ward";
}
break;
case "Lightning Shard":
case "Lightning Crystal":
case "Lightning Cluster":
if (Actionmanager.HasSpell("Byregot's Ward"))
{
return "Byregot's Ward";
}
break;
case "Water Shard":
case "Water Crystal":
case "Water Cluster":
if (Actionmanager.HasSpell("Thaliak's Ward"))
{
return "Thaliak's Ward";
}
break;
case "Ice Shard":
case "Ice Crystal":
case "Ice Cluster":
if (Actionmanager.HasSpell("Menphina's Ward"))
{
return "Menphina's Ward";
}
break;
case "Wind Shard":
case "Wind Crystal":
case "Wind Cluster":
if (Actionmanager.HasSpell("Llymlaen's Ward"))
{
return "Llymlaen's Ward";
}
break;
case "Earth Shard":
case "Earth Crystal":
case "Earth Cluster":
if (Actionmanager.HasSpell("Nophica's Ward"))
{
return "Nophica's Ward";
}
break;
}
// handle HQ spells
if (settings.HqOnly)
{
if (Core.Me.ClassLevel >= 35)
{
return Core.Me.CurrentJob.ToString() == "Miner" ? "Unearth II" : "Leaf Turn II";
}
else if (Core.Me.ClassLevel >= 15)
{
return Core.Me.CurrentJob.ToString() == "Miner" ? "Unearth" : "Leaf Turn";
}
else
{
return Core.Me.CurrentJob.ToString() == "Miner" ? "Prospect" : "Triangulate";
}
}
// handle NQ spells
if (Core.Me.ClassLevel >= 10)
{
return Core.Me.CurrentJob.ToString() == "Miner" ? "Sharp Vision III" : "Field Mastery III";
}
else if (Core.Me.ClassLevel >= 5)
{
return Core.Me.CurrentJob.ToString() == "Miner" ? "Sharp Vision II" : "Field Mastery II";
}
else if (Core.Me.ClassLevel >= 4)
{
return Core.Me.CurrentJob.ToString() == "Miner" ? "Sharp Vision" : "Field Mastery";
}
else
{
return Core.Me.CurrentJob.ToString() == "Miner" ? "Prospect" : "Triangulate";
}
throw new ApplicationException(string.Format("CONTACT DEVELOPER! Could not determine a gathering spell for class {0} and item {1}; please update code.", Core.Me.CurrentJob.ToString(), itemRecord.ItemName));
}
catch (Exception ex)
{
this.LogException(ex);
return null;
}
}
}
}