Calculating Items on the PC
Items (Abilities, Skills, Stats, etc.) are added to a PC with certain behaviors, and we need to analyze those behaviors appropriately in order to produce a character sheet. Many of those added behaviors take the form of a variable or a BONUS. In those subsystems, we have a significant loop of behavior that we need to address. At times, I refer to this as the Bonus/Variable/Prerequisite system, since infusion of any ONE of those subsystems into a strategic design immediately pulls ALL of them into the design. This is because they are co-dependent.
The BONUS system is used to control the modification of variables. Also, a BONUS tag can have a trailing prerequisite. Therefore, Variables are intertwined with BONUSes (the BONUS system actually serves as our variable solver), and can be dependent on Prerequisites. Of course, with PREVAR, a prerequisite can contain a variable, so it creates a huge problem of "how do I actually produce a stable answer" given that it could be an infinite loop of behavior.
We also have to recognize that the current design of certain tokens also pulls them into this loop. Tokens like ABILITY that allow a trailing PRExxx token also join into this loop of behavior, since an added object could easily add a new variable or new BONUS. This is not a theoretical attack - extremely large portions of the data now use trailing PRExxx on items attached via the stats (the "Default" object and the "Internal" Ability Category), which means this is a huge performance issue for PCGen... one that code cannot resolve... we must provide (and force) a design that does not depend on this looping analysis.
These are not the only issues (more are described below), but suffice it to say the current system is too intertwined to reasonably unwind. After much pain in attempting to do so (we probably lost at least a year or two trying over and over to find ways to do this "smoothly"), we have made the decision to do a clean break to a new variable system.
Also, as a specific note, it is no accident that the newer tokens and new variable system has no tokens that support trailing PRExxx, and no PRExxx that can pull from the new variable system. Introducing either one of those would pull the entire new variable system into the existing "solve loop" - basically dragging the system down further.
This is not to say the new system is entirely immune to this. There are designs that could attempt to use the older structures to "fool" the new variable system into acting as if it had trailing PRExxx items. I am deeply concerned that the older mental models where data depends heavily on trailing PRExxx (and constant requests from lots of folks about "when am I getting a trailing PRExxx for MODIFY?" - answer: NEVER) means all of the conversions will need very careful data review to ensure they are operating in a different mental model than the traditional data design.
The BONUS Subsystem
The Bonus subsystem calculates numeric values primarily provided as inputs from the LST data. These values can be integer or fractional values. The values can be fixed (such as "3") or variable (such as "INT" or "CL=Fighter"). For the developers, these are stored as a pcgen.base.Formula object. For the data team, they will recognize the BONUS: LST token as providing inputs to the Bonus Subsystem.
To perform these calculations, the BONUS system leans heavily on the variable system, as supported by JEP. In fact, the BONUS system is actually serving as the variable solver at this point, because variables are modified by a BONUS:VAR and the data standard is to set all variables to zero and always use a BONUS:VAR to provide the desired value.
Strategic Viewpoint
With the exception of BONUS:VAR (which impacts variables), the rest of the BONUS values feed into calculations that are done within PCGen. Since we are hoping to move away from a system where those calculations are embedded in code (to one where the data is more self sufficient), there is not really a long term replacement for BONUS. Strategically, the system goes away.
To be precise, the token itself will disappear, and the function it currently provides will be almost entirely consumed by the new formula system. This is not to say there will not be some level of code support for "bonuses". It is possible, if not likely, that some specific code support to handle the overlap and combination rules of the BONUSTYPE characteristics will be extremely helpful. The result will be a controlled quantity and complexity of the variables in the new variable system used for those items where it really is a "BONUS" (usually in a skill rank or other checked roll sense).
Current Issues
The current system has a number of issues:
- BONUS: tokens are not validated at input.
- Bonus calculation is opaque - it can be hard to tell why a value ended up a certain way.
- The Bonus system cannot always handle long dependency chains
- The Bonus system has problems with order of operations
- %LIST and LIST (these pull the value from CHOOSE) are both passive, in that they are resolved at runtime and treated as a text string. So the parsing that should be done at LST load is instead done at runtime.
Design
BONUS processing is performed by our BONUS management system (specifically pcgen.core.BonusManager)
Major characteristics of this system:
- BONUSes have specific values calculated by the formula system
- BONUSes have certain stacking rules based on their type and other flags (.STACK)
- BONUSes allow override of values (.REPLACE)
- BONUSes are used to modify variables (BONUS:VAR|...)
- BONUSes can be conditional (and the condition cane be a variable or other item), making BONUS updates highly self-dependent [this is currently done in a loop to ensure BONUS values stabilize]
- The system does not manage loops/conflicts well, in that lack of stabilization has to be terminated based on a number of tries.
Structure of a Bonus
BonusManager stores the bonus values in a map, with a key that is carefully constructed based on how the Bonus was defined in the LST data. For example, BONUS:MISC|SR|formula|TYPE=Defensive.STACK is stored as: "MISC.SR:Defensive.STACK" and maps to the resolved value of "formula".
Consistent with many of the names in the code, we will call:
- MISC the "Bonus Name"
- SR the "Bonus Info"
- Defensive the "Bonus Type"
- STACK as the "Stack Type"
Calculation
As any individual Bonus is calculated, there are a number of steps that are performed:
- Ensure all dependencies are pre-calculated
- Calculate non-modified, STACK and REPLACE Stack Types individually
- Appropriately combine the StackTypes for each BONUSTYPE. This combination is dependent upon whether the particular Bonus (a) stacks in certain ways (b) allows fractions
- Combine all of the values for each BonusType for a given Bonus Name and Bonus Info.
To resolve BONUS values, the BonusManager runs through all of the necessary calculations for the BONUS. This obviously draws upon the existing JEP formula system. Due to the loop of dependencies between Bonus/Variable/Prerequisite, a settling system is used. All the BONUSes are calculated; then they are all calculated again. If it changed, they are calculated a third time, etc. This is repeated up to a mathematical limit (25 or something). Note that this does not guarantee a solution and it is a bit of a mess in terms of performance. A very large portion of our CPU time is spent in this loop.
This resolution is compounded by a number of additional challenges. A major order of operations issue exists here as well. There are some built in "terms" (see below) that depend internally on BONUS values, so there is yet another loop from BONUS objects back to the variable system, but one that is not always obvious. As a result of some of these feedback loops, There are certain BONUS values that must be calculated first, so that later calculations are correct. Failure to do this either guarantees a bad value or guarantees an infinite loop. Currently BonusManager handles this, but it is code developed empirically from experience in bad data results - it is not a result of forward-thinking design.
Further Reading
- For more information on how formulas are resolved, see Formula Systems
- For more information on how Prerequisites work, see Prerequisites and Requirements