/// <summary>Gossips with the specified wow object. Hearthstone bind popups are automatically accepted</summary> /// <param name="wowObject">The wow object. Navigates to <paramref name="searchLocation" /> null </param> /// <param name="searchLocation">The search location of <paramref name="wowObject" />.</param> /// <param name="movementBy">The movement type to use.</param> /// <param name="navigationFailedAction"> /// The action to take if <paramref name="wowObject" /> or <paramref name="searchLocation"/> cant be navigated to /// </param> /// <param name="notFoundAction"> /// The action to take if <paramref name="wowObject" /> is not found at /// <paramref name="searchLocation" />. /// </param> /// <param name="noGossipFrameAction"> /// The action to take if interaction with <paramref name="wowObject" /> didn't open a /// gossip frame. /// </param> /// <param name="noMatchingGossipOptionAction"> /// <para>The action to take if the passed in gossip type and/or gossip indices </para> /// <para>doesn't match what was offered by <paramref name="wowObject" />.</para> /// </param> /// <param name="gossipEntryType"> /// <para>Type gossip entry type to select. Ignored if set to Unknown.</para> /// <para>If none of this type are found on current page then</para> /// <para> normal gossip types are clicked through in hopes of ending on a page with this gossip type</para> /// </param> /// <param name="gossipIndexes"> /// The gossip indexes to follow through. Has precedence over /// <paramref name="gossipEntryType" />. /// </param> /// <exception cref="Exception">A delegate callback throws an exception.</exception> public static async Task <bool> Gossip( WoWObject wowObject, Vector3 searchLocation, MovementByType movementBy = MovementByType.FlightorPreferred, Action navigationFailedAction = null, Action notFoundAction = null, Action noGossipFrameAction = null, Action noMatchingGossipOptionAction = null, GossipEntry.GossipEntryType gossipEntryType = GossipEntry.GossipEntryType.Unknown, params int[] gossipIndexes) { if (wowObject == null) { if (!Navigator.AtLocation(searchLocation)) { if (await MoveTo(searchLocation, "Gossip object search area", movementBy)) { return(true); } navigationFailedAction?.Invoke(); return(false); } if (notFoundAction != null) { notFoundAction(); } else { TreeRoot.StatusText = "Waiting for the WoW object selected for gossip to spawn"; } return(true); } if (!wowObject.WithinInteractRange) { if (await MoveTo(wowObject.Location, wowObject.SafeName, movementBy, wowObject.InteractRange)) { return(true); } navigationFailedAction?.Invoke(); return(false); } if (await CommonCoroutines.Dismount("Gossiping with " + wowObject.SafeName)) { await Coroutine.Sleep(Delay.BeforeButtonClick); } // If gossip frame is open then we must assume that it doesn't belong to the selected gossip object at this point if (GossipFrame.Instance.IsVisible) { GossipFrame.Instance.Close(); return(true); } Func <bool> isFrameReadyForInput = () => GossipFrame.Instance.IsVisible && (GossipFrame.Instance.GossipOptionEntries != null || (!gossipIndexes.Any() && gossipEntryType == GossipEntry.GossipEntryType.Unknown)); wowObject.Interact(); var openedGossipFrame = await Coroutine.Wait(3000, isFrameReadyForInput); if (!openedGossipFrame) { QBCLog.Warning("No gossip frame was opened after interacting with {0}", wowObject.SafeName); noGossipFrameAction?.Invoke(); return(false); } int gossipPage = 1; // Click through all the gossip indices for (var i = 0; i < gossipIndexes.Length; i++) { var index = gossipIndexes[i] - 1; var gossipEntry = GossipFrame.Instance.GossipOptionEntries.Where(g => g.Index == index) .Select(g => (GossipEntry?)g) .FirstOrDefault(); if (!gossipEntry.HasValue || gossipEntry.Value.Type == GossipEntry.GossipEntryType.Unknown) { QBCLog.Warning("{0} does not provide a gossip at index {1} on page {2}", wowObject.SafeName, index + 1, gossipPage); noMatchingGossipOptionAction?.Invoke(); return(false); } await ClickGossipOption(gossipEntry.Value, gossipPage); // make sure frame didn't close before we're done. if (!isFrameReadyForInput() && (i < gossipIndexes.Length - 1 || gossipEntryType != GossipEntry.GossipEntryType.Unknown)) { // This can happen if some external event causes object to stop offering gossip frame, such as NPC getting into combat. // Usually this can be fixed by interacting with object again at a later time. We let the caller handle this. QBCLog.Warning("Gossip frame for {0} closed unexpectedly.", wowObject.SafeName); return(true); } gossipPage++; } if (gossipEntryType != GossipEntry.GossipEntryType.Unknown) { if (!gossipIndexes.Any()) { while (true) { var gossipEntry = GossipFrame.Instance.GossipOptionEntries.FirstOrDefault(g => g.Type == gossipEntryType); // If no gossip indices were specified then we just click through more gossip, // hopefully it leads to the final gossip type if (gossipEntry.Type != gossipEntryType) { gossipEntry = GossipFrame.Instance.GossipOptionEntries.FirstOrDefault(g => g.Type == GossipEntry.GossipEntryType.Gossip); } if (gossipEntry.Type == GossipEntry.GossipEntryType.Unknown) { QBCLog.Warning("{0} does not provide a {0} gossip type", wowObject.SafeName, gossipEntryType); noMatchingGossipOptionAction?.Invoke(); return(false); } await ClickGossipOption(gossipEntry, gossipPage); if (!isFrameReadyForInput() && gossipEntry.Type != gossipEntryType) { // This can happen if some external event causes object to stop offering gossip frame, such as NPC getting into combat. // Usually this can be fixed by interacting with object again at a later time. We let the caller handle this. QBCLog.Warning("Gossip frame for {0} closed unexpectedly.", wowObject.SafeName); return(true); } if (gossipEntry.Type == gossipEntryType) { break; } gossipPage++; } } } // Set hearthstone automatically const string setHsPopupName = "CONFIRM_BINDER"; if (Lua.GetReturnVal <bool>($"return StaticPopup_Visible('{setHsPopupName}')", 0)) { uint hsId = StyxWoW.Me.HearthstoneAreaId; Lua.DoString( $"local _,frame = StaticPopup_Visible('{setHsPopupName}') if frame then StaticPopup_OnClick(frame, 1) end"); if (await Coroutine.Wait(5000, () => StyxWoW.Me.HearthstoneAreaId != hsId)) { await CommonCoroutines.SleepForRandomReactionTime(); var boundLocation = Lua.GetReturnVal <string>("return GetBindLocation()", 0); QBCLog.Info( "You are now bound at {0} Inn in {1}({2})", (Query.IsViable(wowObject) ? wowObject.SafeName : "the"), boundLocation, Me.HearthstoneAreaId); } } return(true); }
public static async Task <SpellCastResult> CastSpell( int spellId, WoWObject target = null, System.Action actionOnSuccessfulSpellCastDelegate = null) { // Viable target? // If target is null, then assume 'self'. // NB: Since target may go invalid immediately upon casting the spell, // we cache its name for use in subsequent log entries. var selectedObject = target ?? Me; if (!Query.IsViable(selectedObject)) { QBCLog.Warning("Target is not viable!"); return(SpellCastResult.InvalidTarget); } var targetName = selectedObject.SafeName; // Target must be a WoWUnit for us to be able to cast a spell on it... var selectedTarget = selectedObject as WoWUnit; if (!Query.IsViable(selectedTarget)) { QBCLog.Warning("Target {0} is not a WoWUnit--cannot cast spell on it.", targetName); return(SpellCastResult.InvalidTarget); } // Spell known? WoWSpell selectedSpell = WoWSpell.FromId(spellId); if (selectedSpell == null) { QBCLog.Warning("{0} is not known.", Utility.GetSpellNameFromId(spellId)); return(SpellCastResult.SpellNotKnown); } var spellName = selectedSpell.Name; // Need to be facing target... // NB: Not all spells require this, but many do. Utility.Target(selectedTarget, true); // Wait for spell to become ready... if (!SpellManager.CanCast(selectedSpell)) { QBCLog.Warning( "{0} is not usable, yet. (cooldown remaining: {1})", spellName, Utility.PrettyTime(selectedSpell.CooldownTimeLeft)); return(SpellCastResult.NotReady); } // Notify user of intent... var message = string.Format("Attempting cast of '{0}' on '{1}'", spellName, targetName); message += selectedTarget.IsDead ? " (dead)" : string.Format(" (health: {0:F1})", selectedTarget.HealthPercent); QBCLog.DeveloperInfo(message); // Set up 'interrupted use' detection, and cast spell... using (var castMonitor = SpellCastMonitor.Start(spellId)) { SpellManager.Cast(selectedSpell, selectedTarget); // NB: The target or the spell may not be valid after this point... // Some targets will go 'invalid' immediately afer interacting with them. // Most of the time this happens, the target is immediately and invisibly replaced with // an identical looking target with a different script. // We must assume our target and spell is no longer available for use after this point. await Coroutine.Sleep((int)Delay.AfterItemUse.TotalMilliseconds); // If item use requires a second click on the target (e.g., item has a 'ground target' mechanic)... await CastPendingSpell(selectedTarget); // Wait for any casting to complete... // NB: Some interactions or item usages take time, and the WoWclient models this as spellcasting. var castResult = await castMonitor.GetResult(); if (castResult != SpellCastResult.Succeeded) { string reason = castResult == SpellCastResult.UnknownFail ? castMonitor.FailReason : castResult.ToString(); QBCLog.Warning("Cast of {0} failed. Reason: {1}", spellName, reason); // Give whatever issue encountered a chance to settle... // NB: --we want the Sequence to fail when delay completes. if (castResult != SpellCastResult.LineOfSight && castResult != SpellCastResult.OutOfRange && castResult != SpellCastResult.TooClose) { await Coroutine.Sleep(1500); } return(castResult); } QBCLog.DeveloperInfo("Cast of '{0}' on '{1}' succeeded.", spellName, targetName); if (actionOnSuccessfulSpellCastDelegate != null) { actionOnSuccessfulSpellCastDelegate(); } return(SpellCastResult.Succeeded); } }
/// <summary> /// <para>This reports problems, and stops BT processing if there was a problem with attributes... /// We had to defer this action, as the 'profile line number' is not available during the element's /// constructor call.</para> /// <para>It also captures the user's configuration, and installs Behavior Tree hooks. The items will /// be restored when the behavior terminates, or Honorbuddy is stopped.</para> /// </summary> /// <return>true, if the behavior should run; false, if it should not.</return> /// <param name="extraGoalTextDescription"></param> protected bool OnStart_QuestBehaviorCore(string extraGoalTextDescription = null) { // Semantic coherency / covariant dependency checks... UsageCheck_SemanticCoherency(Element, ((QuestObjectiveIndex > 0) && (QuestId <= 0)), context => string.Format("QuestObjectiveIndex of '{0}' specified, but no corresponding QuestId provided", QuestObjectiveIndex)); EvaluateUsage_SemanticCoherency(Element); // Deprecated attributes... EvaluateUsage_DeprecatedAttributes(Element); // This reports problems, and stops BT processing if there was a problem with attributes... // We had to defer this action, as the 'profile line number' is not available during the element's // constructor call. OnStart_HandleAttributeProblem(); // If the quest is complete, this behavior is already done... // So we don't want to falsely inform the user of things that will be skipped. // NB: Since the IsDone property may skip checking the 'progress conditions', we need to explicltly // check them here to see if we even need to start the behavior. if (!(IsDone || !UtilIsProgressRequirementsMet(QuestId, QuestRequirementInLog, QuestRequirementComplete))) { this.UpdateGoalText(QuestId, extraGoalTextDescription); // Start the timer to measure the behavior run time... _behaviorRunTimer.Restart(); // Monitored Behaviors... if (QuestBehaviorCoreSettings.Instance.MonitoredBehaviors.Contains(GetType().Name)) { QBCLog.Debug("MONITORED BEHAVIOR: {0}", GetType().Name); AudibleNotifyOn(true); } _configMemento = CreateConfigMemento(); if (Targeting.Instance != null) { Targeting.Instance.IncludeTargetsFilter += TargetFilter_IncludeTargets; Targeting.Instance.RemoveTargetsFilter += TargetFilter_RemoveTargets; Targeting.Instance.WeighTargetsFilter += TargetFilter_WeighTargets; } Query.InCompetitionReset(); Utility.BlacklistsReset(); _behaviorTreeHook_CombatMain = BehaviorHookInstall("Combat_Main", CreateBehavior_CombatMain()); _behaviorTreeHook_CombatOnly = BehaviorHookInstall("Combat_Only", CreateBehavior_CombatOnly()); _behaviorTreeHook_DeathMain = BehaviorHookInstall("Death_Main", CreateBehavior_DeathMain()); _behaviorTreeHook_QuestbotMain = BehaviorHookInstall("Questbot_Main", CreateBehavior_QuestbotMain()); BlackspotManager.AddBlackspots(_temporaryBlackspots.GetBlackspots()); if (_temporaryAvoidMobs != null) { foreach (var avoidMobId in _temporaryAvoidMobs.GetAvoidMobIds()) { // NB: ProfileManager.CurrentProfile.AvoidMobs will never be null if (!ProfileManager.CurrentProfile.AvoidMobs.Contains(avoidMobId)) { ProfileManager.CurrentProfile.AvoidMobs.Add(avoidMobId); } } } return(true); // behavior should run } return(false); // behavior should NOT run }
protected QuestBehaviorBase(Dictionary <string, string> args) : base(args) { QBCLog.BehaviorLoggingContext = this; try { // Quest handling... // QuestRequirement* attributes are explained here... // http://www.thebuddyforum.com/mediawiki/index.php?title=Honorbuddy_Programming_Cookbook:_QuestId_for_Custom_Behaviors // ...and also used for IsDone processing. QuestId = GetAttributeAsNullable <int>("QuestId", false, ConstrainAs.QuestId(this), null) ?? 0; QuestRequirementComplete = GetAttributeAsNullable <QuestCompleteRequirement>("QuestCompleteRequirement", false, null, null) ?? QuestCompleteRequirement.NotComplete; QuestRequirementInLog = GetAttributeAsNullable <QuestInLogRequirement>("QuestInLogRequirement", false, null, null) ?? QuestInLogRequirement.InLog; QuestObjectiveIndex = GetAttributeAsNullable <int>("QuestObjectiveIndex", false, new ConstrainTo.Domain <int>(1, 10), null) ?? 0; // Tunables... IgnoreMobsInBlackspots = GetAttributeAsNullable <bool>("IgnoreMobsInBlackspots", false, null, null) ?? true; MovementBy = GetAttributeAsNullable <MovementByType>("MovementBy", false, null, null) ?? MovementByType.FlightorPreferred; NonCompeteDistance = GetAttributeAsNullable <double>("NonCompeteDistance", false, new ConstrainTo.Domain <double>(0.0, 50.0), null) ?? 20.0; TerminateAtMaxRunTimeSecs = GetAttributeAsNullable <int>("TerminateAtMaxRunTimeSecs", false, new ConstrainTo.Domain <int>(0, int.MaxValue), null) ?? int.MaxValue; // Go ahead and compile the "TerminateWhen" expression to look for problems... // Doing this in the constructor allows us to catch 'blind change'problems when ProfileDebuggingMode is turned on. // If there is a problem, an exception will be thrown (and handled here). var terminateWhenExpression = GetAttributeAs <string>("TerminateWhen", false, ConstrainAs.StringNonEmpty, null); TerminateWhenCompiledExpression = Utility.ProduceParameterlessCompiledExpression <bool>(terminateWhenExpression); TerminateWhen = Utility.ProduceCachedValueFromCompiledExpression(TerminateWhenCompiledExpression, false); TerminationChecksQuestProgress = GetAttributeAsNullable <bool>("TerminationChecksQuestProgress", false, null, null) ?? true; // Dummy attributes... // These attributes are accepted, but not used. They are here to help the profile writer document without // causing "unknown attribute" warnings to be emitted. GetAttributeAs <string>("QuestName", false, ConstrainAs.StringNonEmpty, null); // XML types // Add temporary avoid mobs, if any were specified... // NB: ConfigMemento will restore the orginal list in OnFinished _temporaryAvoidMobs = AvoidMobsType.GetOrCreate(Element, "AvoidMobs"); // Add temporary blackspots, if any were specified... // NB: Ideally, we'd save and restore the original blackspot list. However, // BlackspotManager does not currently give us a way to "see" what is currently // on the list. _temporaryBlackspots = BlackspotsType.GetOrCreate(Element, "Blackspots"); PursuitList = PursuitListType.GetOrCreate(Element, "PursuitList"); } catch (Exception except) { if (Query.IsExceptionReportingNeeded(except)) { // Maintenance problems occur for a number of reasons. The primary two are... // * Changes were made to the behavior, and boundary conditions weren't properly tested. // * The Honorbuddy core was changed, and the behavior wasn't adjusted for the new changes. // In any case, we pinpoint the source of the problem area here, and hopefully it can be quickly // resolved. QBCLog.Exception(except); } IsAttributeProblem = true; } }
/// <summary> /// <para>Uses the hearthstone.</para> /// <para>Dismounts if mounted and stops moving before attempting to cast hearthstone. </para> /// <para>Does not yield until hearthstone is casted unless it can't be casted, already in hearthstone area or cast failed. </para> /// </summary> /// <param name="useGarrisonHearthstone">Use garrison hearthstone if set to <c>true</c>.</param> /// <param name="inHearthAreaAction">The action to take if already in hearthstone area.</param> /// <param name="noHearthStoneInBagsAction">The action to take if no hearthstone is in bags.</param> /// <param name="hearthNotSetAction">The action to take if hearth is not set.</param> /// <param name="hearthOnCooldownAction">The action to take if hearth is on cooldown.</param> /// <param name="hearthCastedAction">The action to take if hearth is successfully casted.</param> /// <param name="hearthCastFailedAction">The action to take if hearth failed to cast. The reason string is passed in argument.</param> /// <returns>Returns <c>true</c> if hearth was casted</returns> /// <exception cref="Exception">A delegate callback throws an exception.</exception> public static async Task <bool> UseHearthStone( bool useGarrisonHearthstone = false, Action inHearthAreaAction = null, Action noHearthStoneInBagsAction = null, Action hearthNotSetAction = null, Action hearthOnCooldownAction = null, Action hearthCastedAction = null, Action <string> hearthCastFailedAction = null) { if (IsInHearthStoneArea(useGarrisonHearthstone)) { if (inHearthAreaAction != null) { inHearthAreaAction(); } else { QBCLog.DeveloperInfo("Already at hearthstone area"); } return(false); } var hearthStones = useGarrisonHearthstone ? GetHearthStonesByIds(ItemId_GarrisonHearthStoneId) : GetHearthStonesByIds(ItemId_HearthStoneId, ItemId_TheInnkeepersDaughter); if (!hearthStones.Any()) { if (noHearthStoneInBagsAction != null) { noHearthStoneInBagsAction(); } else { QBCLog.DeveloperInfo("No hearthstone found in bag"); } return(false); } if (!useGarrisonHearthstone && Me.HearthstoneAreaId == 0) { // I can only see this occurring if using the Innkeeper's Daughter hearthtone since the normal hearthstone // only shows up in bags if hearth has been set. if (hearthNotSetAction != null) { hearthNotSetAction(); } else { QBCLog.DeveloperInfo("Hearth has not been set"); } return(false); } var usableHearthstone = hearthStones.FirstOrDefault(i => !i.Effects.First().Spell.Cooldown); if (usableHearthstone == null) { if (hearthOnCooldownAction != null) { hearthOnCooldownAction(); } else { QBCLog.DeveloperInfo("Hearth is on cooldown"); } return(false); } // the following coroutines execute sequentially, they do not return until dismounted or movement has stopped. await CommonCoroutines.LandAndDismount(); await CommonCoroutines.StopMoving(); // Close any frame that can prevent hearthstone use... // For example WoW will try to sell to hearthstone if merchant frame is open when hearthstone is used await CloseFrames(); var hearthstoneSpell = usableHearthstone.Effects.First().Spell; using (var castMonitor = SpellCastMonitor.Start(hearthstoneSpell.Id)) { QBCLog.DeveloperInfo("Using hearthstone: {0}", hearthstoneSpell.Name); usableHearthstone.Use(); var castResult = await castMonitor.GetResult(12000); if (castResult == SpellCastResult.Succeeded) { await Coroutine.Wait(2000, () => IsInHearthStoneArea(useGarrisonHearthstone)); if (hearthCastedAction != null) { hearthCastedAction(); } else { QBCLog.DeveloperInfo("Successfully used hearthstone"); } return(true); } string reason = castResult == SpellCastResult.UnknownFail ? castMonitor.FailReason : castResult.ToString(); if (hearthCastFailedAction != null) { hearthCastFailedAction(reason); } else { QBCLog.Warning("Cast of {0} failed. Reason: {1}", hearthstoneSpell.Name, reason); } return(false); } }
/// <summary> /// <para>Uses item defined by ITEMID.</para> /// <para> /// Notes: /// <list type="bullet"> /// <item> /// <description> /// <para> /// * It is up to the caller to assure that all preconditions have been met for /// using the item (i.e., the item is off cooldown, etc). /// </para> /// </description> /// </item> /// <item> /// <description> /// <para> /// * If item use was successful, coroutine returns 'true'; /// otherwise, 'false' is returned (e.g., item is not ready for use, /// item use was interrupted by combat, etc). /// </para> /// </description> /// </item> /// </list> /// </para> /// </summary> /// <param name="itemId">The item provided should be viable, and ready for use.</param> /// <param name="actionOnMissingItemDelegate">This delegate will be called if the item /// is missing from our backpack. This delegate may not be null.</param> /// <param name="actionOnFailedItemUseDelegate">If non-null, this delegate will be called /// if we attempted to use the item, and it was unsuccessful. Examples include attemtping /// to use the item on an invalid target, or being interrupted or generally unable to use /// the item at this time.</param> /// <param name="actionOnSuccessfulItemUseDelegate">If non-null, this delegate will be called /// once the item has been used successfully.</param> /// <returns></returns> /// <remarks>20140305-19:01UTC, Highvoltz/chinajade</remarks> public static async Task <bool> UseItem( int itemId, System.Action actionOnMissingItemDelegate, System.Action actionOnFailedItemUseDelegate = null, System.Action actionOnSuccessfulItemUseDelegate = null) { // Waits for global cooldown to end to successfully use the item await Coroutine.Wait(500, () => !SpellManager.GlobalCooldown); // Is item in our bags? var itemToUse = Me.CarriedItems.FirstOrDefault(i => (i.Entry == itemId)); if (!Query.IsViable(itemToUse)) { QBCLog.Error("{0} is not in our bags.", Utility.GetItemNameFromId(itemId)); if (actionOnMissingItemDelegate != null) { actionOnMissingItemDelegate(); } return(false); } var itemName = itemToUse.SafeName; // Wait for Item to be usable... // NB: WoWItem.Usable does not account for cooldowns. if (!itemToUse.Usable || (itemToUse.Cooldown > 0)) { TreeRoot.StatusText = string.Format( "{0} is not usable, yet. (cooldown remaining: {1})", itemName, Utility.PrettyTime(itemToUse.CooldownTimeLeft)); return(false); } // Notify user of intent... QBCLog.DeveloperInfo("Attempting use of '{0}'", itemName); // Set up 'interrupted use' detection, and use item... using (var castMonitor = SpellCastMonitor.Start(null)) { itemToUse.Use(); // NB: The target or the item may not be valid after this point... // Some targets will go 'invalid' immediately afer interacting with them. // Most of the time this happens, the target is immediately and invisibly replaced with // an identical looking target with a different script. // Some items are consumed when used. // We must assume our target and item is no longer available for use after this point. await Coroutine.Sleep((int)Delay.AfterItemUse.TotalMilliseconds); // Wait for any casting to complete... // NB: Some interactions or item usages take time, and the WoWclient models this as spellcasting. // NB: We can't test for IsCasting or IsChanneling--we must instead look for a valid spell being cast. // There are some quests that require actions where the WoWclient returns 'true' for IsCasting, // but there is no valid spell being cast. We want the behavior to move on immediately in these // conditions. An example of such an interaction is removing 'tangler' vines in the Tillers // daily quest area. var castResult = await castMonitor.GetResult(); if (castResult != SpellCastResult.Succeeded && castResult != SpellCastResult.NoCastStarted) { string reason = castResult == SpellCastResult.UnknownFail ? castMonitor.FailReason : castResult.ToString(); QBCLog.Warning("Use of {0} interrupted. Reason: {1}", itemName, reason); // Give whatever issue encountered a chance to settle... // NB: --we want the Sequence to fail when delay completes. if (castResult != SpellCastResult.LineOfSight && castResult != SpellCastResult.OutOfRange && castResult != SpellCastResult.TooClose) { await Coroutine.Sleep(1500); } if (actionOnFailedItemUseDelegate != null) { actionOnFailedItemUseDelegate(); } return(false); } } QBCLog.DeveloperInfo("Use of '{0}' succeeded.", itemName); if (actionOnSuccessfulItemUseDelegate != null) { actionOnSuccessfulItemUseDelegate(); } return(true); }
/// <summary> /// <para>Uses item defined by ITEMID on target defined by SELECTEDTARGET.</para> /// <para> /// Notes: /// <list type="bullet"> /// <item> /// <description> /// <para> /// * It is up to the caller to assure that all preconditions have been met for /// using the item (i.e., the target is in range, the item is off cooldown, etc). /// </para> /// </description> /// </item> /// <item> /// <description> /// <para> /// * If item use was successful, coroutine returns 'true'; /// otherwise, 'false' is returned (e.g., item is not ready for use, /// item use was interrupted by combat, etc). /// </para> /// </description> /// </item> /// <item> /// <description> /// <para> /// * It is up to the caller to blacklist the target, or select a new target /// after successful item use. The actionOnFailedItemUseDelegate argument /// can facilitate these activities. /// </para> /// </description> /// </item> /// </list> /// </para> /// </summary> /// <param name="selectedTarget">The target provided should be viable.</param> /// <param name="itemId">The item provided should be viable, and ready for use.</param> /// <param name="actionOnMissingItemDelegate">This delegate will be called if the item /// is missing from our backpack. This delegate may not be null.</param> /// <param name="actionOnFailedItemUseDelegate">If non-null, this delegate will be called /// if we attempted to use the item, and it was unsuccessful. Examples include attemtping /// to use the item on an invalid target, or being interrupted or generally unable to use /// the item at this time.</param> /// <param name="actionOnSuccessfulItemUseDelegate">If non-null, this delegate will be called /// once the item has been used successfully.</param> /// <returns></returns> /// <remarks>20140305-19:01UTC, Highvoltz/chinajade</remarks> public static async Task <bool> UseItemOnTarget( int itemId, WoWObject selectedTarget, System.Action actionOnMissingItemDelegate, System.Action actionOnFailedItemUseDelegate = null, System.Action actionOnSuccessfulItemUseDelegate = null) { // Waits for global cooldown to end to successfully use the item await Coroutine.Wait(500, () => !SpellManager.GlobalCooldown); // qualify... // Viable target? // NB: Since target may go invalid immediately upon using the item, // we cache its name for use in subsequent log entries.; if (!Query.IsViable(selectedTarget)) { QBCLog.Warning("Target is not viable!"); if (actionOnFailedItemUseDelegate != null) { actionOnFailedItemUseDelegate(); } return(false); } var targetName = selectedTarget.SafeName; // Is item in our bags? var itemToUse = Me.CarriedItems.FirstOrDefault(i => (i.Entry == itemId)); if (!Query.IsViable(itemToUse)) { QBCLog.Error("{0} is not in our bags.", Utility.GetItemNameFromId(itemId)); if (actionOnMissingItemDelegate != null) { actionOnMissingItemDelegate(); } return(false); } var itemName = itemToUse.SafeName; // Need to be facing target... // NB: Not all items require this, but many do. Utility.Target(selectedTarget, true); // Wait for Item to be usable... // NB: WoWItem.Usable does not account for cooldowns. if (!itemToUse.Usable || (itemToUse.Cooldown > 0)) { TreeRoot.StatusText = string.Format( "{0} is not usable, yet. (cooldown remaining: {1})", itemName, Utility.PrettyTime(itemToUse.CooldownTimeLeft)); return(false); } // Notify user of intent... var message = string.Format("Attempting use of '{0}' on '{1}'", itemName, targetName); var selectedTargetAsWoWUnit = selectedTarget as WoWUnit; if (selectedTargetAsWoWUnit != null) { if (selectedTargetAsWoWUnit.IsDead) { message += " (dead)"; } else { message += string.Format(" (health: {0:F1})", selectedTargetAsWoWUnit.HealthPercent); } } QBCLog.DeveloperInfo(message); // Set up 'interrupted use' detection, and use item... // MAINTAINER'S NOTE: Once these handlers are installed, make sure all possible exit paths from the outer // Sequence unhook these handlers. I.e., if you plan on returning RunStatus.Failure, be sure to call // UtilityBehaviorSeq_UseItemOn_HandlersUnhook() first. // Set up 'interrupted use' detection, and use item... using (var castMonitor = SpellCastMonitor.Start(null)) { itemToUse.Use(selectedTarget.Guid); // NB: The target or the item may not be valid after this point... // Some targets will go 'invalid' immediately afer interacting with them. // Most of the time this happens, the target is immediately and invisibly replaced with // an identical looking target with a different script. // Some items are consumed when used. // We must assume our target and item is no longer available for use after this point. await Coroutine.Sleep((int)Delay.AfterItemUse.TotalMilliseconds); await CastPendingSpell(selectedTarget); // Wait for any casting to complete... // NB: Some interactions or item usages take time, and the WoWclient models this as spellcasting. // NB: We can't test for IsCasting or IsChanneling--we must instead look for a valid spell being cast. // There are some quests that require actions where the WoWclient returns 'true' for IsCasting, // but there is no valid spell being cast. We want the behavior to move on immediately in these // conditions. An example of such an interaction is removing 'tangler' vines in the Tillers // daily quest area. var castResult = await castMonitor.GetResult(); if (castResult != SpellCastResult.Succeeded && castResult != SpellCastResult.NoCastStarted) { string reason = castResult == SpellCastResult.UnknownFail ? castMonitor.FailReason : castResult.ToString(); QBCLog.Warning("Use of {0} interrupted. Reason: {1}", itemName, reason); // Give whatever issue encountered a chance to settle... // NB: --we want the Sequence to fail when delay completes. if (castResult != SpellCastResult.LineOfSight && castResult != SpellCastResult.OutOfRange && castResult != SpellCastResult.TooClose) { await Coroutine.Sleep(1500); } if (actionOnFailedItemUseDelegate != null) { actionOnFailedItemUseDelegate(); } return(false); } } QBCLog.DeveloperInfo("Use of '{0}' on '{1}' succeeded.", itemName, targetName); if (actionOnSuccessfulItemUseDelegate != null) { actionOnSuccessfulItemUseDelegate(); } return(true); }