Setting up the new Formula System
Contents
|
The basics
The new formula system is designed to do a few things relative to data:
- Validate formulas at LST load instead of runtime
- Dramatically increase flexibility
- It will replace many tokens, including all the BONUS tokens
- It allows a sensible location for global modifications (things generally related to rule book type modifications that impact all objects of a given type)
Key concepts
To use the new system a few key things need to be remembered:
- It is possible to have local variables
- All variables must be defined prior to use
- Variables are not only numbers, but can be Strings, Dice, etc.
- We call this the variable FORMAT
You will want to keep track of what these key terms mean as you learn the new formula system:
- FORMAT: The type of variable, e.g. NUMBER, BOOLEAN, ORDEREDPAIR.
- SCOPE: The part of the data in which a (local) variable is available
- MODIFIER: An argument to MODIFY* tokens, specifically the "command" of what the modification represents
- OPERATOR: A mathematical operator as you would see in a formula ("+", "-", etc.)
- GROUPING: A name for a set of objects in PCGen
- ASSOCIATION: An additional set of information attached to a MODIFY* token. This is a key-value pair of information, e.g. PRIORITY=100
Creating and Using Variables
Required Setup
Every format that needs to be used requires a default value. This value is shared throughout an entire system (it CANNOT be redefined to a different value). This is placed into a file referred to in a PCC file from the DATACONTROL: token.
DATACONTROL (PCC File)
DATACONTROL: is a new Campaign file token (for PCC files). It is treated "recursively" like other tokens, e.g. TEMPLATE.
DATACONTROL:x
- x is the file to be loaded as a DATACONTROL file
- Limitations: It does not support PRExxx or INCLUDE/EXCLUDE.
DEFAULTVARIABLEVALUE (DATACONTROL LST)
The Format is:
DEFAULTVARIABLEVALUE:X|Y
- X is the format name
- Y is the default value
Example:
DEFAULTVARIABLEVALUE:NUMBER|0
It is a best practice to always define a DEFAULTVARIABLEVALUE for every FORMAT regardless of whether the data uses that FORMAT. The internal parts of PCGen may use it, so a default value may be required.
Note: it IS safe to "redefine" a DEFAULTVARIABLEVALUE to the same value. So if multiple datasets are loaded and they all had DATACONTROL: and all defined the default value for NUMBER to be 0, they will all happily load as the system is capable of recognizing the defaults are the same
How to define a variable
VARIABLE (PCC File)
To define a variable, you need to add an LST file that is referred to in a PCC file from the VARIABLE: token. It is treated "recursively" like other tokens, e.g. TEMPLATE.
VARIABLE:x
- x is the file to be loaded as a VARIABLE file
- Limitations: It does not support PRExxx or INCLUDE/EXCLUDE.
There are 2 basic tokens for the VARIABLE file: GLOBAL and LOCAL.
GLOBAL (VARIABLE File)
- GLOBAL defines a global variable, usable from anywhere within the PlayerCharacter.
The format is:
GLOBAL:X=Y
- X is the format name
- Y is the variable name
Example:
GLOBAL:ORDEREDPAIR=Face
A GLOBAL token can take additional token on the line called EXPLANATION, which it is advisable to use to communicate to other data monkeys (and it is likely any future editor will also draw on this information).
LOCAL (VARIABLE File)
- LOCAL defines a local variable, usable only within the object on which the variable exists (or its children)
- The place where a local variable is usable is called the SCOPE. This could include a STAT (such as the stat's SCORE or STATMOD) or EQUIPMENT. The scopes possible in PCGen are hard-coded (see below for more info)
The format is:
LOCAL:W|X=Y
- W is the SCOPE of the variable
- X is the format name
- Y is the variable name
Example:
LOCAL:STAT|NUMBER=Score
A LOCAL token can take additional token on the line called EXPLANATION, which it is advisable to use to communicate to other data monkeys (and it is likely any future editor will also draw on this information).
EXPLANATION (VARIABLE File)
EXPLANATION: is a free-text token (no parsing is really done) that is designed to describe why a variable exists
Format:
EXPLANATION:x
- x is the text of the Explanation
- Limitations: By Design: Must not appear as the first token on a line
- Behavior: Overwrites (as if someone would use it twice??)
Defaults in the VARIABLE file
A few notes about the VARIABLE files: If you see an item without a leading token, e.g.:
ORDEREDPAIR=Face
...then the system is using the default (GLOBAL). For now, the data standard is to avoid using this shortcut (as far as I know)
If you see an item without a leading format ("X" in both GLOBAL and LOCAL above), e.g.:
LOCAL:STAT|Score
...then the format is a NUMBER. For now, the data standard is to avoid using this shortcut (as far as I know)
Note both defaults can be combined, so a variable name with no other information on that line is a Global NUMBER
No Overlapping
In most programming languages, it is legal to have local variable share the same name as the global variable and thus "override" it in that context (although it is considered less than ideal, and in some languages it will now produce a warning). It is not legal in PCGen.
If a Global variable called "Hands" exists, then NO local variable IN ANY SCOPE can be "Hands".
Similar restrictions exist for scopes and SubScopes. If "CritMult" is an EQUIPMENT variable, then "CritMult" can't be an EQUIPMENT.PART variable.
Non-overlapping (when the Scopes do not interact) is legal. "Score" can be a local STAT variable and a local CHECK variable, for example.
For clarity:
- This occurs regardless of the FORMAT of the variables - tokens will infer the format from the variable name, so you can't have overlapping variables even of different FORMAT.
- Two variables of the same name but different FORMAT in the same SCOPE will cause an error as well, because that is effectively overlapping, and those variables can't be distinguished in most circumstances.
Identical Duplication Legal
It is legal for a load to include one or more PCC files that have one or more VARIABLE: files that redefine the same variables. As long as they are the same format and scope, they are not considered an error. If either the scope or format differs, then the "No Overlapping" rules above are applied.
A difference in the "Explanation" text is not considered a difference in the variables significant enough to trigger an error. (In fact, the last loaded Explanation will "win" -> it is overwritten each time it is encountered for redefined but identical variables)
Setting and Modifying a variable
MODIFY (LST files)
The MODIFY token is used to set a variable that is accessible in the current SCOPE. (Note: It could be local to the current scope or any ancestor of the current scope... so modifying a global variable always just requires MODIFY)
The format of the token is:
MODIFY:V|W|X|Y=Z|Y=Z
- V is the Variable name for which the value will be modified
- This variable MUST be visible in the current scope. Therefore, it must be either a global variable or a local variable on the current object or its parents.
- W is the MODIFIER to be taken on the variable (e.g. SET)
- There can be multiple actions (more below), but SET will ALWAYS be available for any format.
- X is the value related to the action. If the action is set, the variable will be set to the value in this part of the token.
- Note: The value CAN be a formula. It is possible, for example, to SET a variable to OtherVar+4 ... the system will solve that formula. For legal OPERATORS (e.g. "+") , see below.
- Y is the name of an (optional) ASSOCIATION. For now, PRIORITY (more below) is the only available association.
- Z is the value of the association.
Note: If a PRIORITY is not set it is assumed to be ZERO.
Example: Give a Player Character 2 hands:
MODIFY:Hands|SET|2
MODIFYOTHER (LST files)
The MODIFYOTHER token is used to set a variable that is not accessible in the current SCOPE. Beyond what is necessary in MODIFY, this token also requires the "address" of where to go before it runs the modification. Think of it as the ability to remotely place a MODIFY on another object (but revoke that MODIFY if the current object is no longer granted to the PC)
The format of the token is:
MODIFYOTHER:T|U|V|W|X|Y=Z|Y=Z
- T is the FORMAT of the object on which the resulting MODIFY (as defined by V-Z) should occur
- U is the object or GROUPING of objects on which the MODIFY (as defined by V-Z) should occur
- Note that this does not and will not support TYPE.
- V is the Variable name for which the value will be modified
- This variable MUST be visible in the scope of the addressed object. Therefore, it is likely to be a local variable on the remote object or its parents.
- In rare cases, a remote modification of a global variable is useful. It effectively adds to that global variable only if both objects are granted to the PC.
- W is the MODIFIER to be taken on the variable (e.g. SET)
- There can be multiple actions (more below), but SET will ALWAYS be available for any format.
- X is the value related to the action. If the action is set, the variable will be set to the value in this part of the token.
- Note: The value CAN be a formula. It is possible, for example, to SET a variable to OtherVar+4 ... the system will solve that formula. For legal OPERATORS (e.g. "+") , see below.
- Y is the name of an (optional) ASSOCIATION. For now, PRIORITY (more below) is the only available association.
- Z is the value of the association.
Note: If a PRIORITY is not set it is assumed to be ZERO.
Example: Reduce the HandsRequired on all Weapons by 1:
MODIFYOTHER:EQUIPMENT|GROUP=Weapon|HandsRequired|ADD|-1
Note this assumes GROUP:Weapon is in the LST line of every Weapon... PCGen can't guess or derive what a weapon is :)
Formulas
SET as a MODIFIER allows the use of a formula for the value. This follows many mathematical systems of using parenthesis - "(" and ")" to be precise - to group (and thus prioritize parts of the calculation). There are a number of built in OPERATORs (see below). The system can also use functions, which use parenthesis to contain their arguments. For example:
- min(this,that)
Would return the minimum of the two variables "this" and "that".
Built in Functions
The system also has a series of built-in functions that work with the NUMBER format:
- abs: calculates the absolute value of the one argument.
- ceil: calculates the next integer that is >= the value of the one argument.
- floor: calculates the next lower integer that is <= the value of the one argument
- if: conditionally performs an operation. The first argument must resolve to a BOOLEAN. If true, the second argument is evaluated; if false, the third argument is evaluated.
- max: calculates the maximum of the provided two or more comma-separated arguments
- min: calculates the minimum of the provided two or more comma-separated arguments
- round: calculates the closest integer to the value of the one argument. Exactly 0.5 is rounded up.
- value: returns the value of the calculation before the current modification was started (no arguments)
Additional functions specifically related to PCGen are available:
- get: This function takes two arguments. The first is a Object type (like "SKILL"), the second is the key of a CDOMObject (like a Skill) (also in quotes)
- Example: get("SKILL","Hide")
- getFact: This function returns the value of a FACT. The first two arguments are much like the two arguments of "get", the third argument is the name of the FACT to be returned
Legal Values
FORMATs
Currently, the following are built-in formats:
- NUMBER: This is a mathematical value. Note that integer arithmetic is done if possible, so if you have an integer 6 and an integer 3, 6/3 will be an integer 2. If the 6 or 3 is a decimal value (double), then integer arithmetic was not possible and thus rounding may occur.
- STRING: A String
- BOOLEAN: A True/False value
- ORDEREDPAIR: An ordered pair of numbers. This could be an X,Y point (or in a game system more like a FACE)
- DICE: A value that takes the form AdB+C. A is the number of rolls of the die, B is the number of sides on the die, and C is the value to be added afterwords
- ALIGNMENT: An alignment object (None is default typically)
OPERATORs
For NUMBER, the following modifiers are available:
- +: performs addition
- -: performs subtraction (or is the unary minus operator to make a value negative)
- *: performs multiplication
- /: performs division
- %: performs a remainder function
- ^: performs an exponential calculation
- ==: Checks for equality (NOTE: Returns a BOOLEAN, not a NUMBER)
- !=: Checks for inequality (NOTE: Returns a BOOLEAN, not a NUMBER)
- >: Checks for greater than (NOTE: Returns a BOOLEAN, not a NUMBER)
- <: Checks for less than (NOTE: Returns a BOOLEAN, not a NUMBER)
- >=: Checks for greater than or equal to (NOTE: Returns a BOOLEAN, not a NUMBER)
- <=: Checks for less than or equal to (NOTE: Returns a BOOLEAN, not a NUMBER)
For BOOLEAN, the following modifiers are available:
- ==: Checks for equality
- !=: Checks for inequality
- &&: Performs a logical AND
- ||: Performs a logical OR
- !: Negates the value
For other formats, the following operators are always available:
- ==: Checks for equality (NOTE: Returns a BOOLEAN, not the compared format)
- !=: Checks for inequality (NOTE: Returns a BOOLEAN, not the compared format)
MODIFIERs
For NUMBER, the following modifiers are available:
- SET: This sets the variable to the value provided in the "X" parameter
- ADD: This adds the current value of the variable to the value provided in the "X" parameter
- MAX: This takes the maximum value of the current value of the variable and the value provided in the "X" parameter (i.e. min(current value, "X"))
- MIN: This takes the minimum value of the current value of the variable and the value provided in the "X" parameter (i.e. max(current value, "X"))
- MULTIPLY: This multiplies the current value of the variable and the value provided in the "X" parameter
- DIVIDE: This divides the current value of the variable by the value provided in the "X" parameter
For other Formats, the SET modifier will always be available
GROUPINGs
At present, the second argument of MODIFYOTHER supports 3 possible values:
- ALL
- This means the MODIFYOTHER will impact ALL objects of that type. This is useful for setting a general default for that value (e.g. you could default "HandsRequired" on weapons to 1)
- GROUP=X
- This is set by the GROUP token in LST data
- A specific object KEY
More will be added over time.
SCOPEs
The following are the legal scopes (not including GLOBAL):
- EQUIPMENT: This is a variable local to equipment
- EQUIPMENT.PART: This is a variable local to an equipment PART (or in older terms, an Equipment HEAD). Note that due to double sided weapons, Damage, CritMult and other items are generally on an equipment PART, not on the Equipment itself. Note also that this is a SUBSCOPE of the EQUIPMENT scope (so all variables in the EQUIPMENT scope can also be seen in the EQUIPMENT.PART scope)
- SAVE: This is a variable local to Save (or if you prefer, Check)
- SIZE: This is a variable local to the Size of a PC
- SKILL: This is a variable local to a Skill
- STAT: This is a variable local to a Stat
More will be added over time.
DYNAMIC can also be used to create a Scope and Object Type (see below)
Examples
Understanding SubScope in practice
To understand how local variables work in SubScopes, consider the following (note this is not all the variables we will use but a critical set to define the scope):
GLOBAL:NUMBER=Hands LOCAL:EQUIPMENT|NUMBER=HandsRequired LOCAL:EQUIPMENT|BOOLEAN=Penalized LOCAL:EQUIPMENT.PART|NUMBER=CritMult
The Equipment part is what the traditional system called a "head" (so a double headed axe has two "parts" in the new system - this was done since in some game systems, an item could have more than 2)
To understand how variables are available and how they get modified consider this:
Inside of a piece of equipment it could do something like:
MODIFY:Penalized|SET|HandsRequired>Hands
This would be "true" if HandsRequired on the Equipment were more than the Hands on the PC. The "Hands" variable is Global, so it is available anywhere. A Template of "Extra Hands" could do something like:
MODIFY:Hands|ADD|2
... and that adds to the Global variable "Hands".
Now consider an Equipment Modifier that was something like "Lighter Than Normal". Assume the game benefit was one less hand is required. This would look like:
Lighter Than Normal MODIFY:HandsRequired|ADD|-1
(This ignores for the moment any situation like "can't be reduced to below one".)
Note that the EquipmentModifier falls into EQUIPMENT.PART, so that it has access to the HandsRequired variable from the EQUIPMENT scope and can modify it. Note that this change could then impact the result of the BOOLEAN variable Penalized, since HandsRequired changed. Note also that "HandsRequired", when evaluated anywhere in that Equipment is now 1 less. Even if another EquipmentModifier evaluates HandsRequired, it is analyzing the variable from its parent scope, so that value on a variable from EQUIPMENT is the same in every EquipmentModifier (in the EQUIPMENT.PART scope) of that piece of Equipment.
Understanding SubScope on Equipment
Since variables like CritMult and Damage would be on an Equipment "part" it is important to understand how to get access to that "part" from the LST code. If a single-headed weapon wanted a larger CritMult, it would be silly to have to add a Dummy EquipmentModifier just to modify the CritMult on the part. (Much of the point of the new system is to make things more direct)
There is a token for Equipment called PART. This effectively takes 2 arguments: A number (today 1 for the primary head, 2 for the secondary head) and a full token otherwise valid on a plain object. MODIFY and MODIFYOTHER will both work in that situation, so for a piece of Equipment to set its own CritMult to 3 on its primary (and perhaps only) head, it would do:
PART:1|MODIFY:CritMult|SET|3
Note since the MODIFY is a "real" token call it uses ":" after the MODIFY which makes PART look slightly different to most tokens which use | as an internal separator. Since PART is effectively calling a "real" token, we need to use the inner ":" in order to have it pass through the same code/parsing system.
Understanding SubScopes in relation to MODIFYOTHER
- Note Jan 29 2018: There is a possibility of a solution for this issue that would allow MODIFYOTHER to do direct addressing of sub-objects under certain conditions
Normally MODIFYOTHER allows you to "address" another scope. For example:
MODIFYOTHER:EQUIPMENT|Bastard Sword|HandsRequired|ADD|-1
However, since items like Damage and CritMult are actually on the EQUIPMENT.PART, that is different. If you consider how you would have to "address" the first head on "Bastard Sword", you would need something like:
SOMEWEIRDTOKEN:EQUIPMENT|Bastard Sword|PART|1|CritMult|ADD|1
Note that this requires both a primary and secondary address. No token like this exists (at the moment). To alter items in a SubScope, you need to pass them down to the SubScope. An attempt to modify it at the Equipment level, such as:
MyAbility MODIFYOTHER:EQUIPMENT|ALL|CritMult|ADD|1
... will fail (at LST load) because CritMult was on EQUIPMENT.PART, so it is not visible to the EQUIPMENT scope. Said another way, you can't universally modify all EQUIPMENT.PART variables by attempting to use that variable at a higher scope.
A more appropriate method (since this is relevant to a Feat that might alter CritMult) is to add another variable at the EQUIPMENT level:
LOCAL:EQUIPMENT|NUMBER=CritMultAdder
If all the heads then get
MODIFY:CritMult|ADD|CritMultAdder
... you can have the Feat do a MODIFYOTHER to alter CritMultAdder and the MODIFY will "pull" that into each PART:
SomeFeat MODIFYOTHER:EQUIPMENT|ALL|CritMultAdd|ADD|1
So the data can be set up to only require one modification from a Feat and each Equipment Part can pull that modification down into the part. (A tangential note for those objecting that the known modifiers of CritMult select a specific weapon type: Yep, got it. That's not possible yet, so not documented yet. Suspend disbelief on the examples - Imagine a more powerful Feat for now)
Understanding PRIORITY and processing of MODIFY and MODIFYOTHER
When a PlayerCharacter has multiple items that attempt to modify a variable, they are sorted by two systems:
- First, they are sorted by their user priority. This is provided in the PRIORITY=Z association. Lower PRIORITY will be processed first.
- Second, they are sorted by their inherent priority. This is a built-in system that approximates mathematical priority rules. It ensures, for example, that a SET will occur before ADD if they both have a matching user priority.
Example: Assume we had the following items:
MODIFY:Hands|MULTIPLY|2|PRIORITY=100 MODIFY:Hands|SET|2|PRIORITY=50 MODIFY:Hands|ADD|1|PRIORITY=50
The first thing is that we can tell from the PRIORITY items that the SET and ADD will both occur BEFORE the multiply (because 50 < 100). For the SET and ADD, we would need to know the inherent priority (For what it's worth, SET is 0, ADD is 3, MULTIPLY and DIVIDE are 1 and 2 since they are higher priority than ADD in traditional math). This means the SET will happen first. So the result is Hands is SET to 2, then ADD 1 to get 3, then MULTIPLY by 2 to get 6.
By controlling the PRIORITY, the data team can reorder any calculation and perform much more complicated calculations than was possible with BONUS (where movement, for example, had multiple different BONUS tokens to try to do adding before and after a multiply). This is much the same as using parenthesis in a formula in normal math, but this allows you trigger any required ordering even if the MODIFY statements are in different objects.
Using the previous value in a more complex calculation
Take a modification that, for example, wishes to perform a calculation such as: "Reduce the value by 1, but to no less than 1". We could write this as two MODIFY statements, but that is probably undesirable for a few reasons:
- If the book states it as one sentence, it would be preferable to write it as one MODIFY
- It makes the calculation a bit harder to follow as to what is going on
- If the items are different PRIORITY, there is a risk of another dataset attempting to (or accidentally) putting something in between those two steps, resulting in an incorrect calculation.
Therefore, we need a method of drawing on the current value in a formula. To do that we use the value() function. The calculation above then becomes:
max(value()-1,1)
...and we can use it in a MODIFY as such:
MODIFY:HandsRequired|SET|max(value()-1,1)
Note that we are using SET here - if a formula is used, it is likely to be best done as a SET, since that will be much more clear than calculating a formula and then immediately applying another MODIFICATION. Note that some modifications may also prohibit a formula depending on their underlying behavior.
Global Modifier File
There is a new file type that is defined in the PCC files with GLOBALMODIFIER.
This file allows for a centralized (and clear to the data user) location for global modifications (don't hide things on stats or elsewhere anymore). This supports MODIFY for global variables (if you want to start all PCs with 2 hands for example) or MODIFYOTHER for local variables.
GLOBALMODIFIER (PCC File)
Format:
GLOBALMODIFIER:x
- x is the file to be loaded as a GLOBALMODIFIER file
- Limitations: It does not support PRExxx or INCLUDE/EXCLUDE.
Advanced Topics
Dynamic Objects and Scope
This set of features allows the data to supplement the defined SCOPEs with new object types not originally comprehended in PCGen. Within a Dynamic Scope, the data can produce objects.
Warning: Attempts to define a SCOPE that overlaps with existing objects (e.g. LANGUAGE) will not produce a warning today but will get you in trouble long term. It won't work in the way you would like it to work (it won't attach a SCOPE to that object type), so just avoid those overlapping names. Basically if you can CHOOSE it, or use it with FACT, don't DYMAMICSCOPE it.
DYNAMICSCOPE (DATACONTROL LST)
The DYNAMICSCOPE token (used in files referred to by the DATACONTROL: token in PCC files) can define a Dynamic Scope.
DYNAMICSCOPE:x
- x is the new scope
- Any argument to DYNAMICSCOPE becomes a legal "scope" in LOCAL: in the variable definition file
- Limit of one new scope per line. (No delimiter on this token)
- Like other LST tokens, this is case insensitive, but usage in later tokens is always capitalized, so I suspect the data standard should be ALL CAPS
DYNAMIC (PCC File)
In order to create objects within the defined DYNAMICSCOPE, DYNAMIC: becomes a new token legal in PCC files. It's format is:
DYNAMIC:x
- x is the file just as in tokens like DEITY or DOMAIN
- INCLUDE/EXCLUDE on the line are are legal.
Within the Dynamic file, the Scope of the new objects being created MUST appear as a prefix token on the line. For example, if there was a DYNAMICSCOPE:VISION, then the following would be legal in a DYNAMIC file:
VISION:Darkvision VISION:Low-Light Vision
- The "Token Name" (what appears before the ':') is the DYNAMICSCOPE name.
- These are created as basically hollow, simple objects.
- These objects CAN contain variables.
(It is the intent to allow these at some point to also have MODIFY* tokens, but that is not currently supported)
GRANT (LST files)
A Dynamic object MUST be granted in order to have its local variables modified or to have its modifiers have an impact (just like any other object)
GRANT:x|y
- x is a DYNAMICSCOPE
- y is the name of the dynamic object.
Export
Dynamic items can be exported in Freemarker using the
<#list pc.dynamic.x as y>
- x is the name of the DYNAMICSCOPE
- y is the local variable within the #list that the output team wants to use to access the dynamic object
- It is likely for ease of use by the output team, x and y will be identical
For a complete example using output, see below
Example
DATACONTROL: file:
DYNAMICSCOPE:VISION
DYNAMIC: file:
VISION:Normal VISION:Darkvision VISION:Low-Light Vision
VARIABLE: File:
LOCAL:VISION|NUMBER=Range
GLOBALMODIFIERFILE:
MODIFYOTHER:VISION|ALL|Range|Set|60
Sets a "default" range for any VISION, to avoid setting on each VISION object
RACE LST file:
Human <> GRANT:VISION|Normal
Export:
<#list pc.dynamic.vision as vision> ${vision.name} ${vision.val.range} </#list>
(this ignores converting the range to local units and all that, but you get the idea)
ARRAY Format
In addition to the base formats, there is an ARRAY[x] format:
ARRAY[x]
- x is an existing valid Format
- x CANNOT be ARRAY (multi-dimensional arrays not supported
- Just to be precise, yes DYNAMIC formats are legal
(Note: This may get renamed - we need to decide on a few things for how this will work vs other types of collections)
Modifiers
There are two valid MODIFIERs for ARRAY:
- ADD: This adds a value to the ARRAY
- Mathematically, ARRAY acts as an ordered set of objects. Therefore, if you add an item to an array more than once it is ignored.
- SET: This sets the contents of the ARRAY
Custom Functions
Within certain game systems, there are calculations that are likely to be repeated. In order to simplify the calculation of these items, there is an ability to create custom functions.
A function can have zero or more arguments. The number is defined by what appears in the VALUE token (see below).
Note that the function actually has to be *used in the data* (not just defined) for most errors to be caught (since the system can't guess at overall context before a function is used and it would be possible to write a function that would work in more than one FORMAT).
Function (DATACONTROL LST)
FUNCTION:x
- This token must appear as the first token on a line in a DATACONTROL file.
- x is the name of the function. This name must starts with a letter, e.g. A-Z, additional legal characters are 0-9 and _ (none of those additional legal characters as the first character)
- x must not contain spaces, no other special characters.
- Function names are case insensitive (as many other things in LST files)
A Function MUST also contain a VALUE, which defines how the function is calculated.
If a FUNCTION appears more than once, the other characteristics (VALUE) must be identical.
Value (DATACONTROL LST)
VALUE:x
- Must appear as an additional token on the line of a FUNCTION: in the data control file
- x is a valid formula.
- This means it must have valid variable names, valid function names, matching parenthesis, etc.
In addition to all of the built-in functions (e.g. if, ceil, round), two additional items can be used:
- the arg(n) function (see below).
- Any previously defined FUNCTION. This MUST appear in the file before the current Function or in a file processed before the current file.
arg (Function)
The arg function is a "local" function to the VALUE: part of a FUNCTION (it can not generally be used in LST files). The arg function takes one argument. It MUST be a Integer >= 0.
arg(x)
- x is an integer number (zero-indexed)
When arg(x) is used in value, the function pulls from the arguments that were provided to the original function in the data.
Using a Function
When a function is used in LST data, it is called by the name provided in the FUNCTION token. It requires a certain number of arguments. This exactly matches the integer one greater than the highest number of the arg(n) function provided in the VALUE part of a FUNCTION (In computer science terms, the arg(n) function is zero-indexed). The provided arguments can be any legal formula, so:
d20Mod(INT) d20Mod(INT+4)
...are both perfectly legal.
Example
The modification calculation in most d20 games is fairly easy to calculate, but is repeated very often. It basically takes (ability score - 10)/2.
Here is how we would define a new function d20Mod in the DATACONTROL file:
FUNCTION:d20Mod VALUE:floor((arg(0)-10)/2)
This would then be used in LST data as:
d20Mod(n)
The system will catch both of these as errors:
d20Mod() d20Mod(14,5)
These have too few or too many arguments, respectively. It would also catch:
d20Mod("mystring")
...as an error (wrong format - requires a Number, found a String).
The n is substituted where "arg(0)" appears in the "VALUE"... so a statmod would be as easy as:
d20Mod(INT)
or some such... Direct values can also be used, e.g.:
d20Mod(10)
...would return 0.
Performance Considerations
It is possible to write two identical modifiers that perform the same net calculation on a PC. For example:
MODIFY:Age|ADD|2 MODIFY:Age|SET|value()+2
...perform the same calculation (adding 2 to Age).
Why have ADD at all? Answer: These are NOT AT ALL equivalent in memory use or speed/performance.
The ADD (Since it's adding an integer) occupies approximately 40 bytes of memory and is an extremely rapid calculation: If it took 200 nanoseconds I'd be surprised. This is because ADD is using a number. If it was a formula, even 1+2, it would load the formula parser. This special case on constants allows the modification system to modify a value without loading a much larger infrastructure, and this is valuable as a large majority of our calculations are simple modifications of values.
The SET shown above (since it has an operator) loads the formula system, and thus may occupy 500 (or more) bytes of memory (>10x), so it could take 100 times as long to process as the ADD (it also took longer during LST load).
Thus: When possible the data standards should be written to use numeric modifiers and no operator. (Think "static" modifiers ... that only use numbers) rather than a formula.
Lookup and Tables
For some situations, it makes more sense to have a lookup table rather than attempting to have a set of tokens or calculations perform processing. This also allows for much cleaner translation from books when the books are using tables to organize information.
The Table file format is designed to allow you to use a spreadsheet program to modify the tables and export the table in CSV format. While we don't strictly follow full CSV rules, as long as you avoid embedded quotes and embedded return carriage/line feed, it should be safe.
More than one table is allowed per file (to avoid sprawl and help simplify modification/management of tables).
TABLE (PCC File)
TABLE:x
- x is the file to be loaded as a TABLE file
- Limitations: It does not support PRExxx or INCLUDE/EXCLUDE.
Table LST file format
File formatting considerations:
- Trailing commas will not impact any TABLE, and will be ignored
- Blank lines will be ignored. This includes lines with commas but without content. (allows tables to be separated by blank lines or lines of all commas)
- Attempts to use embedded line breaks or embedded quotes may or may not be supported by the parsing system, but certainly aren't supported for purposes of PCGen.
- Lines that have the first cell starting with "#" are considered comment lines and will be ignored. This will likely need to include if the first cell is escaped in quotes, just for protection from any tools that quote by default.
- Since the CSV format ignores leading/trailing spaces, one can easily have a spaced out version that is a table in a text editor if folks don't want to edit in a spreadsheet. (Having someone write a "PrettyTable" ... a trivial perl or python program to do that for the data team seems fairly easy)
A TABLE starts with STARTTABLE:
STARTTABLE:x
- x is the name of the Table. Most characters are allowed, avoiding quotes is advisable.
- Allowing Trailing commas allows the STARTTABLE lines to have blank cells where the rest are the table contents... we don't want to complain on minor things that may tools will do.
If a STARTTABLE is encountered when another Table is active, an LST load error will occur.
A TABLE ends with ENDTABLE:
ENDTABLE:x
- x is the name of the Table. Most characters are allowed, avoiding quotes is advisable.
- Allowing Trailing commas allows the ENDTABLE lines to have blank cells where the rest are the table contents... we don't want to complain on minor things that may tools will do.
If an ENDTABLE is encountered without a STARTTABLE, an LST load error will occur.
A Table must have a minimum of 3 rows, NOT counting the STARTTABLE and ENDTABLE tokens.
The First row:
x,x
- x is the column name. These are always the first line of the table after STARTTABLE:
Any column which has data MUST have a name.
The Second row:
y,y
- y is the format for the column. Any column that was named must have a FORMAT.
Additional rows:
z,z
- z represents contents of the table. These must be in the FORMAT provided for the appropriate column.
Example
STARTTABLE:Carrying Capacity, Strength,Capacity NUMBER,NUMBER 1,10 2,20 ... 29,1400 ENDTABLE:Carrying Capacity,
TABLE[x] Format
When a TABLE is created, it implicitly picks up a FORMAT based on the FORMAT of the first column of the Table. Unlike basic FORMATs shown above, TABLE has a SUBFORMAT (in brackets).
TABLE[x]
- x is the SUB-FORMAT of the TABLE (this is the format of the first column of the Table)
- x must be an otherwise valid FORMAT, and it is advised to avoid any FORMAT that has a SUB-FORMAT
- This limitation may be inconsistently enforced. It certainly is prohibited to create a TABLE[TABLE[x]], but other situations the behavior may not be prohibited by the code (but the behavior is also not guaranteed)
- x must be an otherwise valid FORMAT, and it is advised to avoid any FORMAT that has a SUB-FORMAT
COLUMN[x] Format
When a Column needs to be referred to in the data, it has a special format as well:
Unlike basic FORMATs shown above, COLUMN has a SUBFORMAT (in brackets).
COLUMN[x]
- x is the SUB-FORMAT of the COLUMN (this is the format of the data that appears in that column in a Table)
- x must be an otherwise valid FORMAT, and it is advised to avoid any FORMAT that has a SUB-FORMAT
- This limitation may be inconsistently enforced. Such behavior may not be prohibited by the code (but avoiding bad behavior is also not guaranteed in a situation where embedded brackets occur)
- x must be an otherwise valid FORMAT, and it is advised to avoid any FORMAT that has a SUB-FORMAT
Using Tables
With MODIFY, a TABLE[x] variable doesn't look any different. It's still:
MODIFY:TableNameVar|SET|"SomeTable"
"SomeTable" is still a string to a naive reader, but what the system will do at that point is make sure that SomeTable can actually be interpreted as a TABLE[x]... rather than just treating it as a String. Similar for column names when they are encountered (see below).
In that way, the sub variable will allow multiple tables to be swapped in, AS LONG AS they are the right format. If the format is not compatible, then all bets are off and you'll get an error.
lookup() Function
The major consideration is how to do the lookup in data. We can borrow some inspiration from things like Excel and Google Docs:
lookup(X,Y,Z)
- X is a table. This must resolve in the formula system to a valid table. For now, a get(...) function may be required.
- Y is the lookup value in the table. It must match the format of the first column in the table. At this time, it does an EXACT match.
- Z is the target column. This must resolve in the formula system to a valid table column. It must appear in the table named by the contents of the first argument of lookup.
Example: Max load calculation would be something like:
lookup("Carrying Capacity",floor(StrScore),"Capacity")*SizeMult
We have to do the floor due to the exact name of the lookup
Note: The format of both TABLE "Carrying Capacity" and COLUMN "Capacity" can be derived by PCGen internally and thus are no longer required to have a get (current as of Jan 30, 2018)
Note that this function allows some runtime errors, but catches a lot of possible issues. It is still possible to do a lookup on a Table that doesn't contain a specific column of the given name or format, simply because those items can be variables resolved at runtime. For example, if the third variable is COLUMN[NUMBER], we can check at load that the TABLEFORMAT has columns of FORMAT=NUMBER, but we can't guarantee the variable will resolve to a name actually in that table... that will have to wait for a runtime error. That's life, but it does allow us to know the formats at load time, so we do get a lot of error checking in the context of the usage, if not the exact table and column names.
Note you can do some really dynamic behaviors to have folks override game mode behaviors. For example, one neat item would be that we could alter a query for dice steps to extract the table name into the Global Modifier file:
MODIFY:DiceStepTable|SET|"Dice Step" lookup(get("TABLE[NUMBER]",DiceStepTable),BaseDamage,get("COLUMN[NUMBER]",lookup("Step Column",CurrentSize-BaseSize,"Name")))
Note: In both cases here, the "get" is shown for completeness. In the case of DiceStepTable - it MAY be required or prohibited - it depends on what the Format is of the "DiceStepTable" variable. If it's TABLE, then the get is not necessary (and is actually prohibited). The example shown above assumes DiceStepTable is a String. The better design would be for it to be a TABLE (specifically TABLE[NUMBER]). For the "get" after the lookup, that is required - the lookup value will return a STRING, and as such, will needs to be converted to a COLUMN.
Now, if someone needs different dice steps for some reason... they can override the DiceStepTable variable and it will look up in a different table... so expansions could truly be expansions and not have to reach back into core PCC files in order to change key behaviors.
Consider also that spell tables can be shared across classes, or if a subclass alters the default, just create a new table rather than having to BONUS/MODIFY all the numbers in a table with adding and subtracting...
To be completed
- dropIntoContext
- Channels
Material Changes
lookup function simplification
Effective January 30, 2018, tables and columns can be referred to directly in lookup(). This is actually a "broad" change to how strings are interpreted. The system - when possible - fully asserts a FORMAT, and first attempts to convert the given String per the rules of that FORMAT. This also means an assignment of an ALIGNMENT, by key, will not require a "get":
MODIFY:Alignment|SET|"LE"
...will be fully legal. (This assumes Alignment is FORMAT: of ALIGNMENT)