Difference between revisions of "Referring to Groups in LST Data"
Tom Parker (talk | contribs) (Created page with "{| align="right" | __TOC__ |} At some point, we get to where the data needs to provide for groupings of objects. The primary challenge with this is determining how to pu...") |
(No difference)
|
Latest revision as of 04:13, 27 February 2018
At some point, we get to where the data needs to provide for groupings of objects. The primary challenge with this is determining how to pull together certain items into the bucket of available choices. Today this is linked to many tokens, but the most complex is the CHOOSE token.
A new Grouping system also exists to do what is internally will be called "Grouping". This is not present as of Feb 26, 2018. The MODIFYOTHER Token will use this new system prior to the release of PCGen 6.8.
Most of the code today has been standardized to the plugin system in the form of "Primitives" and "Qualifiers" (see details below).
The original system within PCGen had hardcoded choosers triggered off CHOOSE and the other tokens that would look through relevant data and find the appropriate objects. One major item from this heritage still exists, although it is more modular than its origin incarnation. This is in pcgen.cdom.base.SpellLevelChooseInformation.
Primitives
A Primitive is a descriptor. It is a way to specify a set of game ideas, such as a single feat, all spells of a certain level, or all races. It may be specific, like the name of a language, or broad like text describing all feats of a certain type. It may be direct, like the key of a Skill, or indirect, like the "Preferred Weapon" of a Deity.
In code, the Primitive is not the actual game rule object, but a pointer to one more such objects. These pointers can be CDOMReference objects from pcgen.cdom.reference or PrimitiveCollections built by PrimitiveToken objects in plugin.primitive.* Note that additional support (so that they can be combined) is provided by pcgen.cdom.primitive (CompoundAndPrimitive, CompoundOrPrimitive, NegatingPrimitive, PrimitiveUtilities).
Like many portions of PCGen, Primitives are defined as text in LST files. Primitives are instantiated in PCGen as a PrimitiveCollection. PrimitiveCollections contain reference objects that point to the game rule objects described by the Primitive text and are the primary objects used in the Primitive subsystem.
We have an effective "chain of responsibility" that takes place when looking for a Primitive.* Note: The members of the "chain of responsibility" are identified by various AbstractTokenIterators (one for Primitives, one for Qualifiers, etc.) within pcgen.rules.persistence.TokenLibrary.
(*Technically one member of the chain doesn't know about the next, so this may not literally be the design pattern as some see it. However, in practice, it implements the idea of having more than one potential object be responsible for the work and using the answer from the first to respond, so it is VERY close to the pattern if not exact)
PrimitiveCollection objects can be defined multiple ways. These are attempted in priority order:
- Produced from PrimitiveToken parsing
- Traditional Primitives
- Built in Collections
- Object referred to by a KEY
PrimitiveToken
The first method PCGen attempts to use to match when a Primitive is requested is to use a PrimitiveToken. Note that these are both the parser and the result of the parse.
In practice, the Primitives in plugin.primitive are actually self-owned collections, in that they are constructed through reflection (.newInstance() ) and the initialize() method is used to set up the PrimitiveCollection.*
(*This design is an architectural design decision that makes the primitive plugin a pure Mediator. The "core" of PCGen never comprehends exactly what is going on in the construction of the PrimitiveCollection, so that the token behavior is completely independent to the core and no core code is necessary to support the token. The PrimtiveToken implementation and the associated PrimitiveCollection implementation are forced into one class [which drives the reflection part of the design] due to limitations of our plugin loading system... which is nice and simple and doesn't have the overhead and complexity of something like OSGI).
The job of the PrimitiveToken is to take the String it finds in LST (a string that is intending to directly identify one or more objects) and convert that String into a PrimitiveCollection. Like all of the other tokens, they are part of the "Rules Load System", which is one (very big) Mediator (a design pattern) between the LST files and the in-memory storage of the rules. I refer to the in-memory storage as the "rules data store" [and is somewhat in pcgen.rules] and it is "led" for all practical purposes by the pcgen.rules.context.LoadContext.
This construction and initialization of the token based primitives is in pcgen.rules.persistence.ChoiceSetLoadUtilities.getTokenPrimitive()*
[*If you are thinking that ChoiceSetLoadUtilities is much like a Factory design pattern, you are correct, it has many of those characteristics of hiding underlying implemenation from a caller]
Format of a PrimitiveToken
Primitives look like:
NAME=Argument[Value]
The "name" (surprise?) is defined by the getTokenName() method of the PrimtiveToken interface.
Argument and value are enforced by initialize.
Value may be optional; not all tokens support or require a Value.
Load Failure Examples
What does it mean when initialize returns false and what would happen? It means the data monkey gets to go back and look at their data :D
One primitive, plugin.primitive.language.LangBonusToken, can be seen to return false if it has any arg or any value. Therefore it must appear alone:
CHOOSE:LANG|LANGBONUS
Another primitive, plugin.primitive.deity.PantheonToken, can be seen to require an arg. That's because this:
CHOOSE:DEITY|PANTHEON
... is meaningless. The specific question would be: WHAT Pantheon?!? So we need an argument.
CHOOSE:DEITY|PANTHEON=Norse
The opposite error is also true, so something that puts on an unnecessary argument:
CHOOSE:LANG|LANGBONUS=Norse
is also invalid (we don't want to lead people to believe it is doing something it's not)
Any of those invalid cases will return false, and an error will be generated during LST load. (In practice, a "null" primitive is returned up the call stack and is detected in ChoiceSetLoadUtilities.getChoiceSet and an LST_ERROR parse message is queued)
Load Considerations
For many PrimitiveToken types, a risk of zero contained objects is legal (since we can't predict at LST load whether it will be zero or more)... an example here is RACESUBTYPE=x, which could be zero if the Race of the PC has no subtypes. It could also be more than one if the PC has more than one race sub type (the 2nd often being applied by a template).
So errors are not produced if these don't have any content.
Traditional Primitives
These are constructed from: pcgen.rules.persistence.ChoiceSetLoadUtilities.getTraditionalPrimitive()
Built-in Collections
These primitives can refer to zero or more objects, so you might see LST that looks like:
CHOOSE:LANGUAGE|TYPE=Romance
The TYPE is a Built-in Group Reference.
Note that with certain forms of Group Reference, specifically TYPE, we produce an LST_WARNING if the type is referred to in the data but does not contain any members (and cannot contain any members without a data reload). That is often an indication that someone misspelled the TYPE, e.g. TYPE=Romanec rather than the group is really meant to be empty.
Since the reference concept is core to the Load System Mediator behavior, this usage is pretty much universal across all of our tokens (although most of them only build SimpleRef, TYPE Group References or the ALL Group Reference - All of those are handled by the ReferenceManufacturer).
Note: The ReferenceManufacturer (effectively**) enforces Multiton behavior for the references and underlying objects. Multiton is a design pattern that enforces a name->reference signature for all objects of a given type. This allows us to avoid duplicate references wasting memory and to avoid duplicate object creation. (**Technically since the classes the ReferenceManufacturers use are not private or something to that effect it is not a PURE implementation of this pattern, but purity is overrated in this case and would make it nearly impossible to test and also very, very hard to insert a 'hack' when we need it to do a version conversion). As a side effect, it puts all of our group references into one location so we can check if any are empty at the end of load (and produce the empty group LST_WARNING)
KEY based primitives
- Simple Primitives refer to one object, so you might see LST that looks like:
CHOOSE:LANGUAGE|English|French
The Simple Primitives example will produce two Primitive objects, both of which will be pcgen.cdom.reference.CDOMSimpleSingleRef objects. After the load is complete, we load the actual English and French language objects into those CDOMSimpleSingleRef objects, respectively. This management of references is done by a ReferenceManufacturer, usually a pcgen.cdom.reference.AbstractReferenceManufacturer.
Implementation in Practice: Leveraging References
For example, we might have a token that does:
CSKILL:Diplomacy
This has a CDOMSimpleSingleRef<Skill> built early on in the load called "Diplomacy". (CSKILL asked the ReferenceManufacturer<Skill> for a reference to "Diplomacy"... that CDOMSimpleSingleRef was lazily constructed, if not already in existence). After load is complete, the reference has been resolved to contain the Skill.class instance 'Diplomacy' (which we identify as such because getKeyName() on that instance returns the string "Diplomacy"). To get the underlying object out of the CDOMReference, we simply call .resolvesTo() on the reference at Runtime. (An exception is thrown if you try to resolve an unresolved reference, and if you don't have a Single Reference you are forced to use getContainedObjects() since there may be more than one object the reference has to return.)
Since the references already exist to handle those "built-in" Primitives types, we just extended the CDOMReference objects to implement PrimitiveCollection and treat them as "built-in" Primitives. So in the "not helping" department, I may occasionally refer to the "built-in" primitives:
ALL TYPE=x <name> (where <name> is any object constructed in the LST data)
...as "References" (especially in other documents on the wiki).
Dereferencing PrimitiveCollections
The (resolved) references / PrimitiveCollection objects continue to remain in the CDOMObjects and are used at runtime. This is required because some of them are not deterministic.
The full set of reasons we dereference at runtime, probably in order of increasing importance:
- It is (somewhat) computationally expensive to increment through all objects for all references, especially since a given PlayerCharacter will likely use only a tiny subset of loaded objects (so more computationally wasteful than expensive).
- It is computationally cheap to do de-referencing during runtime (it's a tiny bit of indirection, but basically a method call or two). Also, this form of "Lazy Initialization/Provider" design pattern should be really well recognized as it is used in many situations and is borderline prolific in designs that include Dependency Injection. (So this is should not be overly confusing).
- It requires significant knowledge of "knowing where to look". Right now, the load system (referring to the Mediator classes, primarily those in pcgen.rules.*) really knows nothing about the content. It moves items from one place to another (take a list and put it into the CDOMObject), but doesn't comprehend the contents of the list. If we had to do a replacement, it requires something learn that information on what to replace. For certain items, such as CSKILL, it wouldn't be that hard to increment over the List<CDOMReference<Skill>> and create a List<Skill>. That's not hard to do. For other objects, we have built up a composite of objects and the CDOMReference is deeply embedded into the object that the loading system actually took from the token. CHOOSE is a good example of this, as ChooseInformation has a ChoiceSet has a PrimitiveCollection. We'd have to rebuild that hierarchy (with different classes because the object types would be different - ugh) and/or store information temporarily and then build the composite later on. It leads to a lot of deferred processing that obfuscates what is going on in the tokens, and creates a lot more code that needs to be written, tested, and maintained.
- Resolving the References "up front" does not absolve us of deferred processing, because while it can be done for references, it cannot be done for all PrimitiveCollections (meaning some of the non-Reference Primitives are a problem). This is because some of them are dependent on the PlayerCharacter. One example is the CHOOSE:WEAPONPROF|DEITYWEAPON (see example below)... Since this can ONLY be resolved at runtime, we are forced to have the de-referencing code present at runtime anyway. Rather than going to all of the trouble of the items above and then having to write yet more code to understand what has and hasn't been de-referenced, we simply defer all de-referencing. This makes all the code a lot cleaner as we avoid an IF statement that makes a code reader comprehend why something would strike one branch vs the other. Cleaner to do it one way.
Skill Example
In the LST we may have a file that has:
MyFeat <> CSKILL:Search <> BONUS:SKILLRANK|%LIST|1 <> MULT:YES <> CHOOSE:SKILL|TYPE=Int
During load, we get:
- A CDOMSimpleSingleRef<Skill> for "Search"
- A CDOMGroupRef<Skill> for TYPE=Int (which serves as a Primitive for CHOOSE:SKILL)
Item 1 is placed into the MyFeat Object in a list of CDOMReference<Skill> objects (safe since CDOMSimpleSingleRef extends CDOMReference) Item 2 is (eventually) placed into the ChooseSet that represents the Chooser on MyFeat.
Both references continue to serve in memory during the entire use of PCGen.
When MyFeat is added to the PC, the ChooseSet requests the underlying PrimitiveCollection (in this case the CDOMGroupRef) provide the objects it contains. The CDOMGroupRef does this because during resolution (one of the later parts of the load process) it was told what objects it contains, so it returns that list.
Once the selection is taken, then the CSKILL token is processed. This is a List<CDOMReference<Skill>> and the system iterates over the list, and then calls .getContainedObjects() on each of those CDOMReferences. (This code is in pcgen.cdom.facet.analysis.GlobalSkillCostFacet.dataAdded() )
Weapon Proficiency Example
MyOtherFeat <> AUTO:WEAPONPROF|%LIST <> CHOOSE:WEAPONPROF|DEITYWEAPON
This time we have a Primitive from plugin.primitive.weaponprof, called DEITYWEAPON. This is a PrimitiveCollection that contains the Weapon Proficiencies defined on the DEITYWEAP token (as used in the Deity LST file) of the Deity of the PlayerCharacter.
Note this PrimitiveCollection is not deterministic. It can and will change for different PlayerCharacters that take this Feat as they will have different Deities.
This presents a problem for PrimitiveCollection resolution: It MUST occur at runtime to produce the correct answer.
Qualifier
A Qualifier is a concept, reduced to how it appears within the LST files. It is not one single concept within the code. There is literally no "Qualifier.java".
All of these described behaviors are "self-owned" in that all the code resides in the same class (or class hierarchy really) as the QualifierToken (again, pure Mediator design pattern to avoid knowledge of the token in the core).
The two optional behaviors are behaviors that can be applied within the methods of the PrimitiveCollection interface. Often the object implementing those interfaces is passed into the underlying PrimitiveCollection to provide non-default behavior. In some form, this is an interaction of Visitor (i.e. the converter is a form of a Visitor) and Decorator (since the Qualifier can alter what visitor is provided to the underlying PrimitiveCollection).
Behaviors
Qualifier Token
A "Qualifier" starts with a pcgen.rules.persistence.token.QualifierToken (again, like PrimitiveToken this is a Mediator responsible for taking Strings from LST and making them into the other behaviors).
Note that these are both the parser and the result of the parse.
In practice, the QualifierTokens in plugin.qualifier are actually self-owned collections, in that they are constructed through reflection (.newInstance() ) and the initialize() method is used to set up the PrimitiveCollection.
Since in the data, a Qualifier will contain a Primitive (which will return a PrimitiveCollection), Qualifier also serves as a Decorator of the underlying Primitive.
Primitive Collection
A Qualifier will implement pcgen.cdom.base.PrimitiveCollection, as a Qualifier serves as a *decorator* of collections, not providing "new" information; thus a Qualifier is designed to decorate another PrimitiveCollection.
Primitives and Qualifiers were designed and written together. Qualifiers can't really live without Primitives. (although Primitives do just fine on their own).
Note that the "ALL" Primitive is implied if nothing is provided to a Qualifier. (So a stand-alone qualifier in an LST file is legal and does contain a Primitive).
Filtering
This is an optional service provided by a Qualifier. This implements pcgen.cdom.base.PrimitiveFilter. This takes a set of native objects and reduces it based on certain characteristics.
Conversion
This is an optional behavior of a Qualifier. This involves implementing pcgen.cdom.base.Converter. This takes a PrimitiveCollection and converts it to native objects, although with some complexities.
Support for this behavior is provided by pcgen.cdom.converter (AddFilterConverter, DereferencingConverter, NegativeFilterConverter).
Examples
This is a list of all of our qualifiers and what they do: ANY: This is the "implied" qualifier, and is effectively a passthrough (it's a really bad filter :D ) PC: This is used in various places and means "The PC must possess the item". So it may be PC[TYPE=Foo], and this will return all <somethings - whatever the CHOOSE is> of TYPE=Foo that the PC has... QUALIFIED: This is a filter to check what items the PC is qualified to have (effectively checks the Prerequisites on the object to see what the PC would be eligible to have) EQUIPMENT: This is a converting Qualifier. It takes in Equipment and returns the appropriate type of proficiency. (Effectively this is evaluating the PROFICIENCY token on the equipment identified by the primitives in the brackets) SPELLCASTER: This is specific to weapon proficiencies and is a binary switch. If the PC is a spellcaster, it acts as ANY, if the PC is not a spellcaster, it returns an empty set. (so it's a form of filter, I guess) The following 5 items are for skills and filter the skills based on their relationship to the PC (either in skill cost or in number of ranks of the skill): CLASS CROSSCLASS EXCLUSIVE RANKS=x NORANK
Legal Syntax and default values
So, anyway, we use brackets to indicate the context and perform the filter:
Let me choose from any TYPE=Foo Domains that are possessed by the PC:
CHOOSE:DOMAIN|PC[TYPE=Foo]
Let me choose from any TYPE=Foo Domains for which the PC is qualified:
CHOOSE:DOMAIN|QUALIFIED[TYPE=Foo]
Let me choose from any TYPE=Foo Domains:
CHOOSE:DOMAIN|ANY[TYPE=Foo]
Note that this last filter is a "passthrough". It is effectively "implied" when a Qualifier is not provided in a CHOOSE token (one that supports Qualifiers anyway).
Note also that in a CHOOSE a Qualifier can be used without a Primitive. This is legal:
CHOOSE:DOMAIN|PC
...in that case, the ALL Primitive is implied, so it can be read as:
CHOOSE:DOMAIN|PC[ALL]
You can test this behavior by running the latter through the LST converter (which I think will return the former - without the ALL in brackets)
In other words, when a Qualifier is valid, a CHOOSE token ALWAYS has both a Primitive and a Qualifier, "ALL" and "ANY" respectively are implied when none is provided.
Design Discussion
Why Qualifiers?
The first complexity was that we needed to handle things like "Give me the choice of any Domains already selected by this PC"
We could assert that "PC" is simply another primitive reference... and just join by "AND" if we need to limit to a certain TYPE:
CHOOSE:DOMAIN|PC,TYPE=Foo
The problem with this is you basically build two lists and then conjoin the two lists (effectively do a .retainAll() call in a java.util.List). When one of them is known to be a filter on the other, it is more computationally efficient to actually run it as a filter... This is the Decorator design pattern, as the underlying Primitive and the Qualifier both implement PrimitiveCollection).
However, Qualifiers may be more than filters, in that they can also convert from one object type to another.
CHOOSE:SHIELDPROF|EQUIPMENT[TYPE=Foo]
It is important here to remember that brackets are context - so TYPE=Foo is evaluated *relative to Equipment* (get all equipment of TYPE=Foo) and then that equipment is queried to return any Shield Proficiencies required by that equipment. So inside the brackets you have a set of Equipment.class instances but you are choosing an instance of ShieldProf.class. It is this "context for the question" that is a critical distinction between a Qualifier and a Boolean operation with another Primitive. Another example appears later.
When a Qualifier can do this, it's a pcgen.cdom.base.Converter* (*technically it doesn't have to be, but it's sure a heck of a lot easier to write and comprehend if you do this).
So - where is this "check if the Qualifier is a converter and process it if it is?" happen?
In pcgen.cdom.choiceset.CollectionToChoiceSet... note that all* CHOOSE objects (*well, technically not all, but any that use CollectionToChoiceSet anyway) are run through a dereferencing converter that can change the PrimitiveCollections (often CDOMReferences) into the underlying objects (eventually calling .resolvesTo() or getContainedObjects() on a CDOMReference). This DereferencingConverter is passed into one of the methods on the PrimitiveCollection [in this case the Qualifier, and remember the Qualifier can actually provide a *different* Converter to the underlying PrimitiveCollection].
Lifecycle of a PrimitiveCollection
We have a pipeline of behavior:
- Have the LST define a set of references to specific objects, by whatever characteristics (TYPE, direct name, etc.). These references are changed from the String format in LST to a PrimitiveCollection by the PrimitiveToken
- Take that PrimitiveCollection through a converter. (Which converter is defined by the Qualifier String in the LST and "loaded" by the QualifierToken). This could convert between object types if necessary, but at a minimum will "dereference"/"resolve" any of the references so there is a set of actual objects used in memory in PCGen.
- Take those actual objects through a filter. (Which filter is defined by the Qualifier String in the LST and is loaded by the QualifierToken). This filter could, for example, be related to what the PC already has [PC], what they are qualified for [QUALIFIED] or a no-op that accepts all objects [ANY]
The entire Primitive/Qualifier system is designed around doing the pipeline above in a way that is [in no particular order]:
- As feature complete as possible
- As flexible as possible
- Reusing code where possible (and it's pretty high due to generics)
- As isolated as possible from the core
- Easily expanded
- Easily tested
What a PrimitiveCollection contains
In most cases, all of the objects that the PrimitiveCollection *eventually* contains [once References and the like are expanded/resolved] are Loadable. This requires three subtle clarifications:
Generally are CDOMObjects
In theory, the generics on PrimitiveCollection indicate they are not restricted to Loadable objects, so the design doesn't require the strict limit.
In PRACTICE, however, the getPrimitive() method of ChoiceSetLoadUtilities (our "factory") has "T extends CDOMObject" as its generic. Thus in practice, the objects eventually contained by the PrimitiveCollection are not just Loadable, but are CDOMObjects. New code would be requried to load a PrimitiveCollection that does not have CDOMObjects inside. (I don't forsee writing such code, as the non-CDOMObject CHOOSE subtokens use a different method of setting up their ChoiceSet).
Converting the Output
Note that the PrimitiveCollection itself might remap the output to a different type (e.g. It contains a reference asking for the PC's Deity, but eventually outputs the Deity's chosen weapon).
This can also cause the PrimitiveCollection to return something other than CDOMObjects from the getCollection method. (A specific example of this can be seen in pcgen.cdom.choiceset.CollectionToAbilitySelection).
Indirect References
There is (at least) one situation where the primitive is indirect. FEAT=x refers to the selections made in another feat. So to a degree, the Primitive contains a Loadable (the Feat identified by x), but in practice what is in the collection is actually the selections made in that Feat. Since that target feat could be something like CHOOSE:STRING (String is clearly not Loadable), there is an exception that means the items "effectively inside" (or "returned by" if you prefer) a PrimitiveCollection should not always be assumed to be Loadable.
Use in Practice
Use in CHOOSE
We always "exit" Primitive processing for CHOOSE with one PrimitiveCollection. When we do an AND or OR (AND in CHOOSE is , OR is | ) we simply put together those primitives with the classes in pcgen.cdom.primitive, such as CompoundOrPrimitive or CompoundAndPrimitive. These wrap them together so that when we want to pass around the PrimitiveCollection of all of the references, we can do so with one object. You can think of this as the composite design pattern (also combined in our ChoiceSetLoadUtilities "Factory")
Specifics of Values and getting intended results
Note that the value is sometimes optional... This can be seen in plugin.primitive.spell.ClassListToken.
We can use:
CHOOSE:SPELL|CLASSLIST=Wizard
That's fairly straight-forward, in that you can choose any spell on the "Wizard" list. However, we may want to restrict it. If we had designed the system to use the AND/OR functions we can run into problems. Let's assume this was valid:
CHOOSE:SPELL|CLASSLIST=Wizard,LEVELMAX=5
Well, what happens if it's a 6th level spell for a Wizard and 5th for a Bard? Do we accept it?
That type of problem leads us to: How can we design something so that we have the ability in the system to allow both "YES" and "NO" to that question (and basically avoid ambiguity). Well, first, we take away LEVEL as a primitive... and put LEVEL in context to the CLASSLIST:
CHOOSE:SPELL|CLASSLIST=Wizard[LEVELMAX=5]
In this way, we do the "NO don't accept Bard=5". The LEVEL restriction is processed *in context to* CLASSLIST=Wizard. If we want an independent LEVEL check we can do:
CHOOSE:SPELL|ALL[LEVELMAX=5]
Since these are two independent primitives we can do:
CHOOSE:SPELL|CLASSLIST=Wizard,ALL[LEVELMAX=5]
This says "Any spell that appears on the Wizard class list AND a spell ANYONE (not just the wizard) can cast as a 0-5 level spell"
This gives us the "YES, do accept Bard=5" solution to the above puzzle (and our system is "feature complete").
Note the Spell list based primitives allow a number of restrictions, so:
CHOOSE:SPELL|CLASSLIST=Wizard[LEVELMAX=5;LEVELMIN=4;KNOWN=YES]
...is legal. Semicolon in this case is AND.
Examples: Finding the appropriate Primitive
Simple Example
CHOOSE:WEAPONPROF|DEITYWEAPON
DEITYWEAPON is a primitive for WeaponProf.class. This processes and the PrimitiveCollection is returned.
Group Example
CHOOSE:SKILL|TYPE=x
TYPE=x is not a primitive for Skill.class. This returns null, since no PrimitiveToken was found, and the "built-in" Traditional Primitives are checked. This successfully processes and the PrimitiveCollection is returned.
Example with a PrimitiveToken
CHOOSE:WEAPONPROF|TYPE=x
TYPE=x IS a primitive for WeaponProf.class. This "conflicts with" the "built-in", but due to the chain of responsibility it is processed first, and OVERRIDES the "built-in" TYPE=x. This successfully processes and the PrimitiveCollection is returned.
Example with FEAT
CHOOSE:WEAPONPROF|FEAT=Martial Weapon Proficiency
FEAT=x is not a primitive for WeaponProf.class. Therefore, we check the parent of WeaponProf We do not have any primitives for PObject.class, so we check the parent of PObject We do not have any primitives for CDOMObject.class, so we check the parent of CDOMObject We do not have any primitives for ConcretePrereqObject.class, so we check the parent of ConcretePrereqObject We DO have a primitive for FEAT=x for Object.class. This successfully processes and the PrimitiveCollection is returned.
Note what we have managed to do here is write a primitive ONE time and have it work for ALL CHOOSE: subtokens (regardless of whether it's CHOOSE:WEAPONPROF, CHOOSE:STRING, CHOOSE:LANGUAGE, whatever... FEAT=x will ALWAYS work as a primitive and we did it with one class)
Note that this works for the Class hiararchy, NOT interfaces. (If we allowed interfaces, it would introduce ambiguity as to which should be processed first, and we want to avoid ambiguity!)
A last note for this item: The "chain of responsibility" does NOT fail if something returns false. It tries until one of the primitives succeeds or there are none left to try. (This is not used in practice in Primitives, but operates like normal LST tokens as described in the Rules Persistence System.
Error Catching
Bad Primitive
CHOOSE:WEAPONPROF|BADPRIMITIVE
BADPRIMITIVE is not a primitive, it will therefore go through the "chain of responsibility" similar to the FEAT=x example above, but no primitive token will succeed. Thus, getTokenPrimitive will return null getTraditionalPrimitive will look for built-in primitives and think "BADPRIMITIVE" is a WeaponProf.class and build a CDOMSimpleSingleRef<WeaponProf> for BADPRIMITIVE. However, (barring some really strange data), the WeaponProf file does not contain a Weapon Proficiency called "BadPrimitive" and thus when we get to load validation we will get an "Unconstructed Reference" error. This indicates someone asked for the WeaponProf "BadPrimitive" and it was not constructed.
The lesson here is that some errors are not immediately caught, but require the later validation to understand they are really a problem. The syntax for us has 2 effects: (1) There is ambiguity for us as to whether something was a primitive or a "native" object. Therefore the error message for both problem cases is the same to the data monkey. (2) Any primitive we create "invades" the namespace of that type of object and creates "reserved words" that the data team can't use. They can't, for example, have a Language called "LangBonus" because it could never be explicitly used in a CHOOSE.
Error Catching with FEAT
An earlier example established why the "PrimitiveToken" FEAT=x returns Object.class when it is acting as a PrimitiveToken (saved a lot of duplicate effort and risk of copy/paste errors), but we still haven't answered whey we needed Class<T> to be passed in (and why FEAT=x *captures* that in the PrimitiveCollection).
The answer is that we need error checking later on.
The FEAT=x primitive effectively says "allow me to choose from any items that were chosen on this other Feat". This is used for things like weapon mastery, which requries proficiency.
Imagine we did something like this:
CHOOSE:LANG|FEAT=Martial Weapon Proficiency
This immediately looks like a problem, right? (We kind of know as users that Martial Weapon Proficiences returns WeaponProf.class, but we are telling it to choose a Language.class). But how do we actually detect it in code???
The challenge is that when we hit this CHOOSE token, Martial Weapon Proficiency may not exist yet (remember the multi-pass load). Also, it may be .MODed and thus even if it did exist, we can't ask about it's behavior (the answer might be wrong). We are forced to store a reference to the Feat (a CDOMCategorizedSingleRef<Ability>).
What we *do* factually know is that we *expect* (in this case) the "Martial Weapon Proficiency" Feat to know about Languages. So we can store this information inside of the PrimitiveCollection. The Class<T> that we pass in stores the "we think it's *this*" information.
After load is complete, and the CDOMReferences are resolved, we should go back and ask whether the collection is what we expect. This will then use the getReferenceClass() that is initialized ("what we expect") to validate against the CHOOSE from the target Feat ("what it really is")
Unfortunately, WE DON'T. GAAAH! So - thanks for making we walk through this - you have made me realize there is error checking we are not performing at LST load. :D
The check should be something like: Ability targetFeat = argument.resolvesTo(); Class<?> choiceClass = targetFeat.get(ObjectKey.CHOOSE_INFO).getClassIdentity().getChoiceClass(); if (!cl.isAssignableFrom(choiceClass)) {
//error
}
...and should probably be performed in getGroupingState (should return GroupingState.INvALID in case of error)
Then, we should be incrementing through all objects with CHOOSE to have object.get(ObjectKey.CHOOSE_INFO).getGroupingState() ensure it does not return GroupingState.INVALID. (In practice, this check should be added to the process() method of plugin.lsttokens.ChooseLst)
Further Reading
If you are looking for how Choices are done today and in practice what Primitives and Qualifiers look like, see: CHOOSE Token Proposal for 6.0