Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

[Meta Script] - Fetch v2.0, now with status, player, page, marker, and campaign support

1663729849

Edited 1670339573
timmaugh
Pro
API Scripter
Fetch (Updated Thread) FILE LOCATION:   My Personal Repo  and 1-click (as of 22 Nov 2022 merge) META SCRIPT:  This script is a meta-script, and part of the  ZeroFrame Meta Toolbox . It can be used on its own, or in conjunction with ZeroFrame and the other meta-toolbox scripts, to modify  other  scripts' API commands. ABSTRACT:  Fetch provides a standardized way to retrieve game data with a syntax very close to Roll20 Galactic Standard. This can be game data not previously exposed to the chat interface  (like the current side of a multi-sided token) , or even data that has been available, but now with the ability to arrive at it in different ways (like getting character properties from the speaker, or token properties from the tracker, or character abilities from the selected token). In addition, Fetch improves on the standard Roll20 sheet retrieval syntax by allowing you to specify a default value if the item does not exist (instead of erroring to chat).  Original forum post is here . SUPPORTING SCRIPTS : Fetch requires The Aaron's libTokenMarkers script as well as timmaugh's Messenger script, all of which are available in the 1-click installer. When, Where, and Why Fetch Fetch seeks to mimic what Roll20 offers in terms of retrieval syntax, and also to expand upon it. As a metascript, it doesn't preempt your command line from reaching the intended target (another Mod Script), it simply detects things to do along the way. It modifies the command line by fetching the data you ask for from the game, then lets the message travel on to be picked up by the intended destination script. In other words, it functions quite similarly to the way you would use standard Roll20 formations, except that it has several key benefits, and reasons/times to use it over Roll20 standard syntax: Fetch offers a default value: instead of breaking the chat input if you request something that doesn't exist, Fetch will substitute in a default value. If you do not provide a default value, the default-default is an empty string. See  Default Values  for more info. Fetch can retrieve more things than Roll20 standard: Fetch can retrieve many more properties from many more things within a game. For instance, it can retrieve the top or left properties from a token, unavailable using standard Roll20 syntax. It can retrieve status values, page names, player properties, and much more. Fetch resolves after Roll20 query parsing and ability expansion: This one can get technical, but... remembering that Roll20 queries can break if certain HTML substitutions are not followed, Fetch offers a way to skip any substitutions. Since Fetch doesn't resolve until after Roll20 parsing, it doesn't expand its syntax into the retrieved information until after the queries have been parsed. Fetch works with other metascripts: Because Fetch is part of the metascript toolbox, it plays well with the other metascripts, allowing you to do things like iterate over selected tokens and retrieve individual data for each. What You Could Always Get (v1.x.x) In previous versions, you could get token properties, character properties, character attributes, character repeating attributes, character abilities, and macros. You could also use tracker references to get information about a token from the Turn Tracker, including using a positive or negative offset to get information from other tokens in the turn structure. If you were speaking as a character, you could also use speaker constructions to return character information (properties, attributes, repeating attributes, or abilities). What New Things You Can Get (v2.0.0) As of v2.0.0, you can get all of the above structures with the same syntax as you ever could, but you can also get player properties, page properties, campaign properties, marker properties, and token status properties (is the status assigned to this token? what is the value? what is the string of all the values together? what is the total of the values for all of the markers of a given type?) --  and return that information to your command line to use . That means you could stash any sort of data in token markers and retrieve it as parameters to other scripts, or to inline rolls (with ZeroFrame installed, too). The list of pseudo properties has also been expanded. Pseudo-Property:  A pseudo-property is a property that isn't attached to the object by default, but which has only one potential value, so it can be assigned to a retrievable handle. For instance, if a token represents a character, it stores the ID of that character in the  represents  property. Since there is only 1 potential value for that character's name (and many times that might be more helpful to know), Fetch also offers a  represents_name  property (a  pseudo -property). Fetch 2.0.0 expands the set of pseudo-properties, especially around the idea of status markers. See the sections  Pseudo-Properties  and/or  Token Status Properties  for more information. Finally, if you have the checkLightLevel installed, Fetch will incorporate that script to provide two additional pseudo-properties (one representing if the token is in bright light, and one reporting the total light hitting the token).        (...continued-->)
1663729965

