input() and other new b42 macro functions

Doc requests, organization, and submissions

Moderators: dorpond, trevor, Azhrei

Post Reply
knizia.fan
Giant
Posts: 197
Joined: Wed Jul 30, 2008 3:43 pm

input() and other new b42 macro functions

Post by knizia.fan »

New functions: input, abort, list-processing, and string properties
Updated for 1.3b46

Several new functions are available to macros (some added in b42, some added later). I'll discuss how to use them here.

I'm recommending that all GMs add a new token property called "Private" to their campaigns. That property should probably also be added to the default token property list MapTools uses. A big advantage of the new StrProp functions below is that they allow macros to store multiple custom settings in a token without having to ask every GM to add new token properties to their campaign. (See the example Attack macro posted below.) But you still need one token property to be defined, to provide a place to store all those custom settings. To avoid issues caused by having an empty token property, the Private property should be defined with a default value as follows:

Code: Select all

Private: ;
Bug note: MapTool has a couple of redraw bugs, and occasionally the sample macros below will trigger the issue. Long multiline macros such as the ones given here sometimes cause the chat window to go blank and appear to freeze up. Usually just moving the mouse will cause the window to redraw, or you can minimize the MapTool application and then restore it. Occasionally when the macros below display a dialog box, the box will be blank white. The best thing to do in that case is hit Esc to cancel, and run the macro again (the dialog seems to still be functional, it just doesn't redraw itself correctly). I have no idea currently what is causing this.

Abort and assert functions
  • abort(status) - if status is zero, aborts execution and the rest of the macro is skipped
  • assert(condition, message) - if condition is zero, halts execution and prints a custom error message
List functions
These functions operate on a string of the form "item1, item2, item3, ..."
Each of them takes an optional last argument that gives an alternate list separator (the default is ",").
Note that the original list is never modified. If a function alters a list, it does so by returning a new list with the alterations.
  • listGet(list, index) - returns the entry in the index position (first position is index 0)
  • listDelete(list, index) - returns a new list with the item at index position deleted.
  • listCount(list) - returns the number of list entries
  • listFind(list, target) - returns the index of the target string in the list, or -1 if none of the list entries match.
  • listAppend(list, target) - adds target to the end of the list
  • listInsert(list, index, target) - inserts target before the entry at position index
  • listReplace(list, index, target) - replaces the entry at position index with the target
  • listSort(list, sortType) - returns a sorted list. The sortType determines the type of sort to use.
    If sortType is "A", normal alphabetic sorting is used, and "Monster11" comes before "Monster3". (Default behavior)
    If sortType is "N", the first number in each entry is effectively padded to 4 digits, so that "Monster3" comes before "Monster11".
    The sortType can have a second character of "+" or "-" to specify an ascending or descending sort.
  • listContains(list, target) - returns number of occurrences of target in list
  • listFormat(list, listFormat, itemFormat, separator) - returns a custom-formatted version of the list.
    listFormat is a string that is emitted once. It should contain the text "%list", which is replaced with the formatted items.
    itemFormat is emitted once per item. Each instance of "%item" in the string is replaced with the value of the list item.
    separator is emitted in between the formatted items.
    Example (prints items on separate lines):

    Code: Select all

    [listFormat("apple,bear,cat", "BEGIN LIST<br>%list<br>END LIST", "This item is: %item", "<br>")]
String Property functions
These functions operate on a "property string", which contains multiple settings:

Code: Select all

"key1=value1; key2=value2; ..."
An optional last argument gives an alternate entry separator (the default is ";").

To avoid confusion, I'll call each of these settings a "string property", to distinguish them from the "token properties" that already exist in MapTool.

The functions access and modify a property string. Note that the original string is never altered! When a function modifies a property string, it returns a new property string with the modifications.
  • getStrProp(properties, key) - find a key and return the value, or "" if not found.
    You can provide an optional 3rd argument, which will be returned if the key is not found.
  • setStrProp(properties, key, value) - set the value for a key, inserting it if not present. Returns the new property string.
  • deleteStrProp(properties, key) - delete a key. Returns the new property string.
  • countStrProp(properties) - returns how many key/value settings are in the string.
  • indexKeyStrProp(properties, N) - returns the Nth key in the list.
  • indexValueStrProp(properties, N) - returns the Nth value in the list.
  • varsFromStrProp(properties, nameStyle) - creates variables for each key and assigns the corresponding value. Returns the number of assignments made.
    nameStyle can be one of three settings:
    "NONE" - no assignments are made
    "SUFFIXED" - assignments are made to variable names with an underscore appended
    "UNSUFFIXED" - assignments are made to unmodified variable names
    The SUFFIXED option is preferred over UNSUFFIXED, so that you don't unexpectedly overwrite token properties or other variables.
    Use UNSUFFIXED in special cases, such as when you really do intend to overwrite token properties.
  • strPropFromVars(varList, varStyle) - creates a property string from the names and values of the variables listed in the varList string.
    varStyle is either "SUFFIXED" or "UNSUFFIXED". When fetching the values of the variables, the "SUFFIXED" option will look for variables with underscores appended to the given names. The output property string does not contain the underscores.
    Example (returns "c=cow ; a=3 ; b=bob ; "):

    Code: Select all

    [H: props = "a=3 ; b=bob ; c=cow ; "]
    [H: varsFromStrProp(props, "SUFFIXED")] <!-- creates variables a_, b_, c_ -->
    [strPropFromVars("c,a,b", "SUFFIXED")]
  • formatStrProp(props, listFormat, entryFormat, separator) - returns a custom-formatted version of the property string.
    listFormat is a string that is emitted once. It should contain the text "%list", which is replaced with the formatted items.
    entryFormat is emitted once per item. Any instances of "%key" and "%value" in the string are replaced with the key or value for that setting.
    separator is emitted in between the formatted items.
    Example (prints settings in a formatted table):

    Code: Select all

    [h: props = "Strength=14 ; Constitution=8 ; Dexterity=13 ; Intelligence=4 ; Wisdom=18 ; Charisma=9"]
    [formatStrProp(props, "<table border=1>%list</table>", "<tr> <td><b>%key</b></td> <td>%value</td> </tr>", "")]
Examples, assuming the variable "props" is set to "a=3 ; b=bob ; c=cow":
  • getStrProp(props, "b") returns "bob"
  • setStrProp(props, "d", 44) returns "a=3 ; b=bob ; c=cow ; d=44 ; "
  • deleteStrProp(props, "a") returns "b=bob ; c=cow ; "
  • countStrProp(props) returns 3
  • indexKeyStrProp(props, 0) returns "a"
  • indexValueStrProp(props, 0) returns 3
  • varsFromStrProp(props, "SUFFIXED") assigns values to the variables a_, b_, and c_.
  • strPropFromVars("a,c,b", "SUFFIXED") then constructs the property string "a=3 ; c=cow ; b=bob ; "


New function: input()
The input() function allows you to ask the user to input several values at once. The function takes one or more string arguments, one for each variable you want to assign to. It returns 1 if the user clicked the OK button, and 0 if they clicked Cancel (or hit Esc to exit the dialog). If input() returns 0, no variable assignments were made. (You can use the abort() function to cancel the rest of your macro if input() returns 0.)

As a simple example, the following statement

Code: Select all

[input("AtkBonus", "DmgBonus", "CombatAdvantage")]
will create a single popup asking for values for all three variables.
Image

However, more choices are available. For example, a macro can create this dialog box:

Image

Each argument must be a string with the following format:

Code: Select all

"varname | value | prompt | inputType | options"
Only the varname is required. You can leave out unnecessary '|' characters if you don't provide text for all the slots. All settings and options are case-insensitive.
  • varname is the variable name to assign to.
  • value is the initial value present in the dialog box. If not present, it defaults to "0". In some cases you use a special syntax to indicate multiple values.
  • prompt is a friendly prompt that appears next to the input field in the dialog box. If not present, it defaults to the variable name.
  • inputType describes what type of input control to use in the dialog. They are described below.
  • options is a series of option settings in the form OptionName=OptionValue.
You can combine several of these into a single string, separated by "##". For example, [input("a##b")] is equivalent to [input("a", "b")].

There are several user interface choices for each variable. The inputType can be any of the following.
  • TEXT is the default, and yields a text input box. The contents typed in the box are assigned to the variable.
    • Option: WIDTH=nnn -- Sets the width of the text box. (Default WIDTH=16)
  • LIST creates a dropdown list of choices. The variable is assigned the index (starting at 0) of the item that was selected.
    • Option: SELECT=nnn -- Sets the initial selection in the listbox (the first item is number 0). (Default SELECT=0)
    • Option: VALUE=STRING -- The variable is assigned the string contents of the selected item. (Default VALUE=NUMBER)
    • Option: TEXT=FALSE -- No text is shown in the list entries. (Default TEXT=TRUE)
    • Option: ICON=TRUE -- If the string for the item contains an image asset URL, the image is displayed in the entry. Text before the URL is used for the entry's text. The getTokenImage() and getStateImage() functions can be used to obtain asset URLs. (Default ICON=FALSE)
    • Option: ICONSIZE=nnn -- The size of the icon. (Default ICONSIZE=50)
  • CHECK creates a checkbox. The variable is assigned either 0 or 1.
    • RADIO creates a group of radio buttons. This option works much like LIST, returning the index of the choice that was selected.
      • Option: ORIENT=H -- Arranges the radio buttons horizontally on a single line. (Default ORIENT=V)
      • Option: VALUE=STRING and SELECT=nnn -- Same as the LIST options above.
    • LABEL creates a static label. The varname is ignored, and nothing is assigned to it.
      • Options: The TEXT, ICON, and ICONSIZE options work the same way they do for a LIST.
    • PROPS creates a sub-area with multiple text boxes, one for each entry in the "property string" (see below) stored in value. The varname is assigned a new property string containing all the entries with their updated values.
      • Option: SETVARS=SUFFIXED -- Makes a variable assignment for each of the sub-values, with an underscore appended to the name. (Default SETVARS=NONE)
      • Option: SETVARS=UNSUFFIXED -- Makes variable assignments to unmodified variable names. (SUFFIXED is usually preferred, unless you specifically intend to overwrite token properties.)
    • TAB creates a tab for a tabbed dialog box. The varName variable gets assigned a property string containing all the variable assignments made on this tab. Since some of the variables may be property strings themselves, the tab property string uses the non-default separator "##".
      • Option: SELECT=TRUE -- Causes this tab to be displayed when the dialog appears. (Default SELECT=FALSE)
    Every inputType except TAB accepts a SPAN=TRUE option, which causes the prompt label to be hidden and allows the input field to span the width of the dialog. The prompt text is used as a tooltip or other decoration.

    When using the LIST or RADIO options, the value section is a comma-delimited list of options, e.g. "item1, item2, item3". When using the PROPS option, the value section is a "property string" of the form "key1=value1; key2=value2;". Property strings can be created and manipulated using the various StrProp functions (getStrProp(), setStrProp(), etc.).

    Here are some example formats for the parameters to input().
    • [H: input("Bonus")] [Bonus]
      A text value is read and assigned to the variable called Bonus.
    • [H: input("AtkBonus|2|Attack bonus")] [AtkBonus]
      The text box starts with a value of 2, and has a friendly prompt in front of it.
    • [H: input("CA | 1 | Combat Advantage | CHECK")] [CA]
      A checkbox is shown, already checked.
    • [H: input("Spell | Magic missile, Fireball, Wish || LIST |SELECT=1")] [Spell]
      The dialog box will have a listbox with Fireball selected. Note that the prompt section has been skipped. A number from 0 to 2 will be assigned to the variable Spell.
    • [H: input("YourName | Bob , Andy , Joe , Karl | Pick a name | RADIO | orient=h value=string")] [YourName]
      The options are shown as radio buttons on a single line, and the variable YourName will be assigned the text of the chosen option.
    • [H: input("props | Name=Longsword +1 ; Damage=1d8 ; Keyword=fire | Weapon info | PROPS")] [props]
      Three text boxes are created, and the props variable is assigned a new property string with the user's modified values.
    • [H: input("tab0 | Info || TAB", "Name ## Rank ## Serial number | 1,2,3 || LIST",
      "tab1 | Abilities || TAB", "Strength ## Dexterity ## Wisdom"
      )]
    If a token is impersonated, any variable names that match token property names will assign the result to the token property, just as a statement like [HP=3] would. If you want to reference a variable or token property when typing values in the dialog box, you must enclose the name in braces {} as usual.

    Here's the macro that generated the elaborate example above. It assumes that your campaign file has image states called "Dazed", "Immobilised", "Slowed", and "Quarry_yellow". If you want to try it out yourself, download this campaign settings file, and load it in MapTool (Edit > Campaign Properties > Import). If the user clicks OK to the dialog box, the Creature, CA, MainWpn, OffWpn, FlavorText and State variables will contain the user's choices and can be used in further macro steps.

    Code: Select all

    [H: dazestr = "Dazed" + getStateImage("Dazed") + ","]
    [H: immobstr = "Immobilised" + getStateImage("Immobilised") +","]
    [H: slowstr = "Slowed" + getStateImage("Slowed") + ","]
    [H: quarrystr = "Quarry" + getStateImage("Quarry_yellow") + ","]
    
    [H: input(
       "Creature | (None) | Creature to be attacked",
       "CA || Combat Advantage | CHECK",
       "MainWpn | Battleaxe, Longsword, Bastard sword | Select main hand weapon | RADIO | select=1 value=string",
       "OffWpn | (None), Dagger, Short sword | Select off hand weapon | RADIO | orient=h",
       "FlavorText | Conan swings high and strikes his foe.,
                     Conan smashes his enemy with a swift blow.,
                     With desperate fury Conan hacks left and right. | Attack description | LIST",
       "State | " + dazestr + immobstr + slowstr + quarrystr 
                  +"| Select a state to apply to target | LIST | ICON=true iconsize=45 select=2"
    )]
    Testing note: The code does something very naughty in order to work around an issue with the Java user interface libraries. This may cause serious problems, so I'd appreciate your help testing the feature.

    The issue involves the listboxes. Here's an example to give you several lists to test with.

    Code: Select all

    [H: input(
    "a|1,2,3,4,5|Pick a number|LIST",
    "b|aardvark, beagle, cow, deer|Animals|LIST",
    "c|99|Just a text box|TEXT",
    "d|D&D, GURPs, Hero|Games|LIST",
    "e|a,b,c,d,e||LIST",
    "f|2,3,5,7,11,13,15|Primes|LIST")]
    Execute that in the chat window to get a dialog box with a few lists and one text field. Please try the following. I've only tested on Windows, so Mac and Linux testers will be valuable. Let me know if the behavior is not what you expect for your operating system.
    1. Use the Tab key to jump from item to item. Does it take you more than one press of the Tab key to go from one list to the next? If so, how many?
    2. When you use the Tab key, does the highlight stop at each list, or does it skip any of them (making it impossible to select them using the keyboard)?
    3. With one of the lists selected, hit the down arrow key to drop the list down. With the list down, hit the Tab key to jump to the next input field. Does the dropdown list go away as it should, or does it remain visible?
    4. Use the mouse to drop down a list by clicking on the text. When you hit Tab, does the list close immediately and does the selection jump to the next input field?
    5. Use the mouse to drop down a list by clicking on the little arrow. When you hit Tab, does the list close immediately and does the selection jump to the next input field?
    Thanks,
    --k.fan

    EDIT: 15-Sep-2008 Added b43 info for getStrProp()
    EDIT: 16-Sep-2008 Updated settings file provided to solve a problem on Linux
    EDIT: 1-Oct-2008 Updated for b46 changes
    Last edited by knizia.fan on Wed Nov 19, 2008 10:27 pm, edited 4 times in total.

    knizia.fan
    Giant
    Posts: 197
    Joined: Wed Jul 30, 2008 3:43 pm

    Post by knizia.fan »

    "Attack" macro
    Everyone needs an attack macro, so let's take that as a first example. Often there are modifiers or decisions that are different each time you attack, so it'll be useful to have the macro pop up a dialog box where the user can set all those values.

    All of these example macros are in a campaign file that you can get here. Impersonate the token that's on the map, and you can try these macros yourself.

    The campaign file defines some token properties which are used by the macro code. In the example, they are set as follows.
    Token properties wrote:Weapon0 WpnName=Longsword +1 ; WpnAttackBonus=1 ; WpnDamage=1d8+1 ; WpnMaxDamage=9 ; WpnCritDamage=1d6 ; WpnKeyword= ;
    Weapon1 WpnName=Fire dagger +2 ; WpnAttackBonus=2 ; WpnDamage=1d4+2 ; WpnMaxDamage=6 ; WpnCritDamage=2d6 ; WpnKeyword=fire ;
    Weapon2 WpnName=--none-- ; WpnAttackBonus=0 ; WpnDamage=1d4 ; WpnMaxDamage=4 ; WpnCritDamage=1d6 ; WpnKeyword= ;
    Weapon3 WpnName=--none-- ; WpnAttackBonus=0 ; WpnDamage=1d4 ; WpnMaxDamage=4 ; WpnCritDamage=1d6 ; WpnKeyword= ;
    Weapon4 WpnName=--none-- ; WpnAttackBonus=0 ; WpnDamage=1d4 ; WpnMaxDamage=4 ; WpnCritDamage=1d6 ; WpnKeyword= ;
    Weapon5 WpnName=--none-- ; WpnAttackBonus=0 ; WpnDamage=1d4 ; WpnMaxDamage=4 ; WpnCritDamage=1d6 ; WpnKeyword= ;
    Weapon6 WpnName=--none-- ; WpnAttackBonus=0 ; WpnDamage=1d4 ; WpnMaxDamage=4 ; WpnCritDamage=1d6 ; WpnKeyword= ;
    Weapon7 WpnName=--none-- ; WpnAttackBonus=0 ; WpnDamage=1d4 ; WpnMaxDamage=4 ; WpnCritDamage=1d6 ; WpnKeyword= ;
    Weapon8 WpnName=--none-- ; WpnAttackBonus=0 ; WpnDamage=1d4 ; WpnMaxDamage=4 ; WpnCritDamage=1d6 ; WpnKeyword= ;
    Weapon9 WpnName=--none-- ; WpnAttackBonus=0 ; WpnDamage=1d4 ; WpnMaxDamage=4 ; WpnCritDamage=1d6 ; WpnKeyword= ;
    Private DefaultWpn=1 ;
    Here's the macro called "Attack".

    Code: Select all

    <!-- ATTACK MACRO b46 -->
    
    <!-- Build the list of weapon names. -->
      [H: WpnList = ""]
      [H,COUNT(10): WpnList = listAppend(WpnList, getStrProp(eval("Weapon"+roll.count), "WpnName"))]
      
    <!-- Get the index of the previously selected weapon, if any. (Default to 0 if none.) -->
      [H: WpnNum = getStrProp(Private, "DefaultWpn", 0)]
      
    <!-- Prompt the user for attack options. -->
      [H: status = input(
          "WpnNum | " + WpnList + " | Select weapon to use | LIST | SELECT=" + WpnNum,
          "CA | 0 | Combat Advantage | CHECK",
          "MiscAtk | 0 | Misc. attack bonus",
          "MiscDmg | 0 | Misc. damage bonus"
          )]
      [H: abort(status)]
      
    <!-- Save the default weapon for next time. -->
      [H: Private = setStrProp(Private, "DefaultWpn", WpnNum)]
    
    <!-- Set variables from the weapon's property string.
      -- In this case, the variables are WpnName_, WpnAttackBonus_, 
      -- WpnDamage_, WpnMaxDamage_, WpnCritDamage_, and WpnKeyword_. -->
      [H: varsFromStrProp(eval("Weapon" + WpnNum), "suffixed")]
      
    <!-- Print out the results. -->
      I attack the enemy with my {WpnName_}.
      [H: bonus = WpnAttackBonus_ + if(CA, 2, 0) + MiscAtk]
      <br><b>Attack roll:</b> [AtkRoll = 1d20 + bonus]
        {if(AtkRoll==20+bonus, ", a critical hit!", "")}
        {if(AtkRoll== 1+bonus, ", a critical miss!", "")}
      <br><b>Damage roll:</b> 
        [eval(WpnDamage_) + MiscDmg] {WpnKeyword_} damage, 
        or [WpnMaxDamage_ + eval(WpnCritDamage_) + MiscDmg] {WpnKeyword_} damage on a critical hit.
    
    This displays the following dialog box. We'll examine how the macro builds this dialog below.
    Image

    If you click OK, the output looks like this.
    Image

    The dropdown list is the complex part of this example. It displays the names of up to 9 weapons, and then uses the properties of the selected weapon to make the attack rolls. The user's choice in the list is remembered, and is automatically selected the next time the macro runs.

    The dialog box is displayed by the input() function call. Each of the 4 parameters is a string that defines one of the 4 controls in the dialog box. The last 3 should be obvious once you read the instructions for the input() function. The first parameter is built up from some calculated values. The string that gets built looks like:

    Code: Select all

    "WpnNum | Longsword +1, Fire dagger +2, --none--, --none--, --none--, --none--, --none--, --none--, --none--, --none-- | Select weapon to use | LIST | SELECT=0"
    Note the abort() function just after the input(). You should always do this, so that if the user clicks Cancel, the macro will halt. If you don't do this, the input() function doesn't assign to any variables, and the remainder of the macro will throw up many popups asking the user for missing data.

    First, let's see how the code remembers the user's last choice from the list.

    Code: Select all

    <!-- Get the index of the previously selected weapon, if any. (Default to 0 if none.) -->
      [H: WpnNum = getStrProp(Private, "DefaultWpn", 0)]
    ...  
    <!-- Save the default weapon for next time. -->
      [h: Private = setStrProp(Private, "DefaultWpn", WpnNum)]
    The campaign defines a token property called Private, which these macros use to hold extra info. In this case, the token property holds the text "DefaultWpn=1 ; ". The getStrProp() function scans this string for a string property called "DefaultWpn" and returns the value. Since the property might be missing, the third argument assigns a default value of 0. After the dialog box is shown, the user's selection is saved back into the Private token property for future use.

    For the list of weapon names, we could have just typed them into the macro directly. But then you'd have to edit the macro every time your character got a new weapon. Instead, we want to assemble the list on the fly from data stored in the token properties. This code constructs the list:

    Code: Select all

    <!-- Build the list of weapon names. -->
      [H: WpnList = ""]
      [H,COUNT(10): WpnList = listAppend(WpnList, getStrProp(eval("Weapon"+roll.count), "WpnName"))]
    
    eval() is used to access the properties named "Weapon1" through "Weapon9", and the getStrProp() function then fetches the string property "WpnName" and adds it to the WpnList variable, along with a comma.

    Finally, the text of the attack rolls needs access to the properties of the selected weapon. Instead of using multiple getStrProp() calls, we use the varsFromStrProp() function to scan the entire token property and create a variable for each string property found there. Then we can just use the variables "WpnName_", "WpnAttackBonus_", etc. We use the SUFFIXED option so that our macro doesn't overwrite any token properties that might happen to have the same names as our string properties.

    EDIT: 1-Oct-2008 Updated for b46.
    Last edited by knizia.fan on Wed Nov 19, 2008 10:27 pm, edited 2 times in total.

    knizia.fan
    Giant
    Posts: 197
    Joined: Wed Jul 30, 2008 3:43 pm

    Post by knizia.fan »

    "Edit weapons" macro
    So, we've stored various bits of information about each weapon in a single token property that looks like this:
    Token property for weapons wrote:Weapon0 WpnName=Longsword +1 ; WpnAttackBonus=1 ; WpnDamage=1d8+1 ; WpnMaxDamage=9 ; WpnCritDamage=1d6 ; WpnKeyword= ;
    This is certainly more compact than putting each item in its own token property, and repeating that for every weapon. It also means that we can just add stuff to the string as we make our macros more complex, without having to petition every GM we play with to add more token properties to their campaign file. The disadvantage is that the information is harder to edit when it's packed into one string like this. So, we solve that problem the way any programmer would -- write more code!

    In the example campaign file, there is an "Edit weapons" macro:

    Code: Select all

    /self
    <!-- EDIT WEAPONS MACRO b46 -->
    
    <!-- Build the list of weapon names. -->
      [H: WpnList = ""]
      [H,COUNT(10): WpnList = listAppend(WpnList, getStrProp(eval("Weapon" + roll.count), "WpnName"))]
    
    <!-- Ask the user to select one of the weapons. -->
      [H: status = input("WpnNum | " + WpnList + " | Select weapon to edit | LIST")]
      [H: abort(status)]
      
    <!-- Obtain the property string for the selected weapon. -->
      [H: WpnPropName = "Weapon" + WpnNum]
      [H: WpnProps = eval(WpnPropName)]
      
    <!-- Error checking -- make sure the property string has been set up already. -->
      [H: NumProps = countStrProp(eval(WpnPropName))]
      [H: assert(NumProps!=0, "The " + WpnPropName + " property is not set up correctly.")]
      
    <!-- Put up a dialog with all the properties in the property string. 
      -- Note that the new property string is automatically assigned back to the
      -- token property that holds the weapon's property string. -->
      [H: status = input("blah | " + WpnNum + " | Weapon number | LABEL",
          WpnPropName + " | " + WpnProps + " | Weapon properties | PROPS")]
      [H: abort(status)]
    
    <!-- Print the new values to the chat window for verification. -->
    New properties for weapon #{WpnNum}:
    {formatStrProp(eval(WpnPropName), 
        "<table border=0>%list</table>", 
        "<tr><td style='padding:0px 5px'><b>%key</b></td> <td>%value</td></tr>",
        ""
    )}
    
    When run, the macro first asks which weapon you want to edit:
    Image

    After selecting a weapon and clicking OK, a second dialog allows you to edit the weapon's details.
    Image

    Although the second dialog contains many items, the input() function call is fairly simple with only 2 parameters. The first is a label that displays the number of the weapon being edited. The second uses the PROPS type, which creates a sub-region that is automatically filled with one edit box for each string property found in the provided value string.

    This is what that second parameter looks like after the string is assembled, assuming we chose to edit the first weapon.

    Code: Select all

    "Weapon0 | WpnName=Longsword +1 ; WpnAttackBonus=1 ; WpnDamage=1d8+1 ; WpnMaxDamage=9 ; WpnCritDamage=1d6 ; WpnKeyword= ; | Weapon properties | PROPS"
    As with other input() options, the first slot ("Weapon0") is the variable that will receive the edited value. Because "Weapon0" is a token property, the new value will go directly into that token property. The new value will be another property string, with the user's new values.

    At the end, the formatStrProp() function is used to print a table containing all the new values for the weapon.

    One interesting note about the PROPS type. Since the structure of the dialog box is determined by the value string, we can actually alter the dialog box just by changing the text of a property. For example, suppose we wanted one of our weapons to have an extra Description setting. We could just go into the token's property list and edit the Weapon0 property so it looks like:
    Weapon0 wrote:Weapon0: WpnDescription=A jet-black blade ; WpnName=Longsword +1 ; WpnAttackBonus=1 ; WpnDamage=1d8+1 ; WpnMaxDamage=9 ; WpnCritDamage=1d6 ; WpnKeyword= ;
    When we run the macro and edit Weapon1, the dialog now looks like this:
    Image
    This is remarkable--we altered the user interface for our macro without editing a single line of code! Pretty neat.

    I hope that this new method will allow people to consolidate a lot of the token properties now in use into a smaller number of composite properties. A nice advantage of doing this is that extending the set of properties becomes much easier and doesn't require coordination with the GM, as you saw with the Description weapon property above.

    ===========================================

    There are also "Reset weapons" and "dump wpns" macros in the example. "Reset weapons" lets you assign a default property string to one or all of the Weaponn token properties, in case you want to start over, or if you've messed them up. "dump wpns" just prints out the Weapon0 through Weapon9 token properties, so you can see what's stored there.

    EDIT: 1-Oct-2008 Updated for b46
    Last edited by knizia.fan on Wed Nov 19, 2008 10:28 pm, edited 2 times in total.

    knizia.fan
    Giant
    Posts: 197
    Joined: Wed Jul 30, 2008 3:43 pm

    Post by knizia.fan »

    "Apply state" macro
    Let's get a bit fancier, and create a dialog box for setting a token's state. The example campaign file contains image states for all the 4E D&D states, using AidyBaby's cool state images. There are 47 of them, so having this graphical interface to organize them all will be useful. The macro is long, but repetitive. It's just long because there are so many states, not because there's a lot of code.

    Code: Select all

    /self
    <!-- APPLY STATE MACRO b46 -->
    
    <!-- Break the states into several lists. -->
      [H: MiscStates = 
          "Blinded, Bloodied, Dazed, Dead, Deafened, Dominated, "
          +"Dying, Helpless, Immobilized, Petrified, Prone, Restrained, "
          +"Slowed, Stunned, Surprised, Unconscious, Weakened"]
      [H: CurseStates = "Curse_blue, Curse_cyan, Curse_green, "
                         +"Curse_orange, Curse_red, Curse_yellow"]
      [H: MarkedStates = "Marked_blue, Marked_cyan, Marked_green, "
                          +"Marked_orange, Marked_red, Marked_yellow"]
      [H: QuarryStates = "Quarry_blue, Quarry_cyan, Quarry_green, "
                          +"Quarry_orange, Quarry_red, Quarry_yellow"]
      [H: OngoingStates = 
          "Ongoing_acid, Ongoing_cold, Ongoing_fire, Ongoing_force, "
          +"Ongoing_lightning, Ongoing_necrotic, Ongoing_poison, "
          +"Ongoing_psychic, Ongoing_radiant, Ongoing_thunder"]
    
    <!-- Obtain the image URLs -->
      [H: TempList = MiscStates]
      [H: Num = listCount(TempList)]
      [H,COUNT(Num): TempList = listReplace(TempList, roll.count, 
          listGet(TempList, roll.count) + " " + getStateImage(listGet(TempList, roll.count)))]
      [H: TempList = listInsert(TempList, 0, " ")]
      [H: MiscList = TempList]
    
      [H: TempList = CurseStates]
      [H: Num = listCount(TempList)]
      [H,COUNT(Num): TempList = listReplace(TempList, roll.count, 
          listGet(TempList, roll.count) + " " + getStateImage(listGet(TempList, roll.count)))]
      [H: TempList = listInsert(TempList, 0, " ")]
      [H: CurseList = TempList]
    
      [H: TempList = MarkedStates]
      [H: Num = listCount(TempList)]
      [H,COUNT(Num): TempList = listReplace(TempList, roll.count, 
          listGet(TempList, roll.count) + " " + getStateImage(listGet(TempList, roll.count)))]
      [H: TempList = listInsert(TempList, 0, " ")]
      [H: MarkedList = TempList]
    
      [H: TempList = QuarryStates]
      [H: Num = listCount(TempList)]
      [H,COUNT(Num): TempList = listReplace(TempList, roll.count, 
          listGet(TempList, roll.count) + " " + getStateImage(listGet(TempList, roll.count)))]
      [H: TempList = listInsert(TempList, 0, " ")]
      [H: QuarryList = TempList]
    
      [H: TempList = OngoingStates]
      [H: Num = listCount(TempList)]
      [H,COUNT(Num): TempList = listReplace(TempList, roll.count, 
          listGet(TempList, roll.count) + " " + getStateImage(listGet(TempList, roll.count)))]
      [H: TempList = listInsert(TempList, 0, " ")]
      [H: OngoingList = TempList]
    
    <!-- Build a dialog box for the user. -->
      [H: status = input(
          "Remove | 0 | Remove state instead of applying | CHECK",
          "RemoveAll | 0 | Remove ALL states | CHECK",
          "blah | | Select state(s) to apply | LABEL | text=none",
          "MiscNum | " + MiscList + " | Misc. states | LIST | icon=true iconsize=20",
          "CurseNum | " + CurseList + " | Cursed | LIST | icon=true iconsize=20",
          "MarkedNum | " + MarkedList + " | Marked | LIST | icon=true iconsize=20",
          "QuarryNum | " + QuarryList + " | Quarry | LIST | icon=true iconsize=20",
          "OngoingNum | " + OngoingList + " | Ongoing damage | LIST | icon=true iconsize=20"
          )]
      [H: abort(status)]
    
    <!-- Set the appropriate states. -->
      [H: StateValue = if(Remove, 0, 1)]
      
      [H: StateName = if(MiscNum != 0, "state." + listGet(MiscStates, MiscNum-1), "")]
      [H: set(StateName, StateValue)]
      
      [H: StateName = if(CurseNum != 0, "state." + listGet(CurseStates, CurseNum-1), "")]
      [H: set(StateName, StateValue)]
      
      [H: StateName = if(MarkedNum != 0, "state." + listGet(MarkedStates, MarkedNum-1), "")]
      [H: set(StateName, StateValue)]
      
      [H: StateName = if(QuarryNum != 0, "state." + listGet(QuarryStates, QuarryNum-1), "")]
      [H: set(StateName, StateValue)]
      
      [H: StateName = if(OngoingNum != 0, "state." + listGet(OngoingStates, OngoingNum-1), "")]
      [H: set(StateName, StateValue)]
      
      [H: evalstr = if(RemoveAll, "setAllStates(0)", "true")]
      [H: eval(evalstr)]
    If you use a different set of states, it should be pretty easy to modify this code to use a different group of state names. Note, though, that this only works with image states, not the other types of states in MapTool (e.g. dot, circle, etc.).

    Here's the dialog box that this macro creates.
    Image

    When you click OK, our poor hero gains the states that were selected:
    Image

    The macro defines some lists of state names, and makes use of the list-processing functions to manipulate these lists before passing them to input(). In order to get images in the listboxes, the entries of the list have to look like this:

    Code: Select all

    Bloodied asset://da1a3d06d456cf2e6b5cea68eff51b2d
    The "asset://" part is called an asset URL, an internal name that MapTool uses to look up an image. These URLs can be obtained by calling getStateImage("Bloodied"). We want to do this once for each state, and we don't want to write code for every single state. The list-processing functions help us scan through the lists. For each list, we do something like this:

    Code: Select all

      [H: TempList = MiscStates]
      [H: Num = listCount(TempList)]
      [H,COUNT(Num): TempList = listReplace(TempList, roll.count, 
          listGet(TempList, roll.count) + " " + getStateImage(listGet(TempList, roll.count)))]
      [H: TempList = listInsert(TempList, 0, " ")]
      [H: MiscList = TempList]
    We obtain the length of the list, and then use a loop to replace each state name (e.g. "Bloodied") in the list with the state+image text "Bloodied asset://da1a3d06d456cf2e6b5cea68eff51b2d". We then put a blank entry at the beginning of the list so that each listbox in the dialog can start off empty.

    Once we've built these lists, the input() call is pretty straightforward. Afterwards, we set or unset the states that were chosen.

    EDIT 1-Oct-2008 Updated for b46

    ____________________________________________________________________

    "Character sheet" macro
    Here is a bare-bones character sheet, to demonstrate how to make a tabbed dialog box. Since a character has too many settings to fit on one dialog, a tabbed dialog is a good way to organize the information.

    The Skills token property is used to hold a big property string with all the base bonuses for each skill, e.g.

    Code: Select all

    Skills: Acrobatics=0 ; Arcana=0 ; Athletics=0 ; Bluff=0 ; Diplomacy=0 ; Dungeoneering=0 ; Endurance=0 ; Heal=0 ; History=0 ; Insight=0 ; Intimidate=0 ; Nature=0 ; Perception=0 ; Religion=0 ; Stealth=0 ; Streetwise=0 ; Thievery=0 ;

    Code: Select all

    <!-- CHARACTER SHEET MACRO b46-->
    
    <!-- Set up some information for later use -->
      [H: AbilityNames = "Strength, Constitution, Dexterity, Intelligence, Wisdom, Charisma"]
      [H: AbilityProps = strPropFromVars(AbilityNames, "unsuffixed")]
    
    <!-- Show the dialog box -->
      [H: status = input(
        "TabAbilities | Basics | Level, abilities, etc. | TAB",
          "Level | " + Level + " | Character level | TEXT | width=8",
          "AbilityProps | " + AbilityProps + " | Abilities | PROPS | setvars=unsuffixed",
          "ClassHP | " + ClassHP + " | Hit points per level for class | TEXT | width=8",
          "SurgePerDay | " + SurgePerDay + " | Healing surges per day | TEXT | width=8",
          "ArmorCheckPenalty | " + ArmorCheckPenalty + " | Armor check penalty | TEXT | width=8",
          "Speed | " + Speed + " | Speed | TEXT | width=8",
        "TabSkills | Skills | Set skill bonuses here | TAB",
          "blah | Don't add bonuses from abilities or level here. | Note | LABEL",
          "Skills | " + Skills + " | Set skill bonuses | PROPS"
      )]
      [H: abort(status)]
    The resulting dialog has two tabs.
    Image

    Note the use of the SETVARS=UNSUFFIXED option to set the 6 different token properties Strength, Constitution, etc.

    ____________________________________________________________________
    "Skill check" macro

    This macro uses the Skills token property which is set by the previous macro. For the user's convenience, it computes and displays the net bonus to the skill roll in the list that it constructs.

    Code: Select all

    <!-- SKILL CHECK MACRO b46 -->
    
    <!-- This macro assumes that the Skills property contains 
      -- all permanent bonuses other than level and ability. -->
    
    <!-- Some reference lists -->
      [H: SkillNames = "Acrobatics, Arcana, Athletics, Bluff, Diplomacy, Dungeoneering, Endurance, Heal, History, Insight, Intimidate, Nature, Perception, Religion, Stealth, Streetwise, Thievery"]
      [H: SkillAbils = "Dex, Int, Str, Cha, Cha, Wis, Con, Wis, Int, Wis, Cha, Wis, Wis, Int, Dex, Cha, Dex"]
      [H: ArmorCheckAbils = "Str, Con, Dex"]
      [H: NumSkills = listCount(SkillNames)]
    
    <!-- Compute the total bonus for each skill -->
      [H: SkillBonuses = Skills]
      [H,COUNT(NumSkills): SkillBonuses = setStrProp(
        SkillBonuses, 
        listGet(SkillNames, roll.count), 
        getStrProp(Skills, listGet(SkillNames, roll.count)) 
          + LevelBonus
          + eval(listGet(SkillAbils, roll.count)+"Bonus") 
          + if(listContains(ArmorCheckAbils, listGet(SkillAbils, roll.count)), ArmorCheckPenalty, 0)
      )]
    
    <!-- Construct the string for the listbox -->
      [H: SkillStr = SkillNames]
      [H,COUNT(NumSkills): SkillStr = listReplace(
        SkillStr, 
        roll.count, 
        listGet(SkillNames, roll.count) 
          + " (" 
          + if(getStrProp(SkillBonuses, listGet(SkillNames, roll.count)) < 0, "", "+")
          + getStrProp(SkillBonuses, listGet(SkillNames, roll.count))
          + ")"
      )]
    
    <!-- Display the dialog box, selecting the skill that was chosen last time -->
      [H: success = input(
        "SkillNum | " + SkillStr + " | Select a skill | LIST | SELECT=" + getStrProp(Private, "LastSkill", 0),
        "TempBonus | 0 | Temporary bonus"
      )]
      [H: abort(success)]
    
    <!-- Make the roll -->
      [H: SkillBonus = indexValueStrProp(SkillBonuses, SkillNum)]
      <b>{listGet(SkillNames, SkillNum)} check:</b> [1d20 + SkillBonus + TempBonus]
      
    <!-- Save the selected skill for next time -->
      [H: Private = setStrProp(Private, "LastSkill", SkillNum)]
    You get the following dialog, which displays the net skill check bonus for your verification before you make the roll. It remembers the last skill you used.
    Image
    Last edited by knizia.fan on Wed Nov 19, 2008 10:29 pm, edited 1 time in total.

    User avatar
    BigO
    Dragon
    Posts: 558
    Joined: Mon Jul 28, 2008 12:23 pm
    Location: Oshkosh, WI
    Contact:

    Post by BigO »

    Excellent work on this stuff knizia.fan! I haven't had time to play around with this stuff yet or fully read through your notes, but I noticed one thing right off the bat.

    It's counter-intuitive to me to have abort(0) be the thing that stops the execution. If you're naming it 'abort' then the inherent question asked by calling the function is "Do you want to abort?" and by normal Boolean logic, an answer of zero means "No" or in this case "No, I don't want to abort". But that's the opposite of what it does.

    I hope I explained that well enough. It's very late here.
    --O

    I am a small and fragile flower.
    http://maptool.rocks.andyousuck.com

    knizia.fan
    Giant
    Posts: 197
    Joined: Wed Jul 30, 2008 3:43 pm

    Post by knizia.fan »

    BigO wrote:Excellent work on this stuff knizia.fan! I haven't had time to play around with this stuff yet or fully read through your notes, but I noticed one thing right off the bat.

    It's counter-intuitive to me to have abort(0) be the thing that stops the execution. If you're naming it 'abort' then the inherent question asked by calling the function is "Do you want to abort?" and by normal Boolean logic, an answer of zero means "No" or in this case "No, I don't want to abort". But that's the opposite of what it does.

    I hope I explained that well enough. It's very late here.
    I definitely understand what you're saying, and I had thought about it as well. The main use of abort() at the moment is to halt execution when the user clicks Cancel in an input dialog box. It made sense for the input() function to return 0 for Cancel (i.e. failure) and 1 for OK (i.e. success). It's important that people be able to use this value without modification in a call to abort(), so I made 0 cause the abort to happen.

    I agree that it can seem odd, and if others also feel it should be changed, this could be tweaked in a later build. However, changing abort() means also changing the meaning of the return value for input() so that a 1 means failure and a 0 means success.

    The current behavior can be thought of as "abort(success_code)", where the abort happens when success_code indicates a failure (i.e. it is 0).

    User avatar
    palmer
    Great Wyrm
    Posts: 1367
    Joined: Sat Sep 06, 2008 7:54 pm

    Post by palmer »

    Now I know what I'm going to be wasting all my slack time at work tomorrow on... studying this...

    The edit dialogue to set the prop strings is sheer genius.


    The state macro is geniuser... especially when you have so many states.

    User avatar
    Orchard
    Great Wyrm
    Posts: 1852
    Joined: Fri May 09, 2008 10:45 am
    Location: Doylestown PA
    Contact:

    Post by Orchard »

    Amazing. Simply amazing.

    I will have to ponder this for a long time.
    0+0=1, for very unstable CPUs.

    User avatar
    BigO
    Dragon
    Posts: 558
    Joined: Mon Jul 28, 2008 12:23 pm
    Location: Oshkosh, WI
    Contact:

    Post by BigO »

    Orchard wrote:Amazing. Simply amazing.

    I will have to ponder this for a long time.
    Heh, yeah, that's what I was thinking too.
    --O

    I am a small and fragile flower.
    http://maptool.rocks.andyousuck.com

    Tacomannerism
    Kobold
    Posts: 23
    Joined: Mon Sep 15, 2008 9:45 am

    Post by Tacomannerism »

    Here's the macro that generated the elaborate example above. It assumes that your campaign file has image states called "Dazed", "Immobilised", "Slowed", and "Quarry_yellow". If you want to try it out yourself, download this campaign settings file, and load it in MapTool (Edit > Campaign Properties > Import). If the user clicks OK to the dialog box, the Creature, CA, MainWpn, OffWpn, FlavorText and State variables will contain the user's choices and can be used in further macro steps.
    Just a note: on my Windows computer, importing your "campaign settings file" seems to work correctly. On my Linux computer, when I click Import, and select the file, the new states do appear in the campaign properties "States" tab. However, when I click "OK" MapTools freezes for about 5 minutes. I get the following Java exception information printed to the console:

    Code: Select all

    Exception occurred during event dispatching:
    java.lang.ClassCastException: net.rptools.maptool.util.PersistenceUtil$PersistedCampaign cannot be cast to net.rptools.maptool.model.CampaignProperties
    	at net.rptools.maptool.util.PersistenceUtil.loadCampaignProperties(PersistenceUtil.java:354)
    	at net.rptools.maptool.client.ui.campaignproperties.CampaignPropertiesDialog$6$1.run(CampaignPropertiesDialog.java:534)
    	at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:226)
    	at java.awt.EventQueue.dispatchEvent(EventQueue.java:602)
    	at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:275)
    	at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:200)
    	at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:194)
    	at java.awt.Dialog$1.run(Dialog.java:1072)
    	at java.awt.Dialog$3.run(Dialog.java:1126)
    	at java.security.AccessController.doPrivileged(Native Method)
    	at java.awt.Dialog.show(Dialog.java:1124)
    	at java.awt.Component.show(Component.java:1457)
    	at java.awt.Component.setVisible(Component.java:1409)
    	at java.awt.Window.setVisible(Window.java:842)
    	at java.awt.Dialog.setVisible(Dialog.java:1011)
    	at net.rptools.maptool.client.ui.campaignproperties.CampaignPropertiesDialog.setVisible(CampaignPropertiesDialog.java:100)
    	at net.rptools.maptool.client.AppActions$49.execute(AppActions.java:1609)
    	at net.rptools.maptool.client.AppActions$ClientAction.actionPerformed(AppActions.java:1940)
    	at javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:2012)
    	at javax.swing.AbstractButton$Handler.actionPerformed(AbstractButton.java:2335)
    	at javax.swing.DefaultButtonModel.fireActionPerformed(DefaultButtonModel.java:404)
    	at javax.swing.DefaultButtonModel.setPressed(DefaultButtonModel.java:259)
    	at javax.swing.AbstractButton.doClick(AbstractButton.java:374)
    	at de.muntjak.tinylookandfeel.TinyMenuItemUI.doClick(TinyMenuItemUI.java:571)
    	at de.muntjak.tinylookandfeel.TinyMenuItemUI$MouseInputHandler.mouseReleased(TinyMenuItemUI.java:421)
    	at java.awt.Component.processMouseEvent(Component.java:6101)
    	at javax.swing.JComponent.processMouseEvent(JComponent.java:3276)
    	at java.awt.Component.processEvent(Component.java:5866)
    	at java.awt.Container.processEvent(Container.java:2105)
    	at java.awt.Component.dispatchEventImpl(Component.java:4462)
    	at java.awt.Container.dispatchEventImpl(Container.java:2163)
    	at java.awt.Component.dispatchEvent(Component.java:4288)
    	at java.awt.LightweightDispatcher.retargetMouseEvent(Container.java:4461)
    	at java.awt.LightweightDispatcher.processMouseEvent(Container.java:4125)
    	at java.awt.LightweightDispatcher.dispatchEvent(Container.java:4055)
    	at java.awt.Container.dispatchEventImpl(Container.java:2149)
    	at java.awt.Window.dispatchEventImpl(Window.java:2478)
    	at java.awt.Component.dispatchEvent(Component.java:4288)
    	at java.awt.EventQueue.dispatchEvent(EventQueue.java:604)
    	at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:275)
    	at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:200)
    	at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:190)
    	at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:185)
    	at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:177)
    	at java.awt.EventDispatchThread.run(EventDispatchThread.java:138)
    
    
    Oddly enough, MapTools recovers and seems to have imported the states successfully, but I thought you would want to know.

    Also got the following Java exception when I encountered the Apply State bug that you mentioned (with the images not being found the first time the macro is run). Again, this is just additional info, since you already know about the bug.

    Code: Select all

    Exception in thread "pool-4-thread-2" java.lang.NullPointerException
    	at net.rptools.maptool.util.ImageManager$BackgroundImageLoader.run(ImageManager.java:193)
    	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
    	at java.lang.Thread.run(Thread.java:636)
    
    

    User avatar
    DrVesuvius
    Giant
    Posts: 199
    Joined: Wed Jun 18, 2008 2:07 pm

    Post by DrVesuvius »

    k.fan

    Nice work! I know I've argued elsewhere for not over-complicating the MapTools macro code, but these changes add some really powerful functionality in a fairly simple to use way. And they're really well documented as well. Kudos!

    Dr V

    knizia.fan
    Giant
    Posts: 197
    Joined: Wed Jul 30, 2008 3:43 pm

    Post by knizia.fan »

    Tacomannerism wrote:Just a note: on my Windows computer, importing your "campaign settings file" seems to work correctly. On my Linux computer, when I click Import, and select the file, the new states do appear in the campaign properties "States" tab. However, when I click "OK" MapTools freezes for about 5 minutes. I get the following Java exception information printed to the console:

    Code: Select all

    Exception occurred during event dispatching:
    java.lang.ClassCastException: net.rptools.maptool.util.PersistenceUtil$PersistedCampaign cannot be cast to net.rptools.maptool.model.CampaignProperties
    	at net.rptools.maptool.util.PersistenceUtil.loadCampaignProperties(PersistenceUtil.java:354)
    	at net.rptools.maptool.client.ui.campaignproperties.CampaignPropertiesDialog$6$1.run(CampaignPropertiesDialog.java:534)
    
    Oddly enough, MapTools recovers and seems to have imported the states successfully, but I thought you would want to know.
    Interesting that it only happens on Linux. I created this file over 3 weeks ago when I submitted the first version of the patch. The state code has been revised since then to add new features and bar states. Based on your stack trace, I'm guessing that there's an incompatibility in the file format.

    I've recreated the settings file using the shipping b42 here. Please give that file a try and let me know what you see.
    Tacomannerism wrote:Also got the following Java exception when I encountered the Apply State bug that you mentioned (with the images not being found the first time the macro is run). Again, this is just additional info, since you already know about the bug.

    Code: Select all

    Exception in thread "pool-4-thread-2" java.lang.NullPointerException
    	at net.rptools.maptool.util.ImageManager$BackgroundImageLoader.run(ImageManager.java:193)
    	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
    	at java.lang.Thread.run(Thread.java:636)
    
    
    Yes, I'm not sure how to track this one down. I'm not even sure it's a bug in my code, because I seem to remember that even using an <img> tag to show a state image used to fail the first time. However I can't get that to happen right now, so I may be misremembering.

    I don't know enough about Eclipse and Java to track down issues involving multiple threads. I may just need to put in some kind of yield in the code to allow the image manager background thread a chance to load the images, but I don't know how to do that, or if that's the problem. I'm afraid for now you'll have to live with this issue.

    User avatar
    Micco
    Giant
    Posts: 148
    Joined: Mon Jun 16, 2008 10:21 pm

    Post by Micco »

    If I had a hat, I'd tip it to you, K.fan.

    Very nice work.

    Thank you!

    :)

    User avatar
    RedDog
    Dragon
    Posts: 393
    Joined: Sat Jan 05, 2008 10:02 pm
    Location: Clearwater, FL

    Post by RedDog »

    This is some pretty polished stuff. Very nice work. I like your implementation for the input command. I don't think it could be any simpler.

    It looks like the primary functionality for this is to assign values to Token Properties and temporary properties. Is there a way to use this to display a message box with a description that spans the width of the window and then have options at the bottom to select (like one would use for a conversation tree)? I can see jury rigging it, but it looks like the diologue box is split into a left and right side.

    knizia.fan
    Giant
    Posts: 197
    Joined: Wed Jul 30, 2008 3:43 pm

    Post by knizia.fan »

    RedDog wrote:This is some pretty polished stuff. Very nice work. I like your implementation for the input command. I don't think it could be any simpler.

    It looks like the primary functionality for this is to assign values to Token Properties and temporary properties. Is there a way to use this to display a message box with a description that spans the width of the window and then have options at the bottom to select (like one would use for a conversation tree)? I can see jury rigging it, but it looks like the diologue box is split into a left and right side.
    Currently the dialog box is a strict two-column grid. (The PROPS type creates a sub-grid in the second column.) I've thought about adding a SPAN=TRUE option to the LABEL type that would cause it to ignore the Prompt section and display the Value across both columns.

    However, a conversation tree runs into a couple of problems. First, you'd need support for multi-line text wrapping. I see some issues that could arise if I tried to support that. The dialog uses a very simple mechanism for determining its size, and wrapping would require a fair bit of effort to get right. I also worry about over-complicating the parameter syntax; I'm not sure how many options I'd need to add in order to get wrapping to work reliably.

    In addition, I think you'd find it very hard to implement a conversation tree in the current macro language, because there isn't any flow control. You'd have to use a lot of if() and eval() trickery to construct the parameters to subsequent calls to input().

    If you're interested in pursuing this further, you could mock up a macro for a conversation tree. Make up some imaginary new options for input(), and show us what the macro would look like. How about a tree which pops up 3 dialog boxes? That should be deep enough to give a good sense of how it would work. If you could mock up graphics of the desired dialog boxes, that would be great too. (Don't work too hard, though! The effort might not lead anywhere.)

    Thanks,
    --k.fan

    Post Reply

    Return to “Documentation Requests/Discussion”