public static Composite Cast(SpellFindDelegate ssd, SimpleBooleanDelegate checkMovement, UnitSelectionDelegate onUnit, SimpleBooleanDelegate requirements, SimpleBooleanDelegate cancel = null, LagTolerance allow = LagTolerance.Yes, bool skipWowCheck = false, CanCastDelegate canCast = null, HasGcd gcd = HasGcd.Yes) { // only need to check these at creation time if (ssd == null || checkMovement == null || onUnit == null || requirements == null) return new ActionAlwaysFail(); if (canCast == null) canCast = CanCastHack; Composite comp = new PrioritySelector( // create a CastContext object to save passed in context and other values ctx => new CastContext(ctx, ssd, onUnit, gcd), new Sequence( // cast the spell, saving state information including if we queued this cast new Action(ret => { CastContext cctx = ret.CastContext(); if (cctx.spell == null) return RunStatus.Failure; if (cctx.unit == null) return RunStatus.Failure; if (!requirements(cctx.context)) return RunStatus.Failure; if (checkMovement(cctx.context) && Me.IsMoving && !AllowMovingWhileCasting(cctx.spell)) { if (SingularSettings.DebugSpellCasting) Logger.WriteDebug("skipping Spell.Cast({0},[{1}]) because we are moving", cctx.unit.SafeName(), cctx.spell.Name); return RunStatus.Failure; } // check we can cast it on target without checking for movement // if (!Spell.CanCastHack(_spell, cctx.unit, true, false, allow == LagTolerance.Yes)) if (!canCast(cctx.sfr, cctx.unit, skipWowCheck)) { if (SingularSettings.DebugSpellCasting) Logger.WriteDebug("skipping Spell.Cast({0},[{1}]) because CanCastHack failed", cctx.unit.SafeName(), cctx.spell.Name); return RunStatus.Failure; } // save status of queueing spell (lag tolerance - the prior spell still completing) cctx.IsSpellBeingQueued = allow == LagTolerance.Yes && (Spell.GcdActive || StyxWoW.Me.IsCasting || StyxWoW.Me.IsChanneling); const int PENANCE = 047540; LogCast( cctx.spell.Name, cctx.unit, cctx.health, cctx.distance, cctx.spell.IsHeal() ? true : (cctx.spell.Id == PENANCE && cctx.unit.IsFriendly) ); if (SingularSettings.DebugSpellCasting) Logger.WriteDebug("Cast('{0}'): dist:{1:F3}, need({2}), hitbox:{3:F3}", cctx.spell.Name, cctx.unit.Distance, cctx.spell.IsMeleeSpell ? "Melee" : (!cctx.spell.HasRange ? "None" : string.Format("min={0:F3},max={1:F3}", cctx.spell.MinRange, cctx.spell.MaxRange)), cctx.unit.CombatReach ); if (!Spell.CastPrimative(cctx.spell, cctx.unit)) { Logger.Write(Color.LightPink, "cast of {0} on {1} failed!", cctx.spell.Name, cctx.unit.SafeName()); return RunStatus.Failure; } SingularRoutine.UpdateDiagnosticCastingState(); return RunStatus.Success; }), new Action(r => { if (SingularSettings.DebugSpellCasting) { CastContext cctx = r.CastContext(); Logger.WriteFile("Spell.Cast[{0}]: checking to ensure cast in progress", cctx.spell.Name); } return RunStatus.Success; }), // for instant spell, wait for GCD to start // for non-instant spell, wait for .IsCasting / .IsChanneling to start new PrioritySelector( new Wait( TimeSpan.FromMilliseconds(350), until => { CastContext cctx = until.CastContext(); if (gcd == HasGcd.No) { if (SingularSettings.DebugSpellCasting) Logger.WriteFile("Spell.Cast[{0}]: has no GCD, status GCD={1}, remains={2}", cctx.spell.Name, Spell.IsGlobalCooldown(allow).ToYN(), (long)Spell.GcdTimeLeft.TotalMilliseconds); return true; } if (cctx.spell.IsInstantCast() && Spell.GcdTimeLeft.TotalMilliseconds > 650) { if (SingularSettings.DebugSpellCasting) Logger.WriteFile("Spell.Cast[{0}]: is instant, status GCD={1}, remains={2}", cctx.spell.Name, Spell.IsGlobalCooldown(allow).ToYN(), (long)Spell.GcdTimeLeft.TotalMilliseconds); return true; } if (Me.CurrentCastTimeLeft.TotalMilliseconds > 750) { if (SingularSettings.DebugSpellCasting) Logger.WriteFile( "Spell.Cast[{0}]: cast time {1} left, iscasting={2}", cctx.spell.Name, (long)Me.CurrentCastTimeLeft.TotalMilliseconds, Spell.IsCasting(allow).ToYN()); return true; } if (Me.CurrentChannelTimeLeft.TotalMilliseconds > 750) { if (SingularSettings.DebugSpellCasting) Logger.WriteFile( "Spell.Cast[{0}]: channel spell and channel has {1} left, ischanneling={2}", cctx.spell.Name, (long)Me.CurrentChannelTimeLeft.TotalMilliseconds, Spell.IsChannelling(allow).ToYN()); return true; } return false; }, new ActionAlwaysSucceed() ), new Action( r => { if (SingularSettings.DebugSpellCasting) { CastContext cctx = r.CastContext(); Logger.WriteFile( "Spell.Cast[{0}]: timed out failing to detect spell in progress - gcdleft={1}/{2}, castleft={3}/{4}, chanleft={5}/{6}", cctx.spell.Name, Spell.IsGlobalCooldown(allow).ToYN(), (long)Spell.GcdTimeLeft.TotalMilliseconds, Spell.IsCasting(allow).ToYN(), (long)Me.CurrentCastTimeLeft.TotalMilliseconds, Spell.IsCasting(allow).ToYN(), (long)Me.CurrentChannelTimeLeft.TotalMilliseconds); } return RunStatus.Success; }) ), // now check for one of the possible done casting states new PrioritySelector( // for cast already ended, assume it has no Global Cooldown new Decorator( ret => !Spell.IsGlobalCooldown() && !Spell.IsCastingOrChannelling(), new Action(r => { CastContext cctx = r.CastContext(); if (SingularSettings.DebugSpellCasting) { if (!Spell.IsGlobalCooldown()) Logger.WriteFile("Spell.Cast(\"{0}\"): complete, no gcd active, lag={0} hasgcd={1}", cctx.spell.Name, allow, gcd); else Logger.WriteFile("Spell.Cast(\"{0}\"): complete, no cast in progress", cctx.spell.Name); } return RunStatus.Success; }) ), // for instant or no cancel method given, we are done new Decorator( ret => gcd == HasGcd.No || cancel == null || ret.CastContext().spell.IsInstantCast(), new Action(r => { CastContext cctx = r.CastContext(); if (SingularSettings.DebugSpellCasting) { if (gcd == HasGcd.No) Logger.WriteFile("Spell.Cast(\"{0}\"): complete, hasgcd=No", cctx.spell.Name); else if (r.CastContext().spell.IsInstantCast()) Logger.WriteFile("Spell.Cast(\"{0}\"): complete, is instant cast", cctx.spell.Name); else Logger.WriteFile("Spell.Cast(\"{0}\"): complete, no cancel delegate given", cctx.spell.Name); } return RunStatus.Success; }) ), // while casting/channeling call the cancel method to see if we should abort new Wait(12, until => { CastContext cctx = until.CastContext(); SingularRoutine.UpdateDiagnosticCastingState(); // Interrupted or finished casting. if (!Spell.IsCastingOrChannelling(allow)) { Logger.WriteDebug("Spell.Cast(\"{0}\"): complete, iscasting=false", cctx.spell.Name); return true; } // check cancel delegate if we are finished if (cancel(cctx.context)) { SpellManager.StopCasting(); Logger.Write(LogColor.Cancel, "/cancel {0} on {1} @ {2:F1}%", cctx.spell.Name, cctx.unit.SafeName(), cctx.unit.HealthPercent); return true; } // continue casting/channeling at this point return false; }, new ActionAlwaysSucceed() ), // if we are here, we timed out after 12 seconds (very odd) new Action(r => { CastContext cctx = r.CastContext(); Logger.WriteDebug("Spell.Cast(\"{0}\"): aborting, timed out waiting on cast, gcd={1} cast={2} chanl={3}", cctx.spell.Name, Spell.IsGlobalCooldown().ToYN(), Spell.IsCasting().ToYN(), Spell.IsChannelling().ToYN()); return RunStatus.Success; }) ), // made it this far then we are RunStatus.Success, so reset wowunit reference and return new Action(ret => { CastContext cctx = ret.CastContext(); cctx.unit = null; cctx.spell = null; return RunStatus.Success; }) ), // cast Sequence failed, so only thing left is to reset cached references and report failure new Action(ret => { CastContext cctx = ret.CastContext(); cctx.unit = null; cctx.spell = null; return RunStatus.Failure; }) ); // when no cancel method in place, we will return immediately so..... // .. throttle attempts at casting this spell. note: this only limits this // .. instance of the spell.cast behavior. in other words, if this is for a cast // .. of flame shock, it would only throttle this behavior tree instance, not any // .. other trees which also call Spell.Cast("flame shock") if (cancel == null) comp = new Throttle( TimeSpan.FromMilliseconds(SingularSettings.Instance.SameSpellThrottle), comp); return comp; }
private static Composite AddCommonBehaviorPrefix( Composite composite, BehaviorType behav) { if (behav == BehaviorType.LossOfControl) { composite = new Decorator( ret => HaveWeLostControl, new PrioritySelector( new Action(r => { if (!StyxWoW.IsInGame) { Logger.WriteDebug(Color.White, "Not in game..."); return RunStatus.Success; } return RunStatus.Failure; }), new ThrottlePasses(1, 1, new Decorator(ret => Me.Fleeing, new Action(r => { Logger.Write( LogColor.Hilite, "FLEEING! (loss of control)"); return RunStatus.Failure; }))), new ThrottlePasses(1, 1, new Decorator(ret => Me.Stunned, new Action(r => { Logger.Write( LogColor.Hilite, "STUNNED! (loss of control)"); return RunStatus.Failure; }))), new ThrottlePasses(1, 1, new Decorator(ret => Me.IsSilenced(), new Action(r => { Logger.Write( LogColor.Hilite, "SILENCED! (loss of control)"); return RunStatus.Failure; }))), new Throttle(1, new PrioritySelector( composite ?? new ActionAlwaysFail(), new Decorator( ret => SingularSettings.Instance.UseRacials, new PrioritySelector( Spell.Cast("Will of the Forsaken", on => Me, ret => Me.Race == WoWRace.Undead && Me.Fleeing), Spell.Cast("Every Man for Himself", on => Me, ret => Me.Race == WoWRace.Human && (Me.Stunned || Me.Fleeing)) ) ), Item.UseEquippedTrinket(TrinketUsage.CrowdControlled), Item.UseEquippedTrinket(TrinketUsage.CrowdControlledSilenced) ) ), new ActionAlwaysSucceed() ) ); } if (behav == BehaviorType.Rest) { Composite combatInRestCheck; if (!SingularSettings.Debug) combatInRestCheck = new ActionAlwaysFail(); else combatInRestCheck = new Throttle( 1, new ThrottlePasses( 1, TimeSpan.FromSeconds(1), RunStatus.Failure, new Decorator( req => Me.IsActuallyInCombat, new Sequence( new PrioritySelector( new ThrottlePasses( 1, 5, new Action(r => Logger.Write(Color.Yellow, "Bot Error: {0} or plugin called Rest behavior while in combat", SingularRoutine.GetBotName())) ), new Action(r => Logger.WriteDebug(Color.Yellow, "Bot Error: {0} or plugin called Rest behavior while in combat", SingularRoutine.GetBotName())) ), new ActionAlwaysFail() ) ) ) ); composite = new LockSelector( new CallWatch("Rest", new Decorator( ret => !Me.IsFlying && AllowBehaviorUsage() && !SingularSettings.Instance.DisableNonCombatBehaviors, new PrioritySelector( // TestDynaWait(), // following is to allow cancelling of kiting in progress new Decorator( ret => Kite.IsKitingActive(), new HookExecutor(HookName("KitingBehavior")) ), new Decorator( req => ShouldWeFightDuringRest(), new PrioritySelector( Safers.EnsureTarget(), new Decorator( req => Unit.ValidUnit(Me.CurrentTarget) && Me.CurrentTarget.SpellDistance() < 45, new PrioritySelector( new ThrottlePasses( 1, TimeSpan.FromSeconds(15), RunStatus.Failure, new Action(r => Logger.WriteDiagnostic(LogColor.Hilite, "Forcing combat from Rest Behavior")), new ActionAlwaysFail() ), SingularRoutine.Instance.HealBehavior, SingularRoutine.Instance.CombatBuffBehavior, SingularRoutine.Instance.CombatBehavior, new ActionAlwaysSucceed() ) ) ) ), // new Action(r => { _guidLastTarget = 0; return RunStatus.Failure; }), new Decorator( req => !OkToCallBehaviorsWithCurrentCastingStatus(allow: LagTolerance.No), new ActionAlwaysSucceed() ), // lost control in Rest -- force a RunStatus.Failure so we don't loop in Rest new Sequence( SingularRoutine.Instance._lostControlBehavior, new ActionAlwaysFail() ), // skip Rest logic if we lost control (since we had to return Fail to prevent Rest loop) new Decorator( req => !HaveWeLostControl, composite ?? new ActionAlwaysFail() ) ) ) ) ); } if (behav == BehaviorType.PreCombatBuffs) { Composite precombatInCombatCheck; if (!SingularSettings.Debug) precombatInCombatCheck = new ActionAlwaysFail(); else precombatInCombatCheck = new Throttle( 1, new ThrottlePasses( 1, TimeSpan.FromSeconds(1), RunStatus.Failure, new Decorator( req => Me.IsActuallyInCombat, new Sequence( new PrioritySelector( new ThrottlePasses( 1, 5, new Action(r => Logger.Write(Color.Yellow, "Bot Error: {0} or plugin called PreCombatBuff behavior while in combat", SingularRoutine.GetBotName())) ), new Action(r => Logger.WriteDebug(Color.Yellow, "Bot Error: {0} or plugin called PreCombatBuff behavior while in combat", SingularRoutine.GetBotName())) ), new ActionAlwaysFail() ) ) ) ); composite = new LockSelector( new CallWatch("PreCombat", new Decorator( // suppress non-combat buffing if standing around waiting on DungeonBuddy or BGBuddy queues ret => !Me.Mounted && !Me.HasAnyAura("Darkflight") && !SingularSettings.Instance.DisableNonCombatBehaviors && AllowNonCombatBuffing(), new PrioritySelector( new Decorator( req => !OkToCallBehaviorsWithCurrentCastingStatus(), Spell.WaitForGcdOrCastOrChannel() ), Helpers.Common.CreateUseTableBehavior(), Helpers.Common.CreateUseSoulwellBehavior(), Item.CreateUseFlasksBehavior(), Item.CreateUseScrollsBehavior(), composite ?? new ActionAlwaysFail() ) ) ) ); } if (behav == BehaviorType.PullBuffs) { composite = new LockSelector( new CallWatch("PullBuffs", new Decorator( ret => AllowBehaviorUsage() && OkToCallBehaviorsWithCurrentCastingStatus(), composite ?? new ActionAlwaysFail() ) ) ); } if (behav == BehaviorType.Pull) { composite = new LockSelector( new CallWatch("Pull", new Decorator( ret => AllowBehaviorUsage(), // && (!Me.GotTarget() || !Blacklist.Contains(Me.CurrentTargetGuid, BlacklistFlags.Combat)), new PrioritySelector( new Decorator( ret => !HotkeyDirector.IsCombatEnabled, new ActionAlwaysSucceed() ), #if BOTS_NOT_CALLING_PULLBUFFS _pullBuffsBehavior, #endif CreateLogTargetChanges(BehaviorType.Pull, "<<< PULL >>>"), Item.CreateThunderLordGrappleBehavior(), composite ?? new ActionAlwaysFail() ) ) ) ); } if (behav == BehaviorType.Heal) { Composite behavHealingSpheres = new ActionAlwaysFail(); if (SingularSettings.Instance.MoveToSpheres) { behavHealingSpheres = new ThrottlePasses( 1, 1, new Decorator( ret => Me.HealthPercent < SingularSettings.Instance.SphereHealthPercentInCombat && Singular.ClassSpecific.Monk.Common.AnySpheres(SphereType.Life, SingularSettings.Instance.SphereDistanceInCombat), Singular.ClassSpecific.Monk.Common.CreateMoveToSphereBehavior(SphereType.Life, SingularSettings.Instance.SphereDistanceInCombat) ) ); } composite = new LockSelector( new CallWatch("Heal", // following must occur before any cast or movement during Combat Generic.CreateCancelShadowmeld(), SingularRoutine.Instance._lostControlBehavior, new Decorator( ret => Kite.IsKitingActive(), new HookExecutor(HookName("KitingBehavior")) ), new Decorator( ret => AllowBehaviorUsage() && OkToCallBehaviorsWithCurrentCastingStatus(), new PrioritySelector( Generic.CreatePotionAndHealthstoneBehavior(), composite ?? new ActionAlwaysFail(), behavHealingSpheres ) ) ) ); } if (behav == BehaviorType.CombatBuffs) { composite = new LockSelector( new CallWatch("CombatBuffs", new Decorator( ret => AllowBehaviorUsage() && OkToCallBehaviorsWithCurrentCastingStatus(), new PrioritySelector( new Decorator(ret => !HotkeyDirector.IsCombatEnabled, new ActionAlwaysSucceed()), Generic.CreateUseTrinketsBehaviour(), Generic.CreatePotionAndHealthstoneBehavior(), Item.CreateUseXPBuffPotionsBehavior(), Generic.CreateRacialBehaviour(), Generic.CreateGarrisonAbilityBehaviour(), composite ?? new ActionAlwaysFail() ) ) ) ); } if (behav == BehaviorType.Combat) { composite = new LockSelector( new CallWatch("Combat", new Decorator( ret => AllowBehaviorUsage(), // && (!Me.GotTarget() || !Blacklist.Contains(Me.CurrentTargetGuid, BlacklistFlags.Combat)), new PrioritySelector( new Decorator( ret => !HotkeyDirector.IsCombatEnabled, new ActionAlwaysSucceed() ), CreatePullMorePull(), CreateLogTargetChanges(BehaviorType.Combat, "<<< ADD >>>"), composite ?? new ActionAlwaysFail() ) ) ) ); } if (behav == BehaviorType.Death) { composite = new LockSelector( new CallWatch("Death", new Decorator( ret => AllowBehaviorUsage(), new PrioritySelector( new Action(r => { ResetCurrentTarget(); return RunStatus.Failure; }), new Decorator( req => !OkToCallBehaviorsWithCurrentCastingStatus(), Spell.WaitForGcdOrCastOrChannel() ), composite ?? new ActionAlwaysFail() ) ) ) ); } return composite; }