Architecture Update 1Q2013
On content, perspectives, scope, and the facets of the PCGen Core
Contents
Purpose of the Document
Over the course of some time addressing code the PCGen core, I have come to a set of ideas around organization of information within PCGen, in order to help explain what is happening and to help information flow into and out of the program. It is my belief that formalizing these mental models will help address a few issues: (a) Helping folks understand the organization and design decisions in the code by having consistent use of terms and regular patterns of behavior (b) Helping in communication by having consistent language (both in the code team as well as with the data team) (c) Helping with token design (d) Ensuring that PCGen subsystems interact and are asking questions of the core in the appropriate form. You can see an example where this is violated on a recent thread in ListFileHelp (where a user has multiple spell lists on a class and the data is overly reduced coming out of the core and then there is an attempt to re-add context which results in an accidental leak of a spell into a level where the PC cannot cast spells)
Comments on this draft
This is currently an ugly mixture of concepts and some token syntax. I understand token syntax can get into some weird discussions at times, so this is not an attempt to formalize proposals on syntax. It could be used that way, but that is not the primary intent. I do not want to receive comments on the syntax as this is not a token proposal. That can be left for the appropriate time and on the appropriate forum (pcgen_experimental). The syntax is introduced to show parallels in thought processes, as that is a shortcut for a significant amount of prose. Whether the document deserves to stay as a mixture or whether it should attempt to stay conceptual is open for discussion.
Background
Some time ago, we took on a project of updating the PCGen core to become object oriented, type safe, solve items in ways that were faster than significant cloning, and improve our ability to test the code by having smaller blocks of code and more ability to mock up tests.
The first set of effort was focused around the tokens. Devon had started this work, but additional effort was placed into the tokens to ensure they can each unparse their content (and are tested for round-robin behavior). This had the side effect of making the tokens (for the most part) editor friendly, and also provided a large amount of error checking and enforcement as data was loaded into PCGen. This provided a significant help to later steps in the process. Three major areas of tokens have not been addressed, however. This includes BONUS (and now TEMPBONUS), the Prerequisite tokens, and Game Mode tokens. Up to this point, none of those subsystems has received major changes (the only material exception being a BonusManager rewrite to avoid an infinite loop issue).
After the basic token work was complete, work moved to the actual core (in things like PObject and PlayerCharacter). The thought process was built around using a graph to store the contents of the Player Character. Since that was such a radical shift from the current behavior, a smaller step was undertaken to split up the various storage of the Player Character in to smaller units. This led to our use of Spring, and the facets we have today.
The facet infrastructure generally operates in a listener/event model. When an item is added to a Player Character, the facet to which it is added will often produce an event indicating the object was added. These events flow to other facets which may repeat the process. In that way, there are single sources for all Templates on a character, as well as for all CDOMObjects on a character. When possible, items are pre-processed, so that the list of Spell Resistance objects on a Player Character are well known without having to increment through all of the objects (The only remaining systems that do the full increment are spells and bonuses).
The number of facets has grown to over 200 (though many of those are just instantiations of Generic classes). In that sense, some organization is in order, and such a discussion already took place on the PCGen developers list in 2012. This is an attempt to update that discussion as well as expand it to related topic that may benefit from the same thought process.
Introducing Perspective and Scope
In effect, the facets produce a set of information in various locations, and we can query that information. This is not unlike a database, although stored in a very different way. We might ask what Classes a PC has or Templates, or Race, or number of Hands. Each of these answers can have a different form (single object, list, number). In total, the entire set of items about the character come from these different areas for evaluation. For purposes of this document, we will call each of those areas a "Perspective". So looking at what Templates a PC has is evaluating the Template Perspective. Same for all of the other questions, from Race, to number of spells that can be cast, etc.
Also in the course of building the facets, it has highlighted that certain characteristics of a Player Character only make sense when asked in a certain form. Let me give an example:
Some perspectives would only make sense in a global sense, such as: What Templates does the Player Character have? A question such as: "What Templates does the Player Character have for the Fighter Class?" is rather meaningless, so "Templates" might be called part of the "Global" view of a Player Character. Or thought of another way, the Template Perspective is global. In order to refer to it precisely, I will refer to this view or question context as the "scope". So the Template Perspective has a Global Scope.
Some perspectives make sense in a more local scope. While "Is the Player Character a Spellcaster?" is a very valid question, we can also ask questions like "Is the Class 'Sorcerer' a Spellcaster?". So the Spellcaster Perspective would have a scope of "PCClass". We can then view the global question as simply a combination of all of the local questions.
The local scope is not limited to only one qualifier. Some questions like "How many spells can I cast?" actually require two identifiers in the scope. In this case, first is the PCClass, the second is the spell level. Only with both pieces of information is the question meaningful: "How many 5th level spells can my Sorcerer cast?" That way, there is a single (integer) answer.
When looked at from the perspective of how the facets function, the add function immediately exposes whether the data in question is part of a specific context. An AbstractSourcedListFacet does: add(CharID, T, Object) where T is the item in question and Object is the source. So this is a global scope. An AbstractScopedListFacet does: add(CharID, S, T, Object) where S is the scope, T is the item in question and Object is the source. So this is a localized scope. An AbstractSubScopedListFacet does: add(CharID, S1, S2, T, Object) where S1 and S2 are the scope, T is the item in question and Object is the source. So this is a localized scope.
In that sense, we don't necessarily need to use the scope (or lack thereof) as a package organization. It is already well handled by the class hierarchy.
Core View
Recently, I created a branch called "coreview" in an attempt to provide some better visibility into what is happening in the core. The theory here is to help with debugging since there is a significant amount of shared code in the core due to the user of generics (so simple breakpoints or print statements end up with a significant number of false positives). Since there are patterns of behavior in the facets of the core, this code can attach to a set of facets that fit into those patterns in order to expose the facet content (so the flow of "granting" of objects could be viewed while PCGen is running. In effect, this is displaying a given Perspective of the core, and I would expect that for a Perspective that is not in the Global scope, that selections to indicate the scope to be displayed would be part of the interface.
PCGen Mental Models
I'm going to expand upon the concept of Perspective and Scope to indicate that I think some other subsystems of PCGen can benefit from these mental models. So given the interaction across subsystems and the coreview, I think these become valuable concepts for folks to widely grasp, even outside of the core. This also provides a potential framework for token design, although that tends to become a hotly debated topic at times.
Prerequisites
Let me introduce a syntax for this to reduce the amount of text to discuss the perspective and scope. I'm going to introduce a syntax called HAS: and one called EQUALS: These have the form: HAS:PERSPECTIVE|[SCOPE|]Target|Target,Target EQUALS:PERSPECTIVE|[SCOPE|]Target
So: EQUALS:SPELLCASTER|Wizard|True is asking "Does the Spellcaster Perspective for the Wizard equal True?" EQUALS:HANDS|3 is asking "Does the Hands Perspective (in Global Scope) equal 3?" HAS:TEMPLATE|Foo,Bar is asking "Does the Character have the Template Foo and Bar?" (using comma to denote AND and pipe to denote OR just as we do in CHOOSE today)
In the case of asking a global question about a Scoped Perspective (such as Spellcaster), we can simply substitute ANY: EQUALS:SPELLCASTER|ANY|True
You may immediately note the potential parallels to the Prerequisite system. While certainly not complete (this doesn't address how PREMULT would work, for example, nor how you'd efficiently match 2 of 3 items), it at least provides the framework for asking whether the Perspective and Scope concepts should be adopted as PRExxx is changed to PRE/REQ.
Bonuses
Bonuses actually have a similar situation. In this case, they are all (*well, ALMOST all), modifying an integer Perspective. However, they are certainly not all Global in scope. Let me introduce a syntax of MODIFIER: MODIFIER:PERSPECTIVE|[SCOPE|]value
Some BONUSes are Global: BONUS:MISC|SR|x might be: MODIFIER:SR|x
Some have scope: BONUS:CHECKS|Will|x might be: MODIFIER:CHECKS|Will|x (uhh, not much difference there)
Others have multiple sets of scope: BONUS:SPELLKNOWN|CLASS.Sorcerer;LEVEL.4|x might be: MODIFIER:SPELLKNOWN|Sorcerer|4|v
I find the latter much more clear and less subject to typing issues. The Bonus Subsystem Thoughts page shows a quick scratch-pad level categorization of the existing BONUS tokens by their Scope.
For a BONUS, the "Perspective" can be thought of as the answer it is trying to achieve. For example, "Spell Resistance" (or "SR" as folks would see it in the data), would be a Bonus Perspective. That perspective happens to be in the Global Scope.
Some BONUSes initially appear rather complicated for fitting into this thought process.
BONUS:DC is probably a good example of something that looks challenging.
- DC|ALLSPELLS
- DC|SPELL.x [Spell]
- DC|DOMAIN.x [Domain or perhaps Domain List - need to check]
- DC|CLASS.x [Class or perhaps Class List - need to check]
- DC|TYPE.x [Class Type, I believe]
- DC|SCHOOL.x [School]
- DC|SUBSCHOOL.x [SubSchool]
- DC|DESCRIPTOR.x [Descriptor]
- DC|x [Class]
I've asserted that is within 2 scopes: PCClass and Spell. Given the number of possible second entries (Info) in a BONUS:DC, that may be tricky to believe. However, rather than thinking only in native objects or in TYPE, the thought process needs to cover ALL Primitives (look in plugin.primitive.spell). If we back up to that point of view we can map those items to 2 scopes:
- DC|ALLSPELLS --> DC|ALL|ALL
- DC|SPELL.x --> DC|ALL|x
- DC|DOMAIN.x --> DC|ALL|DOMAINLIST=x
- DC|CLASS.x --> (this is potentially ambiguous, could mean either DC|x|ALL or DC|ALL|CLASSLIST=x)
- DC|TYPE.x --> DC|TYPE.x|ALL
- DC|SCHOOL.x --> DC|ALL|SCHOOL=x
- DC|SUBSCHOOL.x --> DC|ALL|SUBSCHOOL=x
- DC|DESCRIPTOR.x --> DC|ALL|DESCRIPTOR=x
- DC|x --> DC|x|ALL