User-generated content: The content of the Buddy Wiki is generated by Buddy Community members and volunteers.
The Wiki content is neither created nor maintained by Bossland GmbH, or employees of Bossland GmbH.

Honorbuddy:Developer Notebook:Understanding the ObjectManager

From The Buddy Wiki
Jump to: navigation, search

ForumLink: [i][u][url=]Honorbuddy:Developer Notebook:Understanding the ObjectManager[/url][/u][/i]

Other articles in the Honorbuddy:Developer Notebook category.

What is the ObjectManager?

Figure 1.1. The children of WoWObject.
The Honorbuddy API's ObjectManager maintains an in-memory database of all WoWObject-derived classes that are currently visible to the WoWclient. Figure 1.1 shows the WoWObject inheritance lattice which includes the following types:

How does one use the ObjectManager?

The modus operandi of ObjectManager use involves C# LINQ queries (ref: LINQ Introduction, LINQ Basic Queries). Depending on the context, the user may want a scalar (single value), or a container (e.g., List) returned from the query. An example query that returns a List container is shown below:
private IEnumerable<WoWUnit> FindMobsOfInterest()
   int[] mobIdsOfInterest = { 12345, 23456, 34567, 45678 };
   using (StyxWoW.Memory.AcquireFrame())
       IEnumerable<WoWUnit> mobsOfInterestQuery =
           // By setting 'allowInheritence' to false, we only get objects of type 'WoWUnit',
           // and not 'WoWPlayer' and 'LocalPlayer'.
           from wowUnit in ObjectManager.GetObjectsOfType<WoWUnit>(false, false)
               && wowUnit.IsAlive
               && !wowUnit.TaggedByOther
       // ToList() below forces immediate LINQ evaluation...
       // This allows the caller to iterate over a cheaper list (and possibly multiple times),
       // rather than re-running a deferred LINQ query that will be expensive to iterate more than once.
       return mobsOfInterestQuery.ToList();
Why is the .ToList() used on the query?
LINQ queries normally operate in deferred execution mode. Deferred execution is a form of lazy evaluation—a technique that is used to improve performance. By their nature, LINQ expressions are somewhat computationally expensive.
Due to this expense, you do not want a particular LINQ query that returns a 'container' to evaluate more than once. This is not a problem when a scalar value is returned. However, if the user will be using the ObjectManager to return a container-type (e.g., Array, List, etc.), you will want to force LINQ to immediately evaluate by appending a .ToList() term to the LINQ query. This allows the list to be further examined and processed without re-running the same LINQ query.
Why is the query wrapped in an AcquireFrame()
The AcquireFrame forces the LINQ query to completely evaluate in one frame.[1] This is important for queries that will be processing large amounts of data:
  • You want the states of all objects in the list to be consistent
E.g., you don't want a mob you're looking at to be alive in one frame, and dead in the next. This means your list may contain 'invalid' elements that will throw exceptions if you try to access them.
  • You want the query to provide an immediate answer, not take several seconds
Without the AcquireFrame, the work imposed by the query will be spread out over several frames, and it could possibly take several seconds to get the answer you need immediately.
The AcquireFrame is only useful for queries that are evaluated immediately. If the query is allowed to defer execution, then the AcquireFrame() just wastes time.
Large queries are obviously doing a lot of work. As such, you most certainly want to limit how often the query is run. It would be best to cache the answers from the LINQ query until you determine that the cached answer is invalid, or no longer of interest.

Things to Know when using the ObjectManager

  • Minimize your use of the ObjectManager as much as possible.
If you need to query, you need to query. But, be aware that other Honorbuddy subsystems will be running (possibly expensive) queries also.
  • Be aware that the ObjectManager is a database.
To answer certain questions, you must make queries against the ObjectManager. Database queries are generally computationally expensive to begin with. Indiscriminate use of inefficient queries can destroy Honorbuddy's performance rather quickly.
  • Optimize your queries to filter out the maximum number of unwanted objects early
Generally, the first filter is based on 'type'. If you want a WoWPlayer, the type-based query will make this determination inherently. For instance,
var myPlayer = ObjectManger.GetObjectsOfType<WoWGameObject>() ...
...will filter out all unwanted types except for WoWGameObjects. The next best way is to filter by Entry to eliminate all the WoWGameObjects that do not match what we seek:
var myPlayer = ObjectManger.GetObjectsOfType<WoWGameObject>().FirstOrDefault(go => go.Entry == 12345) ...
You would want to perform the most selective checks first. Doing this weeds out the most WoWObjects, before you go making 'general' checks like IsAlive that apply to a large quantity of WoWObjects.
  • Cache your 'answers' from ObjectManager queries
For instance, rather than re-selecting you goal target (with an ObjectManager query) on each tick, select the target then cache it in a local variable until the cached answer becomes invalid, or no longer of interest.
  • "Cached answers" must be checked for validity
The WoWclient may have marked an object you have cached for deletion. If this happens, then attempting to access the properties or methods of that object will cause exceptions to be thrown. Cached objects should always be tested with WoWObject.IsValid before attempts are made to access any other of the cached object's properties or methods. Cached objects that are no longer valid should be immediately discarded.
Caching is a solid, and highly performance-improving technique. However, as with all caches, the code must address the situation where caches go stale, or invalid.