Edited 1663774107
timmaugh
Pro
API Scripter
Syntax Structures Token Properties You can get token properties (like top, left, rotation, etc.) with many formations. FROM | FORMATION ========================================= SELECTED | @(selected.top) TRACKER |  @(tracker.left) TOKEN ID |  @(-NBx25tztsMa_RL6Ioog.rotation) TOKEN NAME | @(YagrothMook1.pageid) For a fuller discussion of the options around using the tracker, see the section  Using the Tracker . Status markers on a token have their own special Fetch retrieval syntax. See the section  Token Status Properties . Tokens also make several pseudo-properties available to you, for instance offering not only the standard  @(selected.represents)  property (which would return a character's ID), but also the  @(selected.represents_name)  so as to get the  name  of the represented character. The full list of token properties is outlined below, and pseudo-properties are discussed more extensively in the section  Pseudo Properties . Character Property All token formations roll up to character references if a token that represents a character is detected and you ask for a character property. FROM | FORMATION ========================================= SELECTED |  @(selected.char_controlledby) TRACKER | @(tracker.char_controlledby) TOKEN ID      |  @(-NBx25tztsMa_RL6Ioog.char_controlledby) TOKEN NAME | @(YagrothMook1.char_controlledby) In addition, you can reference the character directly, by name, character id, or as the speaker (if the speaker is speaking as a character).  FROM | FORMATION ========================================= CHAR NAME |  @(Bob the Bold.character_id) CHAR ID | @(-Mb0sj43Dacc013jd2f2.controlled_by) SPEAKER | @(speaker.character_name) Character Attributes You get attributes with the same  @(...)  formation, except that you reference an attribute name instead of a property. You get the 'current' value by default, though you can specify the 'max' value instead. As with character properties, any token, tracker, or speaker reference that can roll up to a character gives you the access to the character's attributes. FROM | FORMATION ========================================= SELECTED |  @(selected.hp) TRACKER | @(tracker.hp.max) TOKEN ID      |  @(-NBx25tztsMa_RL6Ioog.wisdom_mod) TOKEN NAME | @(YagrothMook1.ac) CHAR NAME | @(Bob the Bold.fire_res) CHAR ID | @(-Mb0sj43Dacc013jd2f2.armor.max) SPEAKER | @(speaker.water_ballooning) Character Repeating Attributes Repeating attributes are special in that they are like rows of a table. Every entry in the list is a row, and every row has fields of data (the columns). Those columns, attributes in their own right, are attached to that row in the list. So each entry in a "spell" list might have a "name" field (an attribute), a "duration" field (another attribute), a "spell_cost" field (another attribute), etc. This arrangement can make it difficult to arrive at the right entry in the repeating list to match what it is you want to do, and quite often you have to have knowledge ahead of time of where the repeating item is in the list ($1, $2, etc.). Worse, sometimes you have to dig through HTML to find a particular identifying bit of information that would let you use that repeating element. Fetch offers ways to arrive at the data more easily, by allowing you to specify patterns of information about the item from the list you are looking for. For instance, if you are looking for a spell, you might already know you're looking for something on the repeating list "spells". The spell might be called "Dangle Puncher". Fetch lets you refer to the repeating item by that name, then trigger the associated "action" attribute/field. If that field were named  spell_attack , that might look like: *(selected.spells.[name='Dangle Puncher'].spell_attack) For a fuller discussion of repeating attribute retrieval (including getting the name of the attribute, the row number, etc.), see the  original Fetch article . For now, here are a few syntax formations just as examples:  *(Bob.spells.[name="Glitter Storm" prepared].spelllevel) *(Bob.spells.[name~Glitter prepared].spelllevel.name) *(Bob.spells.[name~Storm name!~Glitter].spelllevel.rowid) *(Bob.spells.[name~Glitter prepared].spelllevel.row$) *(Bob.spells.[name~Glitter prepared].spelllevel.name$) Repeating attributes are exactly like other character attributes in that any token reference that rolls up to a character ( selected  ,  speaker  ,  tracker  , etc.) can be used as a character reference (for Fetch 2.0.0, the  tracker  syntax is newly available for repeating elements): FROM | FORMATION ========================================= SELECTED | *(selected.spells.[name="Glitter Storm" prepared].spelllevel) TRACKER | *(tracker+1.spells.[name~Glitter prepared].spelllevel.name) SPEAKER | *(speaker.spells.[name~Storm name!~Glitter].spelllevel.rowid) CHAR/TOK ID | *(-N2f0san129n29oos.spells.[name~Glitter prepared].spelllevel.row$) CHAR/TOK NAME | *(Bob.spells.[name~Glitter prepared].spelllevel.name$) Character Abilities Character ability retrieval functions just as character attribute retrieval (including all of the various ways to reference a character), except that, like Roll20 standard syntax, you use the % character rather than the @ character: %(selected.Charcuterie) For Fetch 2.0.0, the  selected  syntax is newly available for character abilities. Macros Macros are a direct reflection of Roll20 syntax, except that they use parentheses: #(MakePotatoSalad)
1663729989

Edited 1663777899
timmaugh
Pro
API Scripter
Player Properties New to v2.0.0 Player properties can be retrieved with syntax very similar to character/token properties. Fetch syntax for player property retrieval starts with  @(player.  followed by the player identifier, the property you are looking for, as well as any default value you want to include (see  Default Values ), and a closing paren. @(player.timmaugh.online) @(player.timmaugh.display_name) @(player.-M123456789abcdef.userid) The one exception to this formation is using the  speaker  syntax to get player information. If a player is speaking as themselves, use the standard  speaker  syntax (no need for the player identifier), and include the property you wish to retrieve: @(speaker.userid) @(speaker.playerid) Campaign Properties New to v2.0.0 Use similar syntax to get campaign properties, except using  campaign  instead of  player  . Also, since there is only one campaign object, you need not supply an identifier. @(campaign.page_name) Certain properties (like markers, playerspecificpages, and turnorder) are available, here, but have better handles for their data. These properties, coming from the Campaign, will contain all of the information in them, which may not be what you want. Marker Properties New to v2.0.0 Fetch refers to markers as the set of token markers installed for a particular game. These have a name, a tag, html for presentation, and sometimes a url. Markers are separate from "statuses" in that statuses refer to token markers that may or may not be assigned to a particular token. Think of markers as being derived from the Campaign, and statuses derived from the token. To retrieve a property from a given marker, use syntax as for a player, except swapping in  marker  for  player  : @(marker.Staggered.html) @(marker.blue.html) @(marker.sugar-plum.tag) If no property is supplied, Fetch assumes you are looking for the html associated with that marker, so these formations are functionally equivalent: @(marker.Staggered.html) @(marker.Staggered) A Note on URLs and HTML Some markers, like the colored circles, are accomplished using HTML and CSS only. There is no 'image' implemented from which to provide you a URL. That is when you want to use the  html  property for any presentation. Token Status Properties Statuses refer to token markers that have been assigned to a given token. They bear all of the properties of a marker, except that they also have other properties (like a  value  property)  that make them quite flexible for reporting data from a token. Use any of the methods for identifying a token (selected, tracker, id, name, etc.), followed by the keyword  status , the status to inquire about, and the property to return: @(selected.status.broken-skull.html) If you don't specify a property to return, Fetch will return the   value  property: @(selected.status.broken-skull) If there is a value connected to this status on the token, it will be returned to your command line. If there is no value, an empty string will be returned and  no default value  will be swapped in (the value is there, it just happens to be an empty string). If you only wish to test whether the token bears that status, use the    is  pseudo-property, instead (see below). See  Default Values  for more information on providing default values. is (pseudo-property) Fetch status returns offer a pseudo-property of being able to tell if that token has the given status (  is  ). This would be the equivalent of asking "is the token staggered?": @(selected.status.Staggered.is) count (pseudo-property) Players can only assign one instance of a given status to a particular token, but Mod Scripts can apply multiple instances (see TokenMod documentation for a method of doing this). In other words, you can have a situation where a Mod Script has assigned multiple blue circles to the same token. If you need to know the number of these, use the    count  pseudo-property: @(selected.status.blue.count) ? (the 'which' switch) By default, a Fetch status retrieval looks for the first instance of that status on the token. The token pictured above has two blue circle statuses assigned to it, the first having a value of 2 and the second a value of 7. For that token, the following formation will return a value of 2: @(selected.status.blue) To designate a different instance to reference, use a question mark.  The following will retrieve a value from the second blue circle (returning 7): @(selected.status.blue?2) Use the  ?all  selector to string together all the values from the instances of this status on this token. For the token with two blue circles (at 2 and 7), this would return 27: @(selected.status.blue?all) Use the   ?all+  selector  to  total  all values from the instances of this status on this token. For the same token, this would return 9: @(selected.status.blue?all+) Using is/count With ? When you have used a question mark to designate a particular instance of the status, you change how   is  and    count   behave. Since you have only designated a single instance of the status,    count  will always return either a 0 or a 1. @(selected.status.blue?2.count) There is only a single status that can be the second instance of the blue status marker, so if it is there, this will return 1. If it is missing, it will return 0. Similarly, the    is  pseudo-property will now refer specifically to the designated instance of the status. In other words, this formation essentially asks, "is the token marked staggered at least twice?": @(selected.status.Staggered?2.is) Pseudo Properties As mentioned, a  pseudo-property is a property that isn't attached to the object by default, but which has only one potential value, so it can be assigned to a retrievable handle. These come in handy as ways to expand way you can use Fetch structures to retrieve data. There isn't anything specific about pseudo-properties that you need to know other than that there are more properties above/beyond what are typically available to a Roll20 object, so if you need a particular datapoint, check the property lists. It may already be there. Here are some specific examples of pseudo-properties: TOKEN PSEUDO-PROPERTY ======================================================= NORMAL:     @(selected.represents)       // returns: -M123456789abcdef PSEUDO:     @(selected.represents_name)  // returns: Bob the Bold NORMAL:     @(selected.page_id)          // returns: -M0a3456789abcoog PSEUDO:     @(selected.page_name)        // returns: Fortress Keep NORMAL: ...tracker info unavailable from token... PSEUDO: @(selected.tracker) // returns the tracker initiative value for this token PSEUDO: @(selected.tracker_offset) // returns the number of turns until this token is "up" CHARACTER PSEUDO-PROPERTY ==================================================== NORMAL:     @(Bob the Bold.char_controlledby) // returns: -Mjsa0vbn209as8sg PSEUDO:     @(Bob the Bold.char_controlledby_name) // returns: FlapjackeryMcGee Token Statuses as Pseudo-Properties Token Statuses are unavailable as Roll20 constructs to begin with, and so in a way are wholly made up of pseudo-properties. Being able to total all of the values for a given status marker (blue@2 + blue@7 = 9), or to chain them into a larger number (blue@2 + blue@7 = 27), or to detect if a given status is assigned to a token at all, or whether it is assigned some number of times, or to return the value of a particular instance of a given status marker... all of that gives you new ways to utilize status markers, especially if you consider using other metascripts (i.e., using APILogic you could effectively construct a command line that take actions based on conditions defined by the status: "if the token is marked 'staggered', take one action, otherwise take a different action"). Read more on token statuses in the section Token Status Properties. Read more about other metascripts at the  ZeroFrame Meta Toolbox  thread, or at the  Metascript wiki article , this  article on using metascripts , or  this page of examples .  Using the Tracker Using tracker syntax to designate a token (or, from there, a character), you don't have to return the current token in the turn order. You can refer to tokens in some offset from the current turn. The following construction would refer to the next-token-up after the current turn: @(tracker+1.ac) Similarly, this would refer to the token who acted 2 turns ago: @(tracker-2.ac) If the offset is more than the number of tokens in the turn order, the process loops until we arrive at the token being referred to. GMs Can Use Filters If you are a GM who has run combat that spanned across pages in your game, you know that your Turn Tracker contains entries for all of the tokens in combat, no matter what page they are on (players only see entries for the tokens on the page they, themselves, are viewing). Your offset values therefore might be different from what players would retrieve. To bridge this, Fetch offers 3 filters to the tracker syntax. Insert them, enclosed in brackets, in the tracker syntax prior to the offset. @(tracker[page].ac) @(tracker[ribbon]+1.hp) %(tracker[gm]-1.HealthReport) Page is the default if no filter is requested. Default Values One of the key benefits of a Fetch construction is that it does not break the chat parsing if the thing you have requested is not there. If you request a character attribute using Roll20 syntax and Roll20 doesn't find that attribute on the specified character, it will give you a message in the chat pane loudly announcing that fact. Using a Fetch construction, you can supply a default value and have that substituted in if the thing you are looking for doesn't exist. All Fetch constructions have a standard default of a zero-length string unless/until you provide something else, so if you don't get a return from your Fetch construction, this could be the reason. Default values can be inserted into any Fetch construction. Enclose the default in brackets, and place it directly before the closing paren (i.e., the last thing within the Fetch syntax): @(Bob the Hirsute.AxeJuggling[Untrained]) @(Sev the Fragrant.HygieneMod[0]) @(selected.tracker[20])
1663730047

Edited 1670338782
timmaugh
Pro
API Scripter
Available Properties By Object Type The following tables provide the ways to reference the various returnable things from a Roll20 game. The first column of each table is the thing you can get (a property or a pseudo-property), and the remaining tables are Fetch aliases for that item. For instance, to use the  represents  property of a token, you could use either of the Fetch aliases: @(selected.represents) @(selected.reps) Token Properties TOKEN PROPERTY FETCH WAYS TO REFERENCE _cardid cardid, cid _id tid, token_id _pageid page_id, pageid, pid, token_page_id, token_pageid, token_pid _subtype sub , subtype _type token_type adv_fow_view_distance adv_fow_view_distance aura1_color aura1 , aura1_color aura1_radius aura1_radius , radius1 aura1_square aura1_square , square1 aura2_color aura2 , aura2_color aura2_radius aura2_radius , radius2 aura2_square aura2_square , square2 bar_location bar_location , bar_loc bar1_link bar1_link , link1 bar1_max bar1_max , max1 bar1_value bar1 , bar1_current , bar1_value bar2_link bar2_link , link2 bar2_max bar2_max , max2 bar2_value bar2 , bar2_current , bar2_value bar3_link bar3_link , link3 bar3_max bar3_max , max3 bar3_value bar3 , bar3_current , bar3_value bright_light_distance bright_light_distance compact_bar compact_bar controlledby token_cby , token_controlledby currentSide currentside , curside , side dim_light_opacity dim_light_opacity directional_bright_light_center directional_bright_light_center directional_bright_light_total directional_bright_light_total directional_low_light_center directional_low_light_center directional_low_light_total directional_low_light_total emits_bright_light emits_bright , emits_bright_light emits_low_light emits_low , emits_low_light fliph fliph flipv flipv gmnotes gmnotes has_bright_light_vision has_bright_light_vision has_directional_bright_light has_directional_bright_light has_directional_low_light has_directional_low_light has_limit_field_of_night_vision has_limit_field_of_night_vision has_limit_field_of_vision has_limit_field_of_vision has_night_vision has_night_vision , has_nv , nv_has height height imgsrc imgsrc isdrawing drawing , isdrawing lastmove lastmove layer layer left left light_angle light_angle light_dimradius light_dimradius light_hassight light_hassight light_losangle light_losangle light_multiplier light_multiplier light_otherplayers light_otherplayers light_radius light_radius light_sensitivity_multiplier light_sensitivity_multiplier , light_sensitivity_mult limit_field_of_night_vision_center limit_field_of_night_vision_center limit_field_of_night_vision_total limit_field_of_night_vision_total limit_field_of_vision_center limit_field_of_vision_center limit_field_of_vision_total limit_field_of_vision_total low_light_distance low_light_distance name token_name night_vision_distance night_vision_distance , nv_dist , nv_distance night_vision_effect night_vision_effect , nv_effect night_vision_tint night_vision_tint , nv_tint playersedit_aura1 playersedit_aura1 playersedit_aura2 playersedit_aura2 playersedit_bar1 playersedit_bar1 playersedit_bar2 playersedit_bar2 playersedit_bar3 playersedit_bar3 playersedit_name playersedit_name represents represents , reps rotation rotation show_tooltip show_tooltip showname showname showplayers_aura1 showplayers_aura1 showplayers_aura2 showplayers_aura2 showplayers_bar1 showplayers_bar1 showplayers_bar2 showplayers_bar2 showplayers_bar3 showplayers_bar3 showplayers_name showplayers_name sides sides statusmarkers markers , statusmarkers tint_color tint , tint_color tooltip tooltip top top width width (page name for the pageid) page , page_name (name for linked attr in bar1) bar1_name , name1 (name for linked attr in bar2) bar2_name , name2 (name for linked attr in bar3) bar3_name , name3 (is token in bright light) checklight_isbright (total light on token) checklight_total (controlledby list as names) token_cby_names , token_controlledby_names , token_cby_name , token_controlledby_name (image source without arguments)      imgsrc_short (image as picture) img (last x position, only) lastx (last y position, only) lasty (name for represents character) represents_name , reps_name (tracker value for this token) tracker (turns in tracker until this token) tracker_offset Character Properties CHARACTER PROPERTY FETCH SYNTAX TO REFERENCE _id char_id , character_id _type char_type , character_type archived archived avatar avatar controlledby character_controlledby , character_cbycontrolledby , char_cby , char_controlledby defaulttoken defaulttoken inplayerjournals inplayerjournals name char_name , character_name (name for players in controlledby) character_controlledby_name , character_cby_name , char_cby_name , char_controlledby_name , character_controlledby_names , character_cby_names , char_cby_names , char_controlledby_names (avatar as picture) char_img, character_img (name for players in inplayerjournals)      inplayerjournals_name , inplayerjournals_names
1663730069

Edited 1663774295
timmaugh
Pro
API Scripter
Player Properties PLAYER PROPERTY FETCH SYNTAX TO REFERENCE _d20userid roll20id , roll20_id , r20id , r20_id , userid , user_id _displayname player_name , displayname , display_name _id player_id _lastpage lastpage , last_page _macrobar macrobar _online online _type player_type color color showmacrobar showmacrobar , show_macrobar speakingas speakingas , speaking_as (name for pageid in lastpage)      lastpagename,  last_page_name Page Properties PAGE PROPERTY FETCH SYNTAX TO REFERENCE _id page_id _type page_type _zorder zorder adv_fow_dim_reveals adv_fow_dim_reveals adv_fow_enabled adv_fow_enabled adv_fow_show_grid adv_fow_show_grid archived archived background_color background_color , bg_color daylight_mode_enabled daylight_mode_enabled daylightModeOpacity daylightmodeopacity , daylight_mode_opacity diagonaltype diagonaltype , diagonal_type dynamic_lighting_enabled      dynamic_lighting_enabled explorer_mode explorer_mode fog_opacity fogopacity , fog_opacity force_lighting_refresh force_lighting_refresh grid_opacity gridopacity , grid_opacity grid_type gridtype , grid_type gridcolor gridcolor , grid_color gridlabels grid_labels , gridlabels height height jukeboxtrigger jukeboxtrigger , jukebox_trigger lightenforcelos lightenforcelos lightglobalillum lightglobalillum lightrestrictmove lightrestrictmove lightupdatedrop lightupdatedrop name page_name scale_number scale_number scale_units scale_units showdarkness showdarkness , show_darkness showgrid showgrid , show_grid showlighting showlighting , show_lighting snapping_increment snapping_increment width width Campaign Properties CAMPAIGN PROPERTY FETCH SYNTAX TO REFERENCE _id campaign_id , id _journalfolder journalfolder _jukeboxfolder jukeboxfolder _jukeboxplaylistplaying jukeboxplaylistplaying _token_markers token_markers , markers _type campaign_type , type initiativepage initiativepage playerpageid pageid , page_id , playerpageid , playerpage_id playerspecificpages playerspecificpages turnorder turnorder (name for pageid in playerpageid)      pagename , page_name , playerpagename , playerpage_name Marker Properties MARKER PROPERTY      FETCH SYNTAX TO REFERENCE html html name marker_name tag marker_id , tag url url Status Properties STATUS PROPERTY      FETCH SYNTAX TO REFERENCE count count html html is is name status_name num num , number , value , val tag status_id , tag url url
1663731603
Oosh
Sheet Author
API Scripter
Great balls of fetch, the documentation alone is worth the price of admission.
1663733727
timmaugh
Pro
API Scripter
I broke my keyboard. Also, the documentation has achieved sentience, soooo...
1663772603
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
This is amazing! Documentation is very clear, and holy cow is there power here! I want to start using this.
1663774399
timmaugh
Pro
API Scripter
Thanks, Keith! There's a lot of new stuff that I did my best to test, but if you run into any problems, let me know!