The implementation of items in the Transcendence engine is one of the more complicated architectural designs. This article describes the current design and proposes a path for simplifying and improving the architecture.

Core Concepts

Each <ItemType> definition in the XML generates an instance of the CItemType class in the game. At load time we create one CItemType object instance for every <ItemType> definition. The set of active CItemType object is kept in the current design collection along with all other design types.

A CItemType object keeps only generic item data: the item's name, mass, price, etc. Armor item types have an additional pointer to a CArmorClass object, while device item types have a pointer to a CDeviceClass object.

When a ship loots a barrel of Centauri rice, we deal with CItem objects. A CItem represents an instance of an item in the game world. A CItem object has a pointer to its type (a CItemType) and a count (for stacking multiple objects).

Each object (ship or station) with a barrel of Centauri rice has a CItem object to represent it, but all those items point to a single CItemType object describing a Platonic barrel of Centauri rice.

Armor segments and devices are the same: when a ship loots an armor segment, we add a CItem object representing that segment to the ship. The CItem object tracks that specific armor segment. If the segment is damaged, we store that in CItem; if the segment is enhanced, we store it there too.

When an armor segment is installed on a ship, we create a CInstalledArmor object on the ship and point it to the CItem that represents the segment. The CInstalledArmor object tracks data specific to the installation, such as where it is installed and how many hit points it has left. We also track the list of enhancements gained from other devices on the ship.

In a similar way, when a device is installed, we create a CInstalledDevice object on the ship and point it to the CItem that represents the device.

Architecture

As you can see, there are several objects that are required to deal with items in the game. We need all these different objects because there are different kinds of items (armor, devices, etc.) and because installed items require additional data. But this separation of objects leads to complexity.

For example, let's imagine we're trying to figure out the maximum number of hit points of an armor segment. Which object should we ask? At first we might expect CItem to have a GetHitPoints() method, but CItem only deals with generic item data. Data specific to armor is in CArmorClass.

Fortunately, the CItem object has a pointer to its CItemType and that object has a pointer to its CArmorClass object. All we need to do it follow the pointers and then ask CArmorClass for the maximum hit points.

But what if the item is enhanced? The enhancement info is contained in CItem, not in CArmorClass. What if hit points depend on item charges? Charges are also contained in CItem. It's clear that we need to pass CItem into CArmorClass when computing maximum hit points.

And that's not all. What if the armor segment is installed on a ship? What if the shield generator adds hit points to the armor? Or what if the armor segment is carbide carapace? How do we know if the ship has a complete set? Clearly we need to pass in both the CInstalledArmor object and the ship object (CSpaceObject).

We need something like this:

int CArmorClass::GetMaxHP (
         CSpaceObject *pSource, 
         CInstalledArmor *pInstalled,
         const CItem &theItem
         )
   {
   ...
   }

CItemCtx

In fact, most CArmorClass methods will need all three of those objects to process. Once I realized that, I decided to create a wrapper object to make passing those arguments easier. Thus CItemCtx was born:

CItemCtx is a small object that contains the above three objects. You can construct a CItemCtx object out of whatever pieces you have and then pass it around:

int CArmorClass::GetMaxHP (CItemCtx &ItemCtx)
   {
   int iMaxHP = m_iMaxHP;

   if (ItemCtx.GetItem().IsEnhanced())
      iMaxHP += CalcHPBonus(ItemCtx);

   if (ItemCtx.IsInstalled())
      iMaxHP += CalcShipBonus(ItemCtx);

   if (gain HP with charges)
      iMaxHP += ItemCtx.GetItem().GetCharges();

   if (has completion bonus)
      iMaxHP += ItemCtx.GetSource().HasCompleteSet();

   ...
   }

The advantage of this system is that you can construct a CItemCtx object with whatever you have. If you have a loose armor segment, you just pass in a CItem object and nothing else. If you have an installed armor segment, you can include all three arguments.

The actual function (e.g., GetMaxHP) is responsible for getting the data from CItemCtx and dealing with missing data. If there is no CInstalledArmor object, then we assume the segment is not installed.

Unfortunately, this system also has a couple of significant drawbacks:

First, you cannot reach installation data (CInstalledArmor object) from a CItem object. This is why there are two ways to get item properties in TLisp: itmGetProperty and objGetItemProperty. The second function is needed to pass in the ship object and thus compute the correct CInstalledArmor object.

The second drawback is that passing CItemCtx can be error prone if you pass it to the wrong object. For example, what if you have a CItemCtx from a laser cannon and pass it into CArmorClass::GetMaxHP. Or what if you pass CItemCtx from reactive armor and pass it to CArmorClass::GetMaxHP for carbide carapace? The C++ type-checking system won't catch this, so everything will seem to work, but the game may fail at some point.

New Proposal

Starting in version 1.9, I've refined the system to make it more streamlined and less error prone:

  1. We add an optional pointer from CItem to CInstalledArmor or CInstalledDevice.
  2. We add a pointer from CInstalledArmor to CSpaceObject (this ship). And we add a similar pointer in CInstalledDevice.
  3. We introduce a new class, CArmorItem, which is responsible for all armor-specific code (rather than spreading it around between CItem, CArmorClass, and CInstalledArmor.

The additional points make it possible to navigate from a CItem to all necessary data, and the CArmorItem object lets us deal with armor items regardless of whether they are installed or not.

When this proposal is implemented fully, there will be no need for CItemCtx.

Staging

Implementing this proposal will take a lot of time because there is a lot of code that uses CItemCtx. We'll proceed as follows:

  1. Convert methods in CArmorClass that take CItemCtx and convert them to take CArmorItem.
  2. Add a wrapper method in CArmorItem to call the method in CArmorClass passing itself as a parameter.
  3. Convert all callers to call CArmorItem instead of CArmorClass.
  4. Do the same for CDeviceClass.