using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Trinity.DbProvider; using Trinity.Technicals; using Zeta.Bot.Dungeons; using Zeta.Bot.Logic; using Zeta.Bot.Navigation; using Zeta.Bot.Pathfinding; using Zeta.Bot.Profile; using Zeta.Common; using Zeta.Game; using Zeta.Game.Internals; using Zeta.Game.Internals.Actors; using Zeta.Game.Internals.SNO; using Zeta.TreeSharp; using Zeta.XmlEngine; using Action = Zeta.TreeSharp.Action; using Logger = Trinity.Technicals.Logger; namespace Trinity.XmlTags { /// /// TrinityExploreDungeon is fuly backwards compatible with the built-in Demonbuddy ExploreArea tag. It provides additional features such as: /// Moving to investigate MiniMapMarker pings and the current ExitNameHash if provided and visible (mini map marker 0 and the current exitNameHash) /// Moving to investigate Priority Scenes if provided (PrioritizeScenes subtags) /// Ignoring DungeonExplorer nodes in certain scenes if provided (IgnoreScenes subtags) /// Reduced backtracking (via pathPrecision attribute and combat skip ahead cache) /// Multiple ActorId's for the ObjectFound end type (AlternateActors sub-tags) /// [XmlElement("TrinityExploreDungeon")] public class TrinityExploreDungeon : ProfileBehavior { /// /// The SNOId of the Actor that we're looking for, used with until="ObjectFound" /// [XmlAttribute("actorId", true)] public int ActorId { get; set; } /// /// Sets a custom grid segmentation Box Size (default 15) /// [XmlAttribute("boxSize", true)] public int BoxSize { get; set; } /// /// Sets a custom grid segmentation Box Tolerance (default 0.55) /// [XmlAttribute("boxTolerance", true)] public float BoxTolerance { get; set; } /// /// The nameHash of the exit the bot will move to and finish the tag when found /// [XmlAttribute("exitNameHash", true)] public int ExitNameHash { get; set; } [XmlAttribute("ignoreGridReset", true)] public bool IgnoreGridReset { get; set; } /// /// Not currently implimented /// [XmlAttribute("leaveWhenFinished", true)] public bool LeaveWhenExplored { get; set; } /// /// Not currently implimented /// [XmlAttribute("leaveAfterBounty", true)] public bool LeaveAfterBounty { get; set; } /// /// The distance the bot must be from an actor before marking the tag as complete, when used with until="ObjectFound" /// [XmlAttribute("objectDistance", true)] public float ObjectDistance { get; set; } /// /// The until="" atribute must match one of these /// public enum TrinityExploreEndType { FullyExplored = 0, ObjectFound, ExitFound, SceneFound, SceneLeftOrActorFound } [XmlAttribute("endType", true)] [XmlAttribute("until", true)] public TrinityExploreEndType EndType { get; set; } /// /// The list of Scene SNOId's or Scene Names that the bot will ignore dungeon nodes in /// [XmlElement("IgnoreScenes")] public List IgnoreScenes { get; set; } /// /// The list of Scene SNOId's or Scene Names that the bot will prioritize (only works when the scene is "loaded") /// [XmlElement("PriorityScenes")] [XmlElement("PrioritizeScenes")] public List PriorityScenes { get; set; } /// /// The list of Scene SNOId's or Scene Names that the bot will use for endtype SceneLeftOrActorFound /// [XmlElement("AlternateScenes")] public List AlternateScenes { get; set; } /// /// The Ignore Scene class, used as IgnoreScenes child elements /// [XmlElement("IgnoreScene")] public class IgnoreScene : IEquatable { [XmlAttribute("sceneName")] public string SceneName { get; set; } [XmlAttribute("sceneId")] public int SceneId { get; set; } public IgnoreScene() { SceneId = -1; SceneName = String.Empty; } public IgnoreScene(string name) { this.SceneName = name; } public IgnoreScene(int id) { this.SceneId = id; } public bool Equals(Scene other) { return (!string.IsNullOrWhiteSpace(SceneName) && other.Name.ToLowerInvariant().Contains(SceneName.ToLowerInvariant())) || other.SceneInfo.SNOId == SceneId; } } private CachedValue> m_IgnoredAreas; private List IgnoredAreas { get { if (m_IgnoredAreas == null) m_IgnoredAreas = new CachedValue>(() => { return GetIgnoredAreas(); }, TimeSpan.FromSeconds(1)); return m_IgnoredAreas.Value; } } private List GetIgnoredAreas() { var ignoredScenes = ZetaDia.Scenes.GetScenes() .Where(scn => scn.IsValid && IgnoreScenes.Any(igns => igns.Equals(scn)) && !PriorityScenes.Any(psc => psc.Equals(scn))) .Select(scn => scn.Mesh.Zone == null ? new Area(new Vector2(float.MinValue, float.MinValue), new Vector2(float.MaxValue, float.MaxValue)) : new Area(scn.Mesh.Zone.ZoneMin, scn.Mesh.Zone.ZoneMax)) .ToList(); Logger.Log(LogCategory.ProfileTag, "Returning {0} ignored areas", ignoredScenes.Count()); return ignoredScenes; } private class Area { public Vector2 Min { get; set; } public Vector2 Max { get; set; } /// /// Initializes a new instance of the Area class. /// public Area(Vector2 min, Vector2 max) { Min = min; Max = max; } public bool IsPositionInside(Vector2 position) { return position.X >= Min.X && position.X <= Max.X && position.Y >= Min.Y && position.Y <= Max.Y; } public bool IsPositionInside(Vector3 position) { return IsPositionInside(position.ToVector2()); } } /// /// The Priority Scene class, used as PrioritizeScenes child elements /// [XmlElement("PriorityScene")] [XmlElement("PrioritizeScene")] public class PrioritizeScene : IEquatable { [XmlAttribute("sceneName")] public string SceneName { get; set; } [XmlAttribute("sceneId")] public int SceneId { get; set; } [XmlAttribute("pathPrecision")] public float PathPrecision { get; set; } public PrioritizeScene() { PathPrecision = 15f; SceneName = String.Empty; SceneId = -1; } public PrioritizeScene(string name) { this.SceneName = name; } public PrioritizeScene(int id) { this.SceneId = id; } public bool Equals(Scene other) { return (SceneName != String.Empty && other.Name.ToLowerInvariant().Contains(SceneName.ToLowerInvariant())) || other.SceneInfo.SNOId == SceneId; } } /// /// The Alternate Scene class, used as AlternateScenes child elements /// [XmlElement("AlternateScene")] public class AlternateScene : IEquatable { [XmlAttribute("sceneName")] public string SceneName { get; set; } [XmlAttribute("sceneId")] public int SceneId { get; set; } [XmlAttribute("pathPrecision")] public float PathPrecision { get; set; } public AlternateScene() { PathPrecision = 15f; SceneName = String.Empty; SceneId = -1; } public AlternateScene(string name) { this.SceneName = name; } public AlternateScene(int id) { this.SceneId = id; } public bool Equals(Scene other) { return (SceneName != String.Empty && other.Name.ToLowerInvariant().Contains(SceneName.ToLowerInvariant())) || other.SceneInfo.SNOId == SceneId; } } [XmlElement("AlternateActors")] public List AlternateActors { get; set; } [XmlElement("AlternateActor")] public class AlternateActor { [XmlAttribute("actorId")] public int ActorId { get; set; } [XmlAttribute("objectDistance")] public float ObjectDistance { get; set; } [XmlAttribute("interactRange")] public float InteractRange { get; set; } public AlternateActor() { ActorId = -1; ObjectDistance = 60f; } } [XmlElement("Objectives")] public List Objectives { get; set; } [XmlElement("Objective")] public class Objective { [XmlAttribute("actorId")] public int ActorID { get; set; } [XmlAttribute("markerNameHash")] public int MarkerNameHash { get; set; } [XmlAttribute("count")] public int Count { get; set; } [XmlAttribute("endAnimation")] public SNOAnim EndAnimation { get; set; } [XmlAttribute("interact")] public bool Interact { get; set; } public Objective() { } } /// /// The Scene SNOId, used with ExploreUntil="SceneFound" /// [XmlAttribute("sceneId")] public int SceneId { get; set; } /// /// The Scene Name, used with ExploreUntil="SceneFound", a sub-string match will work /// [XmlAttribute("sceneName")] public string SceneName { get; set; } /// /// The distance the bot will mark dungeon nodes as "visited" (default is 1/2 of box size, minimum 10) /// [XmlAttribute("pathPrecision")] public float PathPrecision { get; set; } /// /// The distance before reaching a MiniMapMarker before marking it as visited /// [XmlAttribute("markerDistance")] public float MarkerDistance { get; set; } /// /// Disable Mini Map Marker Scouting /// [XmlAttribute("ignoreMarkers")] public bool IgnoreMarkers { get; set; } public enum TimeoutType { Timer, GoldInactivity, None, } /// /// The TimeoutType to use (default None, no timeout) /// [XmlAttribute("timeoutType")] public TimeoutType ExploreTimeoutType { get; set; } /// /// Value in Seconds. /// The timeout value to use, when used with Timer will force-end the tag after a certain time. When used with GoldInactivity will end the tag after coinages doesn't change for the given period /// [XmlAttribute("timeoutValue")] public int TimeoutValue { get; set; } /// /// If we want to use a townportal before ending the tag when a timeout happens /// [XmlAttribute("townPortalOnTimeout")] public bool TownPortalOnTimeout { get; set; } /// /// Ignore last N nodes of dungeon explorer, when using endType=FullyExplored /// [XmlAttribute("ignoreLastNodes")] public int IgnoreLastNodes { get; set; } /// /// Used with IgnoreLastNodes, minimum visited node count before tag can end. /// The minVisistedNodes is purely, and only for use with ignoreLastNodes - it does not serve any other function like you expect. /// The reason this attribute exists, is to prevent prematurely exiting the dungeon exploration when used with ignoreLastNodes. /// For example, when the bot first starts exploring an area, it needs to navigate a few dungeon nodes first before other dungeon nodes even appear - otherwise with ignoreLastNodes > 2, /// the bot would immediately exit from navigation without exploring anything at all. /// [XmlAttribute("minVisitedNodes")] public int MinVisistedNodes { get; set; } [XmlAttribute("SetNodesExploredAutomatically")] [XmlAttribute("setNodesExploredAutomatically")] public bool SetNodesExploredAutomatically { get; set; } [XmlAttribute("minObjectOccurances")] public int MinOccurances { get; set; } [XmlAttribute("interactWithObject")] public bool InteractWithObject { get; set; } [XmlAttribute("interactRange")] public float ObjectInteractRange { get; set; } HashSet> foundObjects = new HashSet>(); /// /// The Position of the CurrentNode NavigableCenter /// private Vector3 CurrentNavTarget { get { if (PrioritySceneTarget != Vector3.Zero) { return PrioritySceneTarget; } if (GetRouteUnvisitedNodeCount() > 0) { return BrainBehavior.DungeonExplorer.CurrentNode.NavigableCenter; } else { return Vector3.Zero; } } } // Adding these for SimpleFollow compatability public float X { get { return CurrentNavTarget.X; } } public float Y { get { return CurrentNavTarget.Y; } } public float Z { get { return CurrentNavTarget.Z; } } private bool InitDone = false; private DungeonNode NextNode; /// /// The current player position /// private Vector3 myPos { get { return Trinity.Player.Position; } } private static ISearchAreaProvider MainGridProvider { get { return Trinity.MainGridProvider; } } /// /// The last scene SNOId we entered /// private int mySceneId = -1; /// /// The last position we updated the ISearchGridProvider at /// private Vector3 GPUpdatePosition = Vector3.Zero; /// /// Called when the profile behavior starts /// public override void OnStart() { Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, "TrinityExploreDungeon OnStart() called"); if (SetNodesExploredAutomatically) { Logger.Log(LogCategory.ProfileTag, "Minimap Explored Nodes Enabled"); BrainBehavior.DungeonExplorer.SetNodesExploredAutomatically = true; } else { Logger.Log(LogCategory.ProfileTag, "Minimap Explored Nodes Disabled"); BrainBehavior.DungeonExplorer.SetNodesExploredAutomatically = false; } if (!IgnoreGridReset) { UpdateSearchGridProvider(); CheckResetDungeonExplorer(); GridSegmentation.Reset(); BrainBehavior.DungeonExplorer.Reset(); MiniMapMarker.KnownMarkers.Clear(); } if (!InitDone) { Init(); } TagTimer.Reset(); timesForcedReset = 0; if (Objectives == null) Objectives = new List(); if (ObjectDistance == 0) ObjectDistance = 25f; PrintNodeCounts("PostInit"); } /// /// Re-sets the DungeonExplorer, BoxSize, BoxTolerance, and Updates the current route /// private void CheckResetDungeonExplorer() { if (!ZetaDia.IsInGame || ZetaDia.IsLoadingWorld || !ZetaDia.WorldInfo.IsValid || !ZetaDia.Scenes.IsValid || !ZetaDia.Service.IsValid) return; // I added this because GridSegmentation may (rarely) reset itself without us doing it to 15/.55. if ((BoxSize != 0 && BoxTolerance != 0) && (GridSegmentation.BoxSize != BoxSize || GridSegmentation.BoxTolerance != BoxTolerance) || (GetGridSegmentationNodeCount() == 0)) { Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, "Box Size or Tolerance has been changed! {0}/{1}", GridSegmentation.BoxSize, GridSegmentation.BoxTolerance); BrainBehavior.DungeonExplorer.Reset(); PrintNodeCounts("BrainBehavior.DungeonExplorer.Reset"); GridSegmentation.BoxSize = BoxSize; GridSegmentation.BoxTolerance = BoxTolerance; PrintNodeCounts("SetBoxSize+Tolerance"); BrainBehavior.DungeonExplorer.Update(); PrintNodeCounts("BrainBehavior.DungeonExplorer.Update"); } } /// /// The main profile behavior /// /// protected override Composite CreateBehavior() { return new Sequence( new DecoratorContinue(ret => !IgnoreMarkers, new Sequence( MiniMapMarker.DetectMiniMapMarkers(0), MiniMapMarker.DetectMiniMapMarkers(ExitNameHash), MiniMapMarker.DetectMiniMapMarkers(Objectives) ) ), UpdateSearchGridProvider(), new Action(ret => CheckResetDungeonExplorer()), new PrioritySelector( CheckIsObjectiveFinished(), PrioritySceneCheck(), new Decorator(ret => !IgnoreMarkers, MiniMapMarker.VisitMiniMapMarkers(myPos, MarkerDistance) ), new Decorator(ret => ShouldInvestigateActor(), new PrioritySelector( new Decorator(ret => CurrentActor != null && CurrentActor.IsValid && Objectives.Any(o => o.ActorID == CurrentActor.ActorSNO && o.Interact) && CurrentActor.Position.Distance(Trinity.Player.Position) <= CurrentActor.CollisionSphere.Radius, new Sequence( new Action(ret => CurrentActor.Interact()) ) ), InvestigateActor() ) ), new Sequence( new DecoratorContinue(ret => DungeonRouteIsEmpty(), new Action(ret => UpdateRoute()) ), CheckIsExplorerFinished() ), new DecoratorContinue(ret => DungeonRouteIsValid(), new PrioritySelector( CheckNodeFinished(), new Sequence( new Action(ret => PrintNodeCounts("MainBehavior")), new Action(ret => MoveToNextNode()) ) ) ), new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, "Error 1: Unknown error occured!")) ) ); } private static bool DungeonRouteIsValid() { return BrainBehavior.DungeonExplorer != null && BrainBehavior.DungeonExplorer.CurrentRoute != null && BrainBehavior.DungeonExplorer.CurrentRoute.Any(); } private static bool DungeonRouteIsEmpty() { return BrainBehavior.DungeonExplorer != null && BrainBehavior.DungeonExplorer.CurrentRoute != null && !BrainBehavior.DungeonExplorer.CurrentRoute.Any(); } private bool CurrentActorIsFinished { get { return Objectives.Any(o => o.ActorID == CurrentActor.ActorSNO && o.EndAnimation == CurrentActor.CommonData.CurrentAnimation); } } private DiaObject CurrentActor { get { var actor = ZetaDia.Actors.GetActorsOfType(true, false) .Where(a => (a.ActorSNO == ActorId || Objectives.Any(o => o.ActorID != 0 && o.ActorID == a.ActorSNO)) && Trinity.SkipAheadAreaCache.Any(o => o.Position.Distance2D(a.Position) >= ObjectDistance && !foundObjects.Any(fo => fo != new Tuple(o.ActorSNO, o.Position)))) .OrderBy(o => o.Distance) .FirstOrDefault(); if (actor != null && actor.IsValid) return actor; return default(DiaObject); } } private Composite InvestigateActor() { return new Action(ret => { RecordPosition(); var actor = ZetaDia.Actors.GetActorsOfType(true, false).FirstOrDefault(a => a.ActorSNO == ActorId); if (actor != null && actor.IsValid && actor.Position.Distance2D(myPos) >= ObjectDistance) PlayerMover.NavigateTo(actor.Position); }); } private bool ShouldInvestigateActor() { if (ActorId == 0 || Objectives.All(o => o.ActorID == 0)) return false; var actors = ZetaDia.Actors.GetActorsOfType(true, false) .Where(a => (a.ActorSNO == ActorId || Objectives.Any(o => o.ActorID != 0 && o.ActorID == a.ActorSNO)) && Trinity.SkipAheadAreaCache.Any(o => o.Position.Distance2D(a.Position) >= ObjectDistance && !foundObjects.Any(fo => fo != new Tuple(o.ActorSNO, o.Position)))); if (actors == null) return false; if (!actors.Any()) return false; var actor = actors.OrderBy(a => a.Distance).FirstOrDefault(); if (actor.Distance <= ObjectDistance) return false; return true; } /// /// Updates the search grid provider as needed /// /// private Composite UpdateSearchGridProvider() { return new DecoratorContinue(ret => mySceneId != Trinity.Player.SceneId || Vector3.Distance(myPos, GPUpdatePosition) > 150, new Sequence( new Action(ret => mySceneId = Trinity.Player.SceneId), new Action(ret => Navigator.SearchGridProvider.Update()), new Action(ret => GPUpdatePosition = myPos), new Action(ret => MiniMapMarker.UpdateFailedMarkers()) ) ); } /// /// Checks if we are using a timeout and will end the tag if the timer has breached the given value /// /// private Composite TimeoutCheck() { return new PrioritySelector( new Decorator(ret => timeoutBreached, new Sequence( new DecoratorContinue(ret => TownPortalOnTimeout && !Trinity.Player.IsInTown, new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "TrinityExploreDungeon inactivity timer tripped ({0}), tag Using Town Portal!", TimeoutValue)), Zeta.Bot.CommonBehaviors.CreateUseTownPortal(), new Action(ret => isDone = true) ) ), new DecoratorContinue(ret => !TownPortalOnTimeout, new Action(ret => isDone = true) ) ) ), new Decorator(ret => ExploreTimeoutType == TimeoutType.Timer, new Action(ret => CheckSetTimer(ret)) ), new Decorator(ret => ExploreTimeoutType == TimeoutType.GoldInactivity, new Action(ret => CheckSetGoldInactive(ret)) ) ); } bool timeoutBreached = false; Stopwatch TagTimer = new Stopwatch(); /// /// Will start the timer if needed, and end the tag if the timer has exceeded the TimeoutValue /// /// /// private RunStatus CheckSetTimer(object ctx) { if (!TagTimer.IsRunning) { TagTimer.Start(); return RunStatus.Failure; } if (ExploreTimeoutType == TimeoutType.Timer && TagTimer.Elapsed.TotalSeconds > TimeoutValue) { Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "TrinityExploreDungeon timer ended ({0}), tag finished!", TimeoutValue); timeoutBreached = true; return RunStatus.Success; } return RunStatus.Failure; } private int lastCoinage = -1; /// /// Will check if the bot has not picked up any gold within the allocated TimeoutValue /// /// /// private RunStatus CheckSetGoldInactive(object ctx) { CheckSetTimer(ctx); if (lastCoinage == -1) { lastCoinage = Trinity.Player.Coinage; return RunStatus.Failure; } else if (lastCoinage != Trinity.Player.Coinage) { TagTimer.Restart(); return RunStatus.Failure; } else if (lastCoinage == Trinity.Player.Coinage && TagTimer.Elapsed.TotalSeconds > TimeoutValue) { Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "TrinityExploreDungeon gold inactivity timer tripped ({0}), tag finished!", TimeoutValue); timeoutBreached = true; return RunStatus.Success; } return RunStatus.Failure; } private int timesForcedReset = 0; private int timesForceResetMax = 5; /// /// Checks to see if the tag is finished as needed /// /// private Composite CheckIsExplorerFinished() { return new PrioritySelector( CheckIsObjectiveFinished(), new Decorator(ret => GetRouteUnvisitedNodeCount() == 0 && timesForcedReset > timesForceResetMax, new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "Visited all nodes but objective not complete, forced reset more than {0} times, finished!", timesForceResetMax)), new Action(ret => isDone = true) ) ), new Decorator(ret => GetRouteUnvisitedNodeCount() == 0, new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "Visited all nodes but objective not complete, forcing grid reset!")), new Action(ret => timesForcedReset++), new Action(ret => Trinity.SkipAheadAreaCache.Clear()), new Action(ret => MiniMapMarker.KnownMarkers.Clear()), new Action(ret => ForceUpdateScenes()), new Action(ret => GridSegmentation.Reset()), new Action(ret => GridSegmentation.Update()), new Action(ret => BrainBehavior.DungeonExplorer.Reset()), new Action(ret => PriorityScenesInvestigated.Clear()), new Action(ret => UpdateRoute()) ) ) ); } private void ForceUpdateScenes() { foreach (Scene scene in ZetaDia.Scenes.GetScenes().ToList()) { scene.UpdatePointer(scene.BaseAddress); } } /// /// Checks to see if the tag is finished as needed /// /// private Composite CheckIsObjectiveFinished() { return new PrioritySelector( TimeoutCheck(), new Decorator(ret => LeaveAfterBounty && GetIsBountyDone(), new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "Bounty is done. Tag Finished.", IgnoreLastNodes)), new Action(ret => isDone = true) ) ), new Decorator(ret => EndType == TrinityExploreEndType.FullyExplored && IgnoreLastNodes > 0 && GetRouteUnvisitedNodeCount() <= IgnoreLastNodes && GetGridSegmentationVisistedNodeCount() >= MinVisistedNodes, new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "Fully explored area! Ignoring {0} nodes. Tag Finished.", IgnoreLastNodes)), new Action(ret => isDone = true) ) ), new Decorator(ret => EndType == TrinityExploreEndType.FullyExplored && GetRouteUnvisitedNodeCount() == 0, new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "Fully explored area! Tag Finished.", 0)), new Action(ret => isDone = true) ) ), new Decorator(ret => EndType == TrinityExploreEndType.ExitFound && ExitNameHash != 0 && IsExitNameHashVisible(), new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "Found exitNameHash {0}!", ExitNameHash)), new Action(ret => isDone = true) ) ), new Decorator(ret => (EndType == TrinityExploreEndType.ObjectFound || EndType == TrinityExploreEndType.SceneLeftOrActorFound) && ActorId != 0 && ZetaDia.Actors.GetActorsOfType(true, false) .Any(a => a.ActorSNO == ActorId && a.Distance <= ObjectDistance), new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "Found Object {0}!", ActorId)), new Action(ret => isDone = true) ) ), new Decorator(ret => (EndType == TrinityExploreEndType.ObjectFound || EndType == TrinityExploreEndType.SceneLeftOrActorFound) && AlternateActorsFound(), new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "Found Alternate Object {0}!", GetAlternateActor().ActorSNO)), new Action(ret => isDone = true) ) ), new Decorator(ret => EndType == TrinityExploreEndType.SceneFound && Trinity.Player.SceneId == SceneId, new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "Found SceneId {0}!", SceneId)), new Action(ret => isDone = true) ) ), new Decorator(ret => EndType == TrinityExploreEndType.SceneFound && !string.IsNullOrWhiteSpace(SceneName) && ZetaDia.Me.CurrentScene.Name.ToLower().Contains(SceneName.ToLower()), new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "Found SceneName {0}!", SceneName)), new Action(ret => isDone = true) ) ), new Decorator(ret => EndType == TrinityExploreEndType.SceneLeftOrActorFound && SceneId != 0 && SceneIdLeft(), new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "Left SceneId {0}!", SceneId)), new Action(ret => isDone = true) ) ), new Decorator(ret => (EndType == TrinityExploreEndType.SceneFound || EndType == TrinityExploreEndType.SceneLeftOrActorFound) && !string.IsNullOrWhiteSpace(SceneName) && SceneNameLeft(), new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "Left SceneName {0}!", SceneName)), new Action(ret => isDone = true) ) ), new Decorator(ret => Trinity.Player.IsInTown, new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.UserInformation, "Cannot use TrinityExploreDungeon in town - tag finished!", SceneName)), new Action(ret => isDone = true) ) ) ); } private bool AlternateActorsFound() { return AlternateActors.Any() && ZetaDia.Actors.GetActorsOfType(true, false) .Where(o => AlternateActors.Any(a => a.ActorId == o.ActorSNO && o.Distance <= a.ObjectDistance)).Any(); } private bool SceneIdLeft() { return Trinity.Player.SceneId != SceneId; } private bool SceneNameLeft() { return !ZetaDia.Me.CurrentScene.Name.ToLower().Contains(SceneName.ToLower()) && AlternateScenes != null && AlternateScenes.Any() && AlternateScenes.All(o => !ZetaDia.Me.CurrentScene.Name.ToLower().Contains(o.SceneName.ToLower())); } private DiaObject GetAlternateActor() { return ZetaDia.Actors.GetActorsOfType(true, false) .Where(o => AlternateActors.Any(a => a.ActorId == o.ActorSNO && o.Distance <= a.ObjectDistance)).OrderBy(o => o.Distance).FirstOrDefault(); } /// /// Determine if the tag ExitNameHash is visible in the list of Current World Markers /// /// private bool IsExitNameHashVisible() { return ZetaDia.Minimap.Markers.CurrentWorldMarkers.Any(m => m.NameHash == ExitNameHash && m.Position.Distance2D(myPos) <= MarkerDistance + 10f); } private Vector3 PrioritySceneTarget = Vector3.Zero; private int PrioritySceneSNOId = -1; private Scene CurrentPriorityScene = null; private float PriorityScenePathPrecision = -1f; /// /// A list of Scene SNOId's that have already been investigated /// private List PriorityScenesInvestigated = new List(); private DateTime lastCheckedScenes = DateTime.MinValue; /// /// Will find and move to Prioritized Scene's based on Scene SNOId or Name /// /// private Composite PrioritySceneCheck() { return new Decorator(ret => PriorityScenes != null && PriorityScenes.Any(), new Sequence( new DecoratorContinue(ret => DateTime.UtcNow.Subtract(lastCheckedScenes).TotalMilliseconds > 1000, new Sequence( new Action(ret => lastCheckedScenes = DateTime.UtcNow), new Action(ret => FindPrioritySceneTarget()) ) ), new Decorator(ret => PrioritySceneTarget != Vector3.Zero, new PrioritySelector( new Decorator(ret => PrioritySceneTarget.Distance2D(myPos) <= PriorityScenePathPrecision, new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, "Successfully navigated to priority scene {0} {1} center {2} Distance {3:0}", CurrentPriorityScene.Name, CurrentPriorityScene.SceneInfo.SNOId, PrioritySceneTarget, PrioritySceneTarget.Distance2D(myPos))), new Action(ret => PrioritySceneMoveToFinished()) ) ), new Action(ret => MoveToPriorityScene()) ) ) ) ); } /// /// Handles actual movement to the Priority Scene /// private void MoveToPriorityScene() { Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, "Moving to Priority Scene {0} - {1} Center {2} Distance {3:0}", CurrentPriorityScene.Name, CurrentPriorityScene.SceneInfo.SNOId, PrioritySceneTarget, PrioritySceneTarget.Distance2D(myPos)); MoveResult moveResult = PlayerMover.NavigateTo(PrioritySceneTarget); if (moveResult == MoveResult.PathGenerationFailed || moveResult == MoveResult.ReachedDestination) { Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, "Unable to navigate to Scene {0} - {1} Center {2} Distance {3:0}, cancelling!", CurrentPriorityScene.Name, CurrentPriorityScene.SceneInfo.SNOId, PrioritySceneTarget, PrioritySceneTarget.Distance2D(myPos)); PrioritySceneMoveToFinished(); } } /// /// Sets a priority scene as finished /// private void PrioritySceneMoveToFinished() { PriorityScenesInvestigated.Add(PrioritySceneSNOId); PrioritySceneSNOId = -1; PrioritySceneTarget = Vector3.Zero; UpdateRoute(); } /// /// Finds a navigable point in a priority scene /// private void FindPrioritySceneTarget() { if (!PriorityScenes.Any()) return; if (PrioritySceneTarget != Vector3.Zero) return; bool foundPriorityScene = false; // find any matching priority scenes in scene manager - match by name or SNOId List PScenes = ZetaDia.Scenes.GetScenes() .Where(s => PriorityScenes.Any(ps => ps.SceneId != -1 && s.SceneInfo.SNOId == ps.SceneId)).ToList(); PScenes.AddRange(ZetaDia.Scenes.GetScenes() .Where(s => PriorityScenes.Any(ps => ps.SceneName.Trim() != String.Empty && ps.SceneId == -1 && s.Name.ToLower().Contains(ps.SceneName.ToLower()))).ToList()); List foundPriorityScenes = new List(); Dictionary foundPrioritySceneIndex = new Dictionary(); foreach (Scene scene in PScenes) { if (!scene.IsValid) continue; if (!scene.SceneInfo.IsValid) continue; if (!scene.Mesh.Zone.IsValid) continue; if (!scene.Mesh.Zone.NavZoneDef.IsValid) continue; if (PriorityScenesInvestigated.Contains(scene.SceneInfo.SNOId)) continue; foundPriorityScene = true; NavZone navZone = scene.Mesh.Zone; NavZoneDef zoneDef = navZone.NavZoneDef; Vector2 zoneMin = navZone.ZoneMin; Vector2 zoneMax = navZone.ZoneMax; Vector3 zoneCenter = GetNavZoneCenter(navZone); List NavCells = zoneDef.NavCells.Where(c => c.IsValid && c.Flags.HasFlag(NavCellFlags.AllowWalk)).ToList(); if (!NavCells.Any()) continue; NavCell bestCell = NavCells.OrderBy(c => GetNavCellCenter(c.Min, c.Max, navZone).Distance2D(zoneCenter)).FirstOrDefault(); if (bestCell != null && !foundPrioritySceneIndex.ContainsKey(scene.SceneInfo.SNOId)) { foundPrioritySceneIndex.Add(scene.SceneInfo.SNOId, GetNavCellCenter(bestCell, navZone)); foundPriorityScenes.Add(scene); } else { Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, "Found Priority Scene but could not find a navigable point!", true); } } if (foundPrioritySceneIndex.Any()) { KeyValuePair nearestPriorityScene = foundPrioritySceneIndex.OrderBy(s => s.Value.Distance2D(myPos)).FirstOrDefault(); PrioritySceneSNOId = nearestPriorityScene.Key; PrioritySceneTarget = nearestPriorityScene.Value; CurrentPriorityScene = foundPriorityScenes.FirstOrDefault(s => s.SceneInfo.SNOId == PrioritySceneSNOId); PriorityScenePathPrecision = GetPriorityScenePathPrecision(PScenes.FirstOrDefault(s => s.SceneInfo.SNOId == nearestPriorityScene.Key)); Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, "Found Priority Scene {0} - {1} Center {2} Distance {3:0}", CurrentPriorityScene.Name, CurrentPriorityScene.SceneInfo.SNOId, PrioritySceneTarget, PrioritySceneTarget.Distance2D(myPos)); } if (!foundPriorityScene) { PrioritySceneTarget = Vector3.Zero; } } private float GetPriorityScenePathPrecision(Scene scene) { return PriorityScenes.FirstOrDefault(ps => ps.SceneId != 0 && ps.SceneId == scene.SceneInfo.SNOId || scene.Name.ToLower().Contains(ps.SceneName.ToLower())).PathPrecision; } /// /// Gets the center of a given Navigation Zone /// /// /// private Vector3 GetNavZoneCenter(NavZone zone) { float X = zone.ZoneMin.X + ((zone.ZoneMax.X - zone.ZoneMin.X) / 2); float Y = zone.ZoneMin.Y + ((zone.ZoneMax.Y - zone.ZoneMin.Y) / 2); return new Vector3(X, Y, 0); } /// /// Gets the center of a given Navigation Cell /// /// /// /// private Vector3 GetNavCellCenter(NavCell cell, NavZone zone) { return GetNavCellCenter(cell.Min, cell.Max, zone); } /// /// Gets the center of a given box with min/max, adjusted for the Navigation Zone /// /// /// /// /// private Vector3 GetNavCellCenter(Vector3 min, Vector3 max, NavZone zone) { float X = zone.ZoneMin.X + min.X + ((max.X - min.X) / 2); float Y = zone.ZoneMin.Y + min.Y + ((max.Y - min.Y) / 2); float Z = min.Z + ((max.Z - min.Z) / 2); return new Vector3(X, Y, Z); } /// /// Checks to see if the current DungeonExplorer node is in an Ignored scene, and marks the node immediately visited if so /// /// private Composite CheckIgnoredScenes() { return new Decorator(ret => timesForcedReset == 0 && IgnoreScenes != null && IgnoreScenes.Any(), new PrioritySelector( new Decorator(ret => IsPositionInsideIgnoredScene(CurrentNavTarget), new Sequence( new Action(ret => SetNodeVisited("Node is in Ignored Scene")) ) ) ) ); } private bool IsPositionInsideIgnoredScene(Vector3 position) { return IgnoredAreas.Any(a => a.IsPositionInside(position)); } /// /// Determines if a given Vector3 is in a provided IgnoreScene (if the scene is loaded) /// /// /// private bool PositionInsideIgnoredScene(Vector3 position) { List ignoredScenes = ZetaDia.Scenes.GetScenes() .Where(scn => scn.IsValid && (IgnoreScenes.Any(igscn => !string.IsNullOrWhiteSpace(igscn.SceneName) && scn.Name.ToLower().Contains(igscn.SceneName.ToLower())) || IgnoreScenes.Any(igscn => scn.SceneInfo.SNOId == igscn.SceneId) && !PriorityScenes.Any(psc => !string.IsNullOrWhiteSpace(psc.SceneName) && scn.Name.ToLower().Contains(psc.SceneName)) && !PriorityScenes.Any(psc => psc.SceneId != -1 && scn.SceneInfo.SNOId != psc.SceneId))).ToList(); foreach (Scene scene in ignoredScenes) { if (scene.Mesh.Zone == null) return true; Vector2 pos = position.ToVector2(); Vector2 min = scene.Mesh.Zone.ZoneMin; Vector2 max = scene.Mesh.Zone.ZoneMax; if (pos.X >= min.X && pos.X <= max.X && pos.Y >= min.Y && pos.Y <= max.Y) return true; } return false; } /// /// Determines if the current node can be marked as Visited, and does so if needed /// /// private Composite CheckNodeFinished() { return new PrioritySelector( new Decorator(ret => LastMoveResult == MoveResult.ReachedDestination, new Sequence( new Action(ret => SetNodeVisited("Reached Destination")), new Action(ret => LastMoveResult = MoveResult.Moved), new Action(ret => UpdateRoute()) ) ), new Decorator(ret => BrainBehavior.DungeonExplorer.CurrentNode.Visited, new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, "Current node was already marked as visited!")), new Action(ret => BrainBehavior.DungeonExplorer.CurrentRoute.Dequeue()), new Action(ret => UpdateRoute()) ) ), new Decorator(ret => GetRouteUnvisitedNodeCount() == 0 || !BrainBehavior.DungeonExplorer.CurrentRoute.Any(), new Sequence( new Action(ret => Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, "Error - CheckIsNodeFinished() called while Route is empty!")), new Action(ret => UpdateRoute()) ) ), new Decorator(ret => CurrentNavTarget.Distance2D(myPos) <= PathPrecision, new Sequence( new Action(ret => SetNodeVisited(String.Format("Node {0} is within PathPrecision ({1:0}/{2:0})", CurrentNavTarget, CurrentNavTarget.Distance2D(myPos), PathPrecision))), new Action(ret => UpdateRoute()) ) ), new Decorator(ret => CurrentNavTarget.Distance2D(myPos) <= 90f && !MainGridProvider.CanStandAt(MainGridProvider.WorldToGrid(CurrentNavTarget.ToVector2())), new Sequence( new Action(ret => SetNodeVisited("Center Not Navigable")), new Action(ret => UpdateRoute()) ) ), new Decorator(ret => CacheData.NavigationObstacles.Any(o => o.Position.Distance2D(CurrentNavTarget) <= o.Radius * 2), new Sequence( new Action(ret => SetNodeVisited("Navigation obstacle detected at node point")), new Action(ret => UpdateRoute()) ) ), //new Decorator(ret => PlayerMover.MovementSpeed == 0 && myPos.Distance2D(CurrentNavTarget) <= 50f && !Navigator.Raycast(myPos, CurrentNavTarget), // new Sequence( // new Action(ret => SetNodeVisited("Stuck moving to node point, marking done (in LoS and nearby!)")), // new Action(ret => UpdateRoute()) // ) //), new Decorator(ret => Trinity.SkipAheadAreaCache.Any(p => p.Position.Distance2D(CurrentNavTarget) <= PathPrecision), new Sequence( new Action(ret => SetNodeVisited("Found node to be in skip ahead cache, marking done")), new Action(ret => UpdateRoute()) ) ), CheckIgnoredScenes() ); } /// /// Updates the DungeonExplorer Route /// private void UpdateRoute() { CheckResetDungeonExplorer(); BrainBehavior.DungeonExplorer.Update(); PrintNodeCounts("BrainBehavior.DungeonExplorer.Update"); // Throw an exception if this shiz don't work ValidateCurrentRoute(); } /// /// Marks the current dungeon Explorer as Visited and dequeues it from the route /// /// private void SetNodeVisited(string reason = "") { Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, "Dequeueing current node {0} - {1}", BrainBehavior.DungeonExplorer.CurrentNode.NavigableCenter, reason); BrainBehavior.DungeonExplorer.CurrentNode.Visited = true; BrainBehavior.DungeonExplorer.CurrentRoute.Dequeue(); MarkNearbyNodesVisited(); PrintNodeCounts("SetNodeVisited"); } public void MarkNearbyNodesVisited() { foreach (DungeonNode node in GridSegmentation.Nodes.Where(n => !n.Visited)) { float distance = node.NavigableCenter.Distance2D(myPos); if (distance <= PathPrecision) { node.Visited = true; string reason2 = String.Format("Node {0} is within path precision {1:0}/{2:0}", node.NavigableCenter, distance, PathPrecision); Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, "Marking unvisited nearby node as visited - {0}", reason2); } } } /// /// Makes sure the current route is not null! Bad stuff happens if it's null... /// private static void ValidateCurrentRoute() { if (BrainBehavior.DungeonExplorer.CurrentRoute == null) { throw new ApplicationException("DungeonExplorer CurrentRoute is null"); } } /// /// Prints a plethora of useful information about the Dungeon Exploration process /// /// private void PrintNodeCounts(string step = "") { if (Trinity.Settings.Advanced.LogCategories.HasFlag(LogCategory.ProfileTag)) { string nodeDistance = String.Empty; if (GetRouteUnvisitedNodeCount() > 0) { try { float distance = BrainBehavior.DungeonExplorer.CurrentNode.NavigableCenter.Distance(myPos); if (distance > 0) nodeDistance = String.Format("Dist:{0:0}", Math.Round(distance / 10f, 2) * 10f); } catch { } } var log = String.Format("Nodes [Unvisited: Route:{1} Grid:{3} | Grid-Visited: {2}] Box:{4}/{5} Step:{6} {7} Nav:{8} RayCast:{9} PP:{10:0} Dir: {11} ZDiff:{12:0}", GetRouteVisistedNodeCount(), // 0 GetRouteUnvisitedNodeCount(), // 1 GetGridSegmentationVisistedNodeCount(), // 2 GetGridSegmentationUnvisitedNodeCount(), // 3 GridSegmentation.BoxSize, // 4 GridSegmentation.BoxTolerance, // 5 step, // 6 nodeDistance, // 7 MainGridProvider.CanStandAt(MainGridProvider.WorldToGrid(CurrentNavTarget.ToVector2())), // 8 !Navigator.Raycast(myPos, CurrentNavTarget), PathPrecision, MathUtil.GetHeadingToPoint(CurrentNavTarget), Math.Abs(myPos.Z - CurrentNavTarget.Z) ); Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, log); } } /* * Dungeon Explorer Nodes */ /// /// Gets the number of unvisited nodes in the DungeonExplorer Route /// /// private int GetRouteUnvisitedNodeCount() { if (GetCurrentRouteNodeCount() > 0) return BrainBehavior.DungeonExplorer.CurrentRoute.Count(n => !n.Visited); else return 0; } /// /// Gets the number of visisted nodes in the DungeonExplorer Route /// /// private int GetRouteVisistedNodeCount() { if (GetCurrentRouteNodeCount() > 0) return BrainBehavior.DungeonExplorer.CurrentRoute.Count(n => n.Visited); else return 0; } /// /// Gets the number of nodes in the DungeonExplorer Route /// /// private int GetCurrentRouteNodeCount() { if (BrainBehavior.DungeonExplorer.CurrentRoute != null) return BrainBehavior.DungeonExplorer.CurrentRoute.Count(); else return 0; } /* * Grid Segmentation Nodes */ /// /// Gets the number of Unvisited nodes as reported by the Grid Segmentation provider /// /// private int GetGridSegmentationUnvisitedNodeCount() { if (GetGridSegmentationNodeCount() > 0) return GridSegmentation.Nodes.Count(n => !n.Visited); else return 0; } /// /// Gets the number of Visited nodes as reported by the Grid Segmentation provider /// /// private int GetGridSegmentationVisistedNodeCount() { if (GetCurrentRouteNodeCount() > 0) return GridSegmentation.Nodes.Count(n => n.Visited); else return 0; } /// /// Gets the total number of nodes with the current BoxSize/Tolerance as reported by the Grid Segmentation Provider /// /// private int GetGridSegmentationNodeCount() { if (GridSegmentation.Nodes != null) return GridSegmentation.Nodes.Count(); else return 0; } private MoveResult LastMoveResult = MoveResult.Moved; private DateTime lastGeneratedPath = DateTime.MinValue; /// /// Moves the bot to the next DungeonExplorer node /// private void MoveToNextNode() { PlayerMover.RecordSkipAheadCachePoint(); NextNode = BrainBehavior.DungeonExplorer.CurrentNode; Vector3 moveTarget = NextNode.NavigableCenter; Vector3 lastPlayerMoverTarget = PlayerMover.LastMoveToTarget; bool isStuck = DateTime.UtcNow.Subtract(PlayerMover.LastRecordedAnyStuck).TotalMilliseconds < 500; if (isStuck && CacheData.NavigationObstacles.Any(o => MathUtil.IntersectsPath(o.Position, o.Radius, Trinity.Player.Position, lastPlayerMoverTarget))) { SetNodeVisited("Nav Obstacle detected!"); UpdateRoute(); return; } string nodeName = String.Format("{0} Distance: {1:0} Direction: {2}", NextNode.NavigableCenter, NextNode.NavigableCenter.Distance(Trinity.Player.Position), MathUtil.GetHeadingToPoint(NextNode.NavigableCenter)); RecordPosition(); LastMoveResult = PlayerMover.NavigateTo(CurrentNavTarget); //Navigator.MoveTo(CurrentNavTarget); } private void RecordPosition() { if (DateTime.UtcNow.Subtract(Trinity.lastAddedLocationCache).TotalMilliseconds >= 100) { Trinity.lastAddedLocationCache = DateTime.UtcNow; if (Vector3.Distance(myPos, Trinity.LastRecordedPosition) >= 5f) { MarkNearbyNodesVisited(); Trinity.SkipAheadAreaCache.Add(new CacheObstacleObject(myPos, 20f, 0)); Trinity.LastRecordedPosition = myPos; } } } /// /// Initizializes the profile tag and sets defaults as needed /// private void Init(bool forced = false) { if (BoxSize == 0) BoxSize = 15; if (BoxTolerance == 0) BoxTolerance = 0.55f; if (PathPrecision == 0) PathPrecision = BoxSize / 2f; float minPathPrecision = 15f; if (PathPrecision < minPathPrecision) PathPrecision = minPathPrecision; if (ObjectDistance == 0) ObjectDistance = 40f; if (MarkerDistance == 0) MarkerDistance = 25f; if (TimeoutValue == 0) TimeoutValue = 1800; Trinity.SkipAheadAreaCache.Clear(); PriorityScenesInvestigated.Clear(); MiniMapMarker.KnownMarkers.Clear(); if (PriorityScenes == null) PriorityScenes = new List(); if (IgnoreScenes == null) IgnoreScenes = new List(); if (AlternateActors == null) AlternateActors = new List(); if (!forced) { Logger.Log(TrinityLogLevel.Info, LogCategory.ProfileTag, "Initialized TrinityExploreDungeon: boxSize={0} boxTolerance={1:0.00} endType={2} timeoutType={3} timeoutValue={4} pathPrecision={5:0} sceneId={6} actorId={7} objectDistance={8} markerDistance={9} exitNameHash={10}", GridSegmentation.BoxSize, GridSegmentation.BoxTolerance, EndType, ExploreTimeoutType, TimeoutValue, PathPrecision, SceneId, ActorId, ObjectDistance, MarkerDistance, ExitNameHash); } InitDone = true; } private bool isDone = false; /// /// When true, the next profile tag is used /// public override bool IsDone { get { return !IsActiveQuestStep || isDone; } } /// /// Resets this profile tag to defaults /// public override void ResetCachedDone() { isDone = false; InitDone = false; } //Had to use ActiveBounty, caused too much lag constantly searching the bounty list public bool GetIsBountyDone() { // Only valid for Adventure mode if (ZetaDia.CurrentAct != Act.OpenWorld) return false; try { var b = ZetaDia.ActInfo.ActiveBounty; if (b == null) { Logger.Log("Active bounty returned null, Assuming done."); return true; } if (!b.Info.IsValid) { Logger.Log("Does this even work? Thinks the bounty is not valid."); } if (b.Info.QuestSNO > 500000 || b.Info.QuestSNO < 200000) { Logger.Log("Got some weird numbers going on with the QuestSNO of the active bounty. Assuming glitched and done."); return true; } //If completed or on next step, we are good. if (b.Info.State == QuestState.Completed) { Logger.Log("Seems completed!"); return true; } if (ZetaDia.IsInTown) { return true; } if (ZetaDia.Me.IsInBossEncounter) { return false; } } catch (Exception ex) { Logger.LogError("Exception reading ActiveBounty " + ex.Message); } return false; } } } /* * Never need to call GridSegmentation.Update() * GridSegmentation.Reset() is automatically called on world change * DungeonExplorer.Reset() will reset the current route and revisit nodes * DungeonExplorer.Update() will update the current route to include new scenes */