Adding items to the PC
In the process of adding items to a Player Character, an Object (like a Language) can be in a number of possible states.
First, it can be "Available". This is possible with things like the list of Bonus Languages, Class Skill Lists, and Class Spell Lists. Things can also be prohibited with items like Prerequisites and Requirements.
Granting
When an object is attached enough to the Player Character in order to change that PC, we refer to that as "granting" the object to the PC. Items like stats (PCStat objects) are universally granted; Skills are granted if they have at least one rank; other objects like Race are granted directly.
Spell Life Cycle
Spells are not granted, they have a different life cycle, which involves known and memorized.
Selections
Certain types of objects may need a choice to be made as they are added to a PC. Abilities, for example, use a CHOOSE: token and MULT:YES to indicate this situation. Templates and Races use CHOOSE: (no MULT: allowed). Equipment Modifiers also can use a different set of CHOOSE: tokens.
Template/Race
When a Template or Race is added to the PC, it is checked to determine if it has a CHOOSE: token. If it does, a chooser is fired, and the user is forced to make a selection.
It is currently not possible to assign the result of a CHOOSE: to a Race or Template when applying the Race or Template in a non-interactive fashion. (Meaning you can't do TEMPLATE:MyTemplate(ChooseResult) and expect it to make the ChooseResult selection from the CHOOSE: token... Templates are NOT Abilities)
Warnings around use
Note that there is some inherent danger in this design: If the system is trying to add a Race or a Template to a PC in a moment when it is not interactive with the user, then the result is not defined, and may result in a crash.
Note also with CHOOSE: on Race that certain information may not yet exist in the PC in order to make certain choices available. This is a race condition we do not intend to fix, as in many cases, the question tryign to be answered cannot be rationally answered at that point in the code.
Storage
When a selection is made, the result is stored as an association to the Race or Template. From a code perspective, it is effectively a Map<Race, Object> or Map<PCTemplate, Object> (although the actual location of the association is in a Facet, e.g. RaceSelectionFacet).
EquipmentModifiers
When an EquipmentModifier is interactively added to a piece of Equipment (in the Equipment Builder), the user is forced to make a choice, if necessary.
When an EquipmentModifier is added through the EQMOD tokens on Equipment, then the result of the selection is provided in brackets in the token, e.g. "MyEqMod[MySelection]".
Storage
Since Equipment is still cloned between instances, EquipmentModifiers are stored on the Equipment. Technically, they are actually stored on the EquipmentHead objects, since the modifiers like +1 (or whatever) are technically attached to the Head, and not to the entire piece of Equipment.
Ability Selections and Ability Life Cycle
We start with an "Ability". An "ability" is a named entry in an Ability file.
If the Ability has a chooser within it, the product of executing the chooser on the Ability is an "AbilitySelection", which contains the result of the choice. If it doesn't, there is still one default "AbilitySelection", with a null choice. There are both UI and LST data file methods of getting to an AbilitySelection - these are described below.
Once a Category and Nature are added, we successfully have a CategorizedNaturedAbilitySelection or CNAS. That can be applied to a PC.
An Example on Terminology
Let's assume there are 2 TYPE=DragonForm abilities:
BasicDragonForm <> TYPE:DragonForm <> MULT:NO AdvancedDragonForm <> TYPE:DragonForm <> MULT:YES <> CHOOSE:PCSTAT|ALL
There are 2 abilities here:
BasicDragonForm AdvancedDragonForm
There are 7 AbilitySelections here:
BasicDragonForm() AdvancedDragonForm(STR) AdvancedDragonForm(INT) AdvancedDragonForm(WIS) AdvancedDragonForm(CON) AdvancedDragonForm(DEX) AdvancedDragonForm(CHA)
Note I'm writing even MULT:NO stuff with a trailing set of () to try to distinguish the AbilitySelection from the Ability. The user never sees that thought process (it's internal to the code)
There are many combinations of CNAS that are possible; they are not all articulated here. (There are, at a minimum, 21 possible combinations. 3 natures for each of the 7 AbilitySelection items listed above assuming there are no child Category objects for the Ability... 7 more for each child category where these items could be selected [child categories only use NORMAL nature].)
Granting an Ability to a PC
Abilities are not directly granted due to their structure. In order for an Ability (e.g. Feat) to be applied to a PC, it requires:
- A Category (either the parent Category or a child Category - in this case to indicate which "pool" is charged for taking the Ability)
- A Nature (Virtual, Automatic, Normal)
- The Ability itself
- Any Selection on the Ability. The Selection is the result of a CHOOSE token being present on the Ability. If no CHOOSE is present, the the selection is null, but still required to be present in order for an Ability to be applied to the PC.
As much as this might make the data team's head hurt, at the end of the day a character can never have an Ability applied to the character. It's always a CNAS. It's just a matter of how you get the full CNAS.
Usually it starts with an AbilitySelection. When combined with a specific Category and Nature, the AbilitySelection can be granted to a PC. (a CNAS can be granted)
From the UI
Even when you select BasicDragonForm from an Ability Pool or the Feat screen or whatever, the system internally evaluates it's MULT:NO and expands it to BasicDragonForm() to get the AbilitySelection. When you select AdvancedDragonForm from a pool, the system evaluates that it's MULT:YES, determines there are multiple possible answers, and makes you pick before it has the AdvancedDragonForm(target stat).
Either way, you have generated an AbilitySelection. The screen/pool you are in defines the Category, and the nature is inherently NORMAL for items selected in the UI - so the system can generate the full CNAS to be applied to the PC. It's just that most of the work is transparent to the data team.
In LST Data
CHOOSE:ABILITY returns an Ability:
BasicDragonForm AdvancedDragonForm
So at any point, I can select an Ability with that... but the only thing I can GRANT to a character (what ABILITY: does) is an AbilitySelection.
Form of the Dragon II
CHOOSE:ABILITYSELECTION|Special Ability|TYPE=DragonForm ABILITY:Special Ability|AUTOMATIC|%CHOICE
This needs to be CHOOSE:ABILITYSELECTION and not CHOOSE:ABILITY because CHOOSE:ABILITY selects an ABILITY and ABILITY: applies a CNAS (so the %LIST must provide an ABILITYSELECTION).
An Example showing the subtlety
To understand the distinction, consider a Feat like Martial Weapon Proficiency.
The Ability is Martial Weapon Proficiency. This appeared in the LST file as an Ability. Applying the first to a character is not meaningful (and reintroduces bugs we had with Martial Weapon Proficiency in the past that we fixed by introducing CHOOSE:FEATSELECTION).
An AbilitySelection is Martial Weapon Proficiency (Longsword). This was the result of the CHOOSE:ABILITYSELECTION and is the piece of information transferred from the CHOOSE to the ABILITY token Applying the second might be possible, but would not know what pool to deduct the application from or where it should be displayed in the UI (or in what color)
A CNAS is FEAT::NORMAL::Martial Weapon Proficiency (Longsword). This was the result of combining the AbilitySelection with the other information provided in the ABILITY: token. The last one (which is effectively generated from all the information provided to the ABILITY token) is grantable (because that's what the ABILITY token does).
A Countercase to using CHOOSE:ABILITY
From an older conversation on pcgen_experimental:
The concern: If none of the abilities-of-interest have choosers within them (so they are like your BasicDragonForm example - in fact, they are BlackDragonForm, BlueDragonForm etc.), it's counterintuitive that I can't grant the ability from the CHOOSE:ABILITY result, as it seems a small leap to the non-coder from the ability BlackDragonForm to its abilityselection BlackDragonForm()!
Let's walk through the scenarios:
CHOOSE:ABILITY|Special Ability|TYPE=DragonForm
Let's assume you start with all of the TYPE=DragonForm items being MULT:NO, so you can infer the AbilitySelection from the Ability. One could assert:
ABILITY|Special Ability|AUTOMATIC|%LIST should work.
Now someone comes along and adds a MULT:YES item. What do we do?
- Error on data load
- Don't let the CHOOSE:ABILITY present it
- Let the user select it, but it never gets applied to the PC.
In case #1, the code needs to recognize that a catcher (%LIST) is asking for an AbilitySelection, and a thrower (CHOOSE) is producing an Ability. Then it needs to check for auto conversion. At each load, all items of that TYPE (or listed individually) need to be checked to ensure they are MULT:NO. So we've spent a bunch of time iterating across the data.
Also, we can produce some nasty cases here. Imagine that a data monkey puts in "hollow" Abilities with only a DESC (which will happily take either Ability or AbilitySelection) to produce output in order to test their data. It has MULT:YES. The system will work with CHOOSE:ABILITY. They then go back and put in an ABILITY token to "do it for real" and the data breaks. Well, "obviously" that's a code problem (when in fact, they have changed what they are targeting).
So this scenario is (a) costly to enforce (b) fragile [meaning it produces non-intuitive errors and small extensions (creating a new Ability) will cause errors to be reported on unrelated objects]
Case #2 suffers from similar issues except that the checks are performed at runtime. If I ask for TYPE=DragonForm in CHOOSE:ABILITY, I expect it to be TYPE=DragonForm not "TYPE=DragonForm if and only if it's MULT:NO". The latter to me is what I would refer to as "magic" and something I generally try to avoid.
Case #3 also seems to suffer some nasty side effects. "I selected BasicDragonForm and it applies, but AdvancedDragonForm won't apply (and by the way, as part of your "code bug" it didn't provide me the ability to select my stat). I really don't want these "support calls" to the code team to fix things that aren't broken.
So we are strict. When a CHOOSE produces an Ability, you can do something with an Ability, when a CHOOSE produces an AbilitySelection, you can do something with the AbilitySelection. We are getting to the point where we actually produce errors about that today if you try to target the wrong type. (Go try CHOOSE:STAT|ALL and FAVOREDCLASS:%LIST in a Template and see what happens... 6.0.0 will silently consume that data. 6.1.x-dev, not so much :) )
Another example using CHOOSE
Reviewing:
ABILITY:FEAT|AUTOMATIC|%LIST wants to "catch" an AbilitySelection.
Using the model where parens indiate an AbilitySelection, this is attempting to catch something of the form:
???(?!?)
Note the ?!? may be null in the case of MULT:NO, but there may be 2 unknowns.
This case:
ABILITY:FEAT|AUTOMATIC|Weapon Proficiency (%LIST)
This wants to "catch"... well, we don't necessarily know without looking. What we do know know is the Ability, it's called Weapon Proficiency. So our AbilitySelection is effectively "Weapon Proficiency (?!?)"
What we need to fill in is a single unknown, the Selection.
CHOOSE:WEAPONPROFICIENCY chooses a Weapon Proficiency. So we are "throwing" a Weapon Proficiency... but are we catching the right thing?
What the code has to do is go peel back at the Feat Weapon Proficiency.
Weapon Proficiency <> MULT:YES <> CHOOSE:???|...
??? had *better* be WEAPONPROFICIENCY, so that the "catcher" is catching the appropriate type. If not, an error should be thrown at data load.
If the CHOOSE *does* match up, then effectively the ABILITY token in this case catches the Selection, plugs that into the Weapon Proficiency to produce Weapon Proficiency(known selection) [aka an AbilitySelection] and can grant that to the PC.
So another way of looking at CHOOSE:ABILITY is that is answers one unknown: The Ability. The CHOOSE:ABILITYSELECTION is actually a hybrid CHOOSE that (somewhere under the covers) answers two questions at once, effectively giving you the Ability and the Selection in one choice.
Pools
In some cases, numerical items are placed into pools. Skills, and a few other items work this way. This can be rather challenging when items are subsequently removed from a PC, since we don't necessarily associate which point added to a pool resulted in which item selected by the user. We want to improve this awareness (and need to do so in order to resolve several bugs).
Longer term, this is going to be a problem, as we will want to store those details, and the result is that the contents of the current PCG file format will be insufficient to rebuild a PC to the full internal state. This transition will need to be managed carefully.
Old Storage
Traditionally, the metadata and implemenation of objects was mixed. (Meaning it was very difficult to distinguish what a Race meant in the rules and what characteristics of that Race pertained to the actual active PlayerCharacter. This behavior was facilitated by cloning of all of the objects before they were added to the PC.
This resulted in a number of issues, including that the clone was not always complete, the clone was not always sufficiently deep to separate PCs from each other (so they didn't collide in updating internal objects), extra memory use and understanding of what was going on for new developers.
A significant portion of the late 5.x series of releases separated the two sets of behaviors within PCGen. However, Equipment still retains the full clone design due to the complexity of Equipment. This means the Equipment object still has a significant number of internal methods and fields that contain the information about the equipment (both from the rules and reslated to the current PC).
Equipment and Heads
It is noted that one of the items that makes Equipment particularly difficult to deal with is the concept of "Primary" and "Secondary" Heads on Equipment. We have instantiated these as separate objects (EquipmentHead) so that the items that are duplicated can be more easily managed, and to prepare for future expansion.
This internal hierarchy has multiple sets of issues associated with it, including:
- MSRD and some other sets have a desire to have more than one additional component (so an arbitrary number of heads)
- This is a more difficult design to avoid cloning, since the depth of association required is much higher
Current Storage
For most other objects, the rules information has been separated from information related to any given PlayerCharacter.
The dynamic information related to a specific PlayerCharacter is now stored in a series of Facet objects (pcgen.cdom.facet).
Facets
Simply stated, we called these "facets" because we started to run out of other words in the English language for "part" or "component" or "aspect" (since those words are all "loaded terms" when thinking about the rules we handle in PCGen).
Facets are constructed by Spring, so that they are properly initialized. Currently we do that through XML. This is currently the only real "static" part of PCGen, thus the only part where we are using Spring to load classes.
Each Facet is designed to be a rather simple service for a PlayerCharacter or for a loaded DataSet.
IDs
If storage is provided as part of that services, we store things based on a CharID (character ID), so that we are not using the very heavy-weight PlayerCharacter object as the key. (The goal was to transition to a very small primary key, while slowly chipping away at the amount of "stuff" in "PlayerCharacter"). Each Facet should provide one service (storing the Race, storing Skill Ranks, etc.)
Each CharID is aware of it's "parent" DataSetID, so that if Data Set/Game Mode information is required from such a Facet, it can be easily accessed if the CharID is known.
DataSet Facets
A number of facets store information related to a particular Data Set - more like storing information about the Game Mode than about an individual PC. When we encounter these types of information, we don't store it by the PC, we store it by the DataSet. These all have a registration occur in their init() method that registers them with the DataSetInitializationFacet for a later callback when a Data Set is fully loaded. This callback is one of the last steps in the Load Process.
Copying
In addition to storage, each Facet is responsible for defining how it needs to copy information from one PlayerCharacter to another. If, for example, it uses an internal structure for storage of complex sets of information, it will need to have a method that performs a proper deep-copy if a PC is cloned. (Unfortunately, there is still something in the old-style output that requires a clone, so unfortunately, we have to be prepared to clone PCs).
Filters
Some facets only provide a filter service, meaning they are reducing items passed to a later Facet.
Facet Chain
In many cases, the Facets provide a chain of services, each providing one simple service with others providing later processing, until we settle upon the information that is actually attached to the PC. For the primary object types (Race, Templates, etc.), the items attached the PC settle into Facets within the "pcgen.cdom.facet.model" package.
The reason there is a neeed Facet Chain is because of a number of characteristics of our current data. These include (but are certainly not limited to):
- The possibility for Languages to be granted MANY different ways and wanting to ensure that they can only be removed from the PC the same way they were granted to the PC
- Need to process MULT:NO Abilities to avoid granting them from multiple locations
- Need to process STACK:NO Abilities to avoid granting the same choice from multiple locations
- Need to Handle multiple methods of spells being made avaialble to a PC.
In general, this Facet chain is set up either in dependencies in the XML file defining the Spring configuration or in FacetInitialization.
Bridge Facets
Note that in some cases there are facets that perform certain services (and that link other facets in their init(), which are not otherwise called (meaning they would never activate if no one ever constructs them). We call these "Bridge Facets" and explicitly contruct them once in the doBridges() method of FacetInitialization.
Priority
Note that there are a few situations where Facets need to learn of changes in a certain order, so a number of the Facets support a priority associated with a listener.
Equality
Note that many of the objects in PCGen have strange equality defined, and unfortunately, removing that equality causes bugs (so *something* is still depending on some best-practice violating equality methods). To avoid false positives, many of the Facets use instance equality for comparison. Some MUST do so in order to be able to properly process Equipment, MULT:YES items, etc.
Trailing PRExxx
Current/older granting tokens allow a trailing PRExxx token to appear on the grant. This produces a number of challenges, including that any change to the PC results in a need to re-evaluate every trailing PRExxx to determine if it changed. These items typically have an update(CharID) method, and you can see them called in the setDirty method of PlayerCharacter. It is not the intent to support this type of behavior going forward, due to the significant difficulty and time required to process this form of logic. (As clarity for those not involved in many of the parts of the formula rebuild: Yes, I get some things are conditional. Yes, something needs to be able to conditionally grant. No, you don't need a trailing PRExxx to accomplish those ends. The full explanation of this is handled elsewhere).
New Storage/Granting
The Facet system in many cases is providing a simple item or list storage mechanism. The new formula system inherently provides such storage as well, and can do so without separate classes required for each piece of storage. Therefore, the strategic design is moving away from Facets and into storing items in the variable system.
The system will effectively be able to grant certain variables. This will start with Alignment. For the moment, there will be a series of Facets that act as a transition, allowing a pull of certain variables from the new formula system and granting the contents of those variables to the PC.