Script:APILogic
From Roll20 Wiki
APILogic
APILogic introduces logical structures (things like IF, ELSEIF, and ELSE) as well as real-time inline math operations, and variable muling to Roll20 command lines. It can test sets of conditions and, depending on the result, include or exclude parts of the command line that actually reaches the chat processor.
For example, given the statement:
!somescript {& if a = a} true stuff {& else} default stuff {& end}
…results in the following reaching the chat:
!somescript true stuff
- Discussion: APILogic thread(Forum)
- File Location: APILogic.js (submitted to the one-click, but until then, find it in my personal repo)
- Script Dependency: libInline.js (also submitted to the one-click)
APILogic exploits a peculiarity of the way many of the scripts reach the chat interface (a peculiarity first discovered by – who else? – The Aaron) to give it the ability to intercept the chat message before it reaches other scripts, no matter if it is installed before or after them in the script library. It also uses a separate bit of script magic to let it retain ownership of the message even when otherwise asynchronous chat calls would be going.
Caveat: The method APILogic utilizes has been tested and shown to work with a large number of scripts. If you find that it doesn’t, you should be reminded that the most foolproof way to ensure proper timing of script execution is to load APILogic in your script library before the other script. But hopefully you’ll find that you don’t need to do that!)
Also, although it requires the API, it is not only for API messages. You can use these logic structures with basic chat messages, too. This document will show you how.
Credits: Created by timmaugh. Many thanks to The Aaron for lending his expertise on questions I had, and to the other members of the House of Mod for sounding out and working through ideas.
Contents |
Triggering and Usage
You won’t invoke APILogic directly by using a particular handle and a line dedicated for the APILogic to detect. Instead, any API call (beginning with an exclamation point: ‘!’) that also includes any IF, DEFINE, MULE, EVAL, EVAL-, or MATH tag somewhere in the line will trigger APILogic to examine and parse the message before handing it off to other scripts.
As mentioned, you are not limited to using APILogic only for calls that are intended for other scripts. There are mechanisms built into the logic that let you output a simple chat message (no API) once you’ve processed all of the logic structures. That means you can use the logic structures in a simple message that was never intended to be picked up by a script, and also in a message that, depending on the conditions provided, might need to be picked up by another script, or alternatively flattened to a simple message to hit the chat log.
The Basic Structures: IF, ELSEIF, ELSE, and END
An IF begins a logical test, providing conditions that are evaluated. It can be followed by any number of ELSEIF tags, followed by zero or 1 ELSE tag. Finally, an IF block must be terminated with an END tag. Each of these are identified by the {& type ... } formation. For instance:
{& if … }
{& elseif …}
{& else}
{& end}
A properly structured IF block might look like this:
{& if (conditions) } true text {& elseif (conditions) } alt text {& else } default text {& end}
Each IF and ELSEIF tag include conditions to be evaluated (discussed in a moment). If an IF’s conditions evaluate as true, the subsequent text is included in the command line. While nested IF blocks embedded within that included text are detected and evaluated, no further sibling tags to the initial IF tag are evaluated until the associated END tag. On the other hand, if an IF evaluates to false, evaluation moves to the next logical structure (ELSEIF, ELSE, or END). ELSEIFs are evaluated just as IFs are, with processing passing forward if we find a false set of conditions. If we ever reach an ELSE, that text is included.
Nesting IF Blocks
You can nest IF blocks in other portions of the line to prompt a new set of evaluation for the enclosed text. They can occur in another IF, in an ELSEIF, or in an ELSE. If the outer logic structure passes validation so that the contents are evaluated, the nested IF block will be evaluated. Each IF must have an END, therefore the first END to follow the last IF belongs to that IF. Similarly, all ELSEIF and ELSE tags that follow an IF (until an END is detected) belong to that IF.
{& if } ... {& elseif } ... {& if } ... {& elseif } ... {& end } ... {& else } ... {& end}
In the example, an IF-ELSEIF-END block exists in the first ELSEIF of the outer IF block. it will only be evaluated (and can only be included) if the outer IF block fails validation and the ELSEIF passes.
Conditions
Each IF and ELSEIF must have at least one condition to evaluate. A condition can be either binary (i.e., a = b), or unary (i.e., c), and each element of a condition (the a, b, or c) can be a sheet item, text, inline roll, or a previously evaluated condition set (more on this in a moment).
Logical Comparisons
The following logical comparisons are allowed for comparing two items (binary operations):
a = b // equals a != b // does not equal a > b // is greater than a >= b // is greater than or equal a < b // is less than a <= b // is less than or equal a ~ b // includes a !~ b // does not include
Sheet Items
The specific ability to retrieve sheet items was removed from APILogic and rolled into the Fetch script (also part of the Meta Toolbox). Use a Fetch construction to retrieve the sheet item data before APILogic evaluates it as part of condition.
Text as Condition
If you need to include space in a bit of text to include as one side of a comparison operation, you should enclose the entire text string in either single quotes, double quotes, or tick marks. With three options available, you should have an option available even if the text you need to include might, itself have an instance of one of those characters. For instance, the following would not evaluate properly, because of the presence of the apostrophe in the word “don’t”:
@(Bob the Slayer.slogan) ~ 'don't go'
Instead, wrap it in another option for denoting the text:
@(Bob the Slayer.slogan) ~ "don't go"
This is good to remember if you intend to use Roll20 parsing to retrieve something. For instance, if you want to use the name of the character associated with the Selected token as a condition, you should wrap that in some form of quotes if there is a chance that name will include a space.
"@{selected|token_name}" ~ Slayer // will reach the API after Roll20 parsing as... "Bob the Slayer" ~ Slayer
Chaining Conditions (AND/OR) and Grouping
Multiple conditions can be used for each IF or ELSEIF tag. Use && to denote and AND case, and use || to denote an OR case.
{& if a = b && c = d }
Conditions are evaluated left to right by default. Use parentheses to enclose groups to force those conditions to evaluate as a group before being compared to sibling conditions:
{& if a = b && ( c = d || e != f) }
Multiple levels of grouping can be used, provided each sibling element (whether group or condition) is connected with && or ||:
{& if ( a = b && ( c = d || e != f ) ) || ( d > b && g ) }
Naming and Reusing Groups
The reasoning behind why you would want to include a part of your command line might be needed at several times in your command line. In that case, you should name your condition group so that you can simply refer to that name later. Name a group by including a bracketed word (no space) after the opening parentheses, before any non-whitespace character:
{& if ([sanitycheck] @|Bob the Slayer|sanity > 10 ) }
The above would store the result of the condition (whether Bob the Slayer’s sanity was over 10) as sanitycheck, available to be used later in the command line, including in future, deferred processing (disucssed later).
{& if ([sanitycheck] @|Bob the Slayer|sanity > 10 ) } conditionally included text {& end} always included text {& if sanitycheck } conditionally included text {& end}
BE AWARE that conditions are ONLY evaluated for IF and ELSEIF tags which reach the parser. If your group is defined in a portion of the command line that is not evaluated because the IF or ELSEIF was never reached, the group will never be evaluated and the test will never be stored.
{& if a = !a } true case text {& if ([sanitycheck] @|Bob the Slayer|sanity > 10 ) } true case for nested if {& end } {& end } always included text {& if sanitycheck} ...{& end}
In that case, the first condition a = !a does not pass validation, so the subsequent text is never evaluated (including the IF tag where the sanitycheck is defined).
If you find yourself in this position, you can either investigate a definition (see Using DEFINE Tag, below), or using a root-level IF tag with a single space of dependent text (that is, providing little alteration to your command line, regardless of if it passes).
!somescript {& if ([sanitycheck] @|Bob the Slayer|sanity > 10 ) } {& end} ...
Because the END tag follows nearly immediately on the IF tag, no important text is included or excluded from the command line, no matter the result of the test. The IF tag is there simply to force the group to be evaluated and the result stored. This would be a better solution than using a definition if the group was particularly complex since the definition is a simple text replacement operation. Using a named group ensures that the group is only being retrieved and evaluated once (read more in Using DEFINE Tag).
Negation
Negation can be applied to any element of a condition or to any group by use of the ! character. This can be handy to test for the non-existence of a sheet item:
!@|Bob the Slayer|weaponsmith
…or to reverse the evaluation of a group:
!( @|Bob the Slayer|weaponsmith > 4 && @|Bob the Slayer|impromptu_poetry > 2 )
…or to get the opposite result from a named group:
{& if !sanitycheck }
Note that if you use negation at the same time you are naming a group, the group will evaluate and the result will be stored as with the name. Negation will then return the opposite of the stored value:
! ( [sanitycheck] a = a )
…will store true as the value of the sanitycheck group, but return false because of the negation. Referring to the sanitycheck group later will retrieve the initial true value.
Using DEFINE Tag
A DEFINE tag is a way to provide definitions for terms that you will then use in text replacements throughout your command line. A DEFINE tag can come anywhere in your command line, and is parsed out before any processing of logical constructs occurs. A DEFINE tag is structured like this:
{& define ([term1] definition1) ([term2] definition2) ... }
The term refers to what you will use, elsewhere in the command line, to represent the definition. Since the definition is terminated by a parentheses, you do NOT need to enclose it in some form of quotation marks UNLESS you need to include leading or trailing spaces.
Since DEFINE replacements are simple text replacement operations, these can be a way to save typing (providing a short term to represent a long definition that will need to be utilized a number of times in a command line). It also provides a way of minimizing work should a definition need to change – giving you only one place to change it instead of many. This means you could define a ([speaker] Bob the Slayer) term, and use speaker anywhere you would refer to the character; then, if you passed that macro language to a fellow player, they would only have to replace the name with their character in one place.
Difference Between Definition and Named Group
As mentioned in the section on using named groups, although the syntax for defining a term is very similar to naming a group, the two structures are different. If you placed the entirety of a group as a definition, you would be replicating that text anywhere you referenced the associated term, but each time that text was encountered, the group would be evaluated anew. Declaring the name for the group in an IF tag, where that name represents a set of conditions, ensures that those conditions are only evaluated once.
Advanced Usage and Tricks
Defining Inline Rolls
Knowing which inline roll marker (i.e., $0) refers to which inline roll can sometimes be confusing, especially for rolls containing rolls in a message that has other rolls containing rolls, or for branches of the logic that don’t exists anymore, or where you deferred an inline roll with escape characters in part of the command line that was never processed.
A rough description is that Roll20 processes (and numbers) the rolls from innermost-leftmost to outermost-rightmost. Like I said, that can be confusing, especially when you add in APILogic letting you nest roll markers. In that case, the second pass of roll indices will start where the first pass left off, from innermost-leftmost to outermost-rightmost.
You can shortcut having to parse all of that by using an inline roll in a DEFINE tag definition, at the proper level of escape for when the roll should process. That way, when the roll in this definition resolves:
{& define ([chaosdice] [[2d10!]] ) }
…the chaosdice term will be filled with the appropriate roll marker definition. In effect, APILogic sees:
{& define ([chaosdice] $[[0]] ) }
…and uses that definition anywhere it sees the chaosdice term else where in the command line.
Table Result Recursion
The normal process of replacing a roll with its value (for instance with the .value command or by nesting it in a deferred inline roll) will return the table entry for a rollable table. Obviously, you may need to verify through straight input into the chat that the roll you enter will return a table entry instead of a number. For instance, [[ 1t[Armor] ]] will return the item from the table, while [[ 2t[Armor] ]] will return the result of only the first roll against the table, and [[ 1t[Armor] + 2 ]] will return 2.
This, paired with the fact that APILogic searches for newly-formed inline roll formations during each pass of the parsing, means you can leverage this behavior to handle recursive rolls based on table entries. If the Armor table has entries of:
[[1d10]] [[1d10+3]] [[2d10r<2-3]] [[2d10r<2]]
…and the following text is encountered in your command line:
[[ 1t[Armor] ]].value
Then whatever result is obtained from rolling against that table will insert another inline roll into the command line, which will be detected and rolled by the Roll20 quantum roller.
Be aware that the resulting roll will, itself be wrapped in an inline roll marker ($0), so if you need to obtain the value from it (and it is not nested in another deferred inline roll) you will need another .value. Obviously, if that value is another table entry, and the table entry points to another inline roll, the process will continue.
Conditional non-API Calls (Basic Chat Messages)
If you want to just want to leverage logical structures for your simple chat message, so you don’t want to end up with an API call at all, put a SIMPLE tag outside of any logical structure (in text that will always be included in the final, reconstructed command line). In that case, simply begin your message (or your first IF or DEFINE tag) after the !.
!{& if @|Bob the Slayer|slogan}@\{Bob the Slayer|slogan}{& end} For tomorrow we dine in hell.{& simple}
If Bob the Slayer has a slogan, this will include that and tack on the extra. Otherwise, it would just output the last portion as a simple chat message.
Obfuscating the API Handle
Since we are interrupting other scripts answering the API message and reconstructing the command line that they see, we can preempt the API handle that would trigger another script to pick up the message. We did this, above, when we had a SIMPLE tag embedded in an IF construct. If we are going to drop the resulting message to a simple chat statement, we probably wouldn’t want the API handle to some other script to be included, so we make its inclusion dependent on the result of some conditions.
!{& if @|Bob the Slayer|smooth_jazz}somescript arg1 arg2{&else} Sorry, speaker doesn't have the smooth_jazz attribute{&simple}{&end}
If smooth_jazz exists for Bob the Slayer, the above example will run the somescript script. If it does not exist, the api handle for that script is dropped, but the {& simple} tag is included, ensuring that a readable message is sent to the chat window.
Mules as Static Access Tables
Rollable tables on Roll20 do a lot to provide random results from weighted entries, which can be good for things like random encounters or the like, but which aren’t as helpful for times when you know the value from which you need to derive the result. For instance, if a given level of a character’s Stamina has a direct correlation to a mod applied to their activities, you don’t need a randomized result… you need the result directly tied to what the character’s Stamina is when you consult the table. Similarly, some systems have charts built for how rolls map an attack roll to damage. A Mule can fill this gap.
Construct your Mule as the entries of the table, with the various states of the referenced input as the variable names. For an Encumbrance Mod table that would return a modifier to rolls based on the weight of the items the character was carrying, that might look like:
0=0 1=0 2=-1 3=-1 4=-1 5=-2 ...etc...
If the Mule were named “EncumbranceTable”, you can reference that using the character’s CarryWeight attribute like so:
... {& mule EncumbranceTable} ... get.@{selected|CarryWeight} ...
Using a Mule this way, you can also leverage a MATH tag, if the input number needs to be altered at all:
get\.{& math @{selected|CarryWeight} + 2*(20-@{selected|Stamina}) }
The above adds twice the value that the character’s Stamina is below 20 to the CarryWeight before determining which row to retrieve. Also note that since MATH tags are evaluated after get statements, the get had to be deferred for one cycle of the loop.
Change Log
Version 2.0.0 - Initial Re-release as part of the ZeroFrame Meta Toolbox
Related Pages
- API:Script Index Other avaiable APIs
- libInline(Forum) - provides an easy interface for inlinerolls
- InsertArg(Forum) -- script preamp, siege engine, menu maker, command line constructor
- SelectManager(Forum) -- Utility to Preserve the Selected Tokens For API-Generated Calls
- Complete Guide to Rolls & Macros