Frequently Asked Questions

Does the ObjectManager 'see' all of objects in the Game World?

No. The ObjectManager 'sees' what the WoWclient does. To minimize load (both client-side and server-side), the WoWserver only makes a limited number of objects available. The distance the ObjectManager can 'see' is measured from the toon. Here are some general guidelines (these numbers are not absolute):
  • WoWUnit can be between 150-350 yards
Both Players and NPCs are of type WoWUnit. Generally speaking, quest pickup and turnin mobs, bosses, and rare mobs can be seen at a greater distance. Very large mobs are usually also visible from a greater distance.
Most WoWGameObjects are not mobile. (I.e., they are at fixed locations in the game world, and cannot move). Immobile game objects are usually visible to a distance of 150 yards or so. "Transport" WoWGameObjects—such as boats, elevators, and zeppelins—will be visible at several thousand yards.

How do I get information about objects that the ObjectManager can't 'see'?

To get information about an NPC, you can use the Query.GetNpcById() method. This consults an internal database maintained by Honorbuddy. The information returned from this query is of type NpcResult. NpcResult contains a substantially reduced subset of information about the WoWUnit that is normally available from the result of an ObjectManager query. The Query class contains a number of other queries that a software developer may find useful.

If information about another player is not available through ObjectManager, then they must be in your Party or Raid. Otherwise, you will not be able to ascertain information about the other players.

The following methods return the group members that are visible to ObjectManager. These methods return a WoWPlayer type which exposes almost everything the WoWclient can know about another toon.
The following methods return the group members regardless of ObjectManager visibility. These methods return a WoWPartyMember type, which is a substantially abbreviated set of the information available from WoWPlayer.
StyxWoW.Me.GroupInfo.PartyMembers property returns info about all Party members.
StyxWoW.Me.GroupInfo.RaidMembers property returns info about all Raid members.

How often is the ObjectManager updated?

The ObjectManager database is synchronized with the WoWclient on each frame. The information is cached until the next frame.
The Honorbuddy API provides an ObjectManager.Update() method to force an update. However, this is intended for internal use, and should never be called by users of the Honorbuddy API.

What's the difference between GetObjectsOfType<T>() and GetObjectsOfTypeFast<T>()?

The two methods under discussion are:
The ObjectManager.GetObjectsOfTypeFast<T>() is nothing more than a convenience wrapper that expands to this:
ObjectManager.GetObjectsOfType<T>(true, true)
This combination of arguments is slightly faster than other combinations of parameters to ObjectManager.GetObjectsOfType<T>(). The downside is the predicate that has to examine the objects will be looking through all objects maintained by the ObjectManager for the classes derived from type T. It will be up to the caller to put additional checks in place to remove unwanted child objects of the wrong type, if needed.

Is there anything wrong with using the ObjectManager to select targets to kill?

The ObjectManager can easily perform this task, but it would be significantly better to use Honorbuddy's Target Filtering API to achieve the goal. Not only is it more effective at the task, but it handles a number of boundary conditions.

I'm getting "InvalidObjectPointerException" despite checking for IsValid

An example:
For some reason I am getting exceptions that say "Cannot read a descriptor on an invalid object" despite the fact I am checking the object is valid. Example:
ObjectManager.GetObjectsOfType<WoWUnit>().Any(u => u.IsValid && u.Attackable && u.Class == WoWClass.Shaman)
The exception:
[14:58:51.565 D] Exception was thrown in BotBase.Root.Tick
[14:58:51.570 D] Styx.InvalidObjectPointerException: Cannot read a descriptor on an invalid object.
    at Styx.WoWInternals.WoWObjects.WoWObject.�[�](UInt32 �)
    at Styx.WoWInternals.WoWObjects.WoWUnit.�()
    at Styx.WoWInternals.WoWObjects.WoWUnit.get_Class()
The problem occurs multiple ticks in a row.
This can only happen if there are threading issues involved. The HB API is NOT thread safe.
If you haven't explicitly created threads, you should be aware that the GUI callback dispatcher is an intrinsic separate thread due to C# architecture. There may be other ways in which you've managed to call the HB API from another thread, say from the Task scheduler. (e.g., Task.Delay).
The object manager is always being pulsed by the main thread, and its sole job is to
  1. Invalidate old instances of objects
  2. Update all objects, including adding new ones
  3. Removing old objects that are still invalid
If you run any code that accesses objects from a different thread in parallel with this, there is (and will always be) a race condition. Similarly if you yourself call ObjectManager.Update() at random points, you can cause this problem to happen in the core of Honorbuddy.
If you have frame lock enabled, you can acquire a frame and Honorbuddy will not allow the object manager to update during that (until it by itself can acquire the frame lock).

  1. Honorbuddy is currently working to enable "frame locking" implicitly. This requires some work internally, and coordination with several of the Community Developers. Once this is done, the need for the AcquireFrame() will no longer be necessary.

Back to Top