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

Want to learn how to write my own script. Any advice?

1564113180
The Aaron
Roll20 Production Team
API Scripter
If you wanted to follow a similar path as your prompt() usage, you might pass back buttons that collect the amount to spend of each denomination, then have a finalize purchase button that applies the collected coinage to the purse.    
Oh yes, that's right. Ok, I'll fix it so it sends chat messages instead of trying to use "prompt"/"window.alert". So then how would  should I write the "on:chat" handler to create a drop-down selection, and how much can I customize it? Ideally, I'd like it to have a short warning to the player letting them know that if they do not have enough of the currency they select, it will convert currencies to make up the cost. (IE: "Warning: conversions will happen automatically") If possible I'd also like a method of cancelling said drop-down if they change their mind or need to double-check their wallet (a spend value of zero is probably the easiest and simplest solution).
1564114274
The Aaron
Roll20 Production Team
API Scripter
You can send pretty much what you could type in chat, so something like: !spend ?{Amount? (Warning: conversions will happen automatically)|0gp} Using a Roll Query means they will get that alert, but if they cancel it, it won't run the command.  Remember that the API will only see the result of what they type, so if they type in "5gp 23sp 15tacos", the API's msg.contents will be: !spend 5gp 23sp 15tacos You'll need to parse those how ever makes sense.
Ok, so I know what I want my Query to be: ?{Type|Spend|Add} ?{Currency|Platinum|Gold|Silver|Copper} ?{Amount|0} This outputs (if all default values are chosen): Spend Platinum 0 How do I turn these into usable variables? I see this line in DXWarlock's script:  var msgFormula = msg.content.split(/\s+/); But I don't fully understand what's happening. Nothing else in his script seems to use the query's output, from what I understand.
1564117165
The Aaron
Roll20 Production Team
API Scripter
.split() divides a string into an array based on a string or regular expression. In the above, /\s+/ is a regular expression that matches 1 or more spaces or tabs. You'd do that, then parse each "word" based on position or matching some string/regex. 
ok, so then if there's letters it'll convert into a string, and if there's numbers it'll be an integer (or decimal in some cases)?
1564117749
GiGs
Pro
Sheet Author
API Scripter
Shelby K. said: Ok, so I know what I want my Query to be: ?{Type|Spend|Add} ?{Currency|Platinum|Gold|Silver|Copper} ?{Amount|0} This outputs (if all default values are chosen): Spend Platinum 0 How do I turn these into usable variables? I see this line in DXWarlock's script:&nbsp; var msgFormula = msg.content.split(/\s+/); But I don't fully understand what's happening. Nothing else in his script seems to use the query's output, from what I understand. Let me refer you back to my earlier post:&nbsp;<a href="https://app.roll20.net/forum/permalink/7639050/" rel="nofollow">https://app.roll20.net/forum/permalink/7639050/</a>
1564117897
GiGs
Pro
Sheet Author
API Scripter
You can also tweak the query&nbsp; ?{Currency|Platinum|Gold|Silver|Copper} to show a pretty label for players, and the supply the actual value the script needs to the script. For instance: ?{Currency|Platinum,pp|Gold,gp|Silver,sp|Copper,cp} if you use pp, gp, sp, and cp as identifiers within your script.
Ok, I'm pretty sure I understand that now. So then, just to confirm I understand properly...&nbsp; My current chat command is:&nbsp; !spend ?{Currency|Platinum|Gold|Silver|Copper} ?{Amount|0} With this line... var respond = msg.content.split(/\s+/); Would my array results look like this (assuming default values)? ["!spend", "Platinum", "0"] If so, will "0" still work as an integer? or will it need to be converted somehow? (Please forgive me if I'm asking stupid questions and/or making you repeat yourself, I'm trying very hard to learn and it's kind of a lot to take in sometimes)
1564120462

Edited 1564120694
GiGs
Pro
Sheet Author
API Scripter
Regarding your apology at the end: no need to apologise for asking questions. This stuff is hard , and we've all gone through painful learning periods. Ask as many questions as you need to, and don't feel embarrassed. We are here to help. You have that correct. The "0" will not be automatically recognised as a number, but there are numerous ways to coerce it into one. One common way: var amount = parseInt(respond[2], 10) || 0; parseint(respond[2]) &nbsp; will turn a string into a number (if its a valid number). The ,10) part ensures its recognised as base 10. It should not actually be needed, since its the default, but if i don't mention it someone will inevitably point it out. This is my attempt tp avoid the thread being derailed by a discussion about that :) The ||0 at the end: || in javascript is an OR statement, so this is a way of setting a default value. If the bit before the || is an error, the script will use the bit after instead. So if you try to parse incorrect text, it'll return 0 instead. You might instead use something else, like || -1 , or || '#error' , and then check the value afterwards to see its a valid value to proceed. But if you omit the ||, and your macro doesnt have at least 3 arguments, the script will crash and take down the sandbox with it. Remember this is text entry, so you have to account for the possibility that players will enter the chat command manually without using the specific macro you create for it, and use the wrong arguments. Another way would be var amount = +respond[2]|| 0; + in this case attempts to coerce it into a number. It's a more concise syntax. It wont make sure its an integer though - you could end up with decimal values if players make mistakes on the input. So for this kind of situation, parseInt is better, I just mention it because its another handy way to handle this situation.
Beautiful! Thank you so much! I'm going to implement that right now! Is it possible to simply parse the array object after it's been initially written? That way we "save" a variable? Example: var respond = msg.content.split(/\s+/); //returns ["!spend", "Platinum", "0" by default] parseInt(respond[2], 10) || 0; //Would this work and keep the array object in its place? I also have a question coming up about ".setWithWorker", but we'll get to that in a moment. Also, just throwing this out there, my sleep schedule is all kinds of whack so please don't let me keep anyone awake xD EDIT: I think the way i'm trying to do it, it should be: respond[2] = parseInt(respond[2], 10) || 0; because "respond[2]" is already an object/variable, and JS needs to know that I want to save the parse by assigning it to whatever the parse result is. Otherwise it would parse but nothing would actually change. Basically I'm trying to do a spot-optimization.
1564124402

Edited 1564124594
GiGs
Pro
Sheet Author
API Scripter
If you're asking if its possible to change the array value in place, without creating a new array, it is posisble, but it's not really a good idea. You would do it like so var respond = msg.content.split(/\s+/); respond[2] = parseInt(respond[2], 10) || 0; But as I mentioned, this isnt a good idea. It's better to create the variables you need, to make them easy to reference and easier to manipulate if needed. For instance, in this specific case, you want to check the input is correct, and handle when it isnt. That respond variable is inherently unstable, since its dependent on user input, and you have no control what other data is in it. It's better to use that variable as a starting point, and build the data in the format you need for the script. Being bound by the original data entry array will limit you. If you look back at that thread I linked earlier (the one with aaron's code) and scroll up a bit, IIRC there's a little bit of a discussion on how to store the data - using an object literal is very good for this kind of situation. You can end up with a single variable that contains all coin types you want to manipulate, whioch could look like this: var spend = {pp: 3, gp: 7, sp: 10, cp: 0}; When you get someones current money from their character sheet, you;ll find storing them in a value like this very handy. Look up javascript object literal for more on what they are and how to use them, and dont be afraid to ask questions.
Ok, I understand. However, for this specific instance I think it should be ok, because I'm not going to change or otherwise manipulate that array object any further. Once "respond[2]" is parsed into an Integer, it will only be read from. In fact, the current way I plan to write all of my commands will not require changing the array values at all. Here's some of my current code to demonstrate: var respond = msg.content.split(/\s+/); //constructs array from message query. [0] is always "!Spend" and is not used. [1] is the currency type. [2] is the amount. respond[2] = parseInt(respond[2], 10) || 0; //converts [2] into an Integer from a String var mDifference = 0; //Main difference for calculation var sDifference = 0; //Secondary difference for calculation var tDifference = 0; //Tertiary difference for calculation var tempDiff = 0; //temporary difference (used for multi-step conversion and continually changes) if (plat === undefined || gold === undefined || silv === undefined || copp === undefined) { sendChat("A wallet value is blank. Please set it to 0 [zero] or the correct value"); return; } if (respond[1] == "Platinum") { if (plat &lt; respond[2]) { //if the amount of Platinum the player has is less than what they want to spend, start converting other currencies, starting with Copper //first check if the player has enough conversion value to cover the cost at all. If they do, find how much Copper CAN be converted, then move on to Silver, then Gold mDifference = respond[2] - plat; //find the difference in how much they want to spend vs how much they have tempDiff = mDifference - floor((copp/1000) + (silv/100) + (gold/10)); //find the total value of convertible currency based on their relative value, using a temporary variable to maintain mDifference's value if (tempDiff &gt; 0) { //if tempDiff is positive, that means there's not enough total conversion value to make the purchase sendChat("You do not have enough money for this."); //tell the player that they don't have enough return; //end the function } //convert copper first if (copp &gt;= mDifference*1000) { //if there's enough copper to cover cost copp.setWithWorker(copp - (mDifference*1000)); //removes required copper plat.setWithWorker(0); //spends all platinum because the amount converted should make plat == respond[2] } } ".setWithWorker" is probably wrong, but if it's not hooray! EDIT: After re-reading a few times I think I understand what you mean now. If they enter "Q" or "10i" for the amount then that could mess things up, but at the same time if that's the case, isn't it going to be handled by "parseInt" either way? I also plan on adding a line where if they submit the command with a value of 0 [zero] then it'll just stop the function since nothing will ultimately happen. Wouldn't that ultimately catch any goofs in the command as well? As for condensing things into a single variable, I'm sure that definitely will come in handy. I'm just trying to make the script work right now. I know it's fat and ugly by comparison to what it could be, but once it works then we can make it efficient.
1564127436

Edited 1564127535
GiGs
Pro
Sheet Author
API Scripter
Shelby K. said: Ok, I understand. However, for this specific instance I think it should be ok, because I'm not going to change or otherwise manipulate that array object any further. Once "respond[2]" is parsed into an Integer, it will only be read from. In fact, the current way I plan to write all of my commands will not require changing the array values at all. You think that now, but it's better to prepare for changes later, and it takes very little extra work to cope with it. setWithWorker isnt quite right. The first issue is, you need to set the "current" parameter. Attributes have a current and a max, and you need to declare with you are changing. So it would be copp.setWithWorker({current: copp - (mDifference*1000)}); //removes required copper plat.setWithWorker({current: 0}); //spends all platinum because the amount converted should make plat == respond[2] The second issue is, i dont see where you are defining copp and plat. If its defined correctly in part of the code not shown here, all is well. You probably dont need setWithWorker , set works the same way. If you have a sheetworker on your character sheet that is triggered by changes to your coinage value, use setwithworker. But IIRC you're not using character sheets, so set is the one to use. It has exactly the same syntax. Incidentally this is why defining your data carefully will save work. I'd recommend having a look at that function I linked by aaron earlier, it would make adjusting coinage of any type much easier, once you've grasped it. You wouldnt need nested different handling for copp, silv, gold, plat etc.
Ok, thank you for clearing that up with "setWithWorker". Since previous posts keep being brought up I'll take some time to dig through the current set of replies I have before asking any new questions. Also yes, I've defined the different currency variables. I borrowed the lines from DXWarlock (along with his character finder code): var plat = findObjs({name: "PP", type: "attribute", characterid: cWho.id},{caseInsensitive: true})[0]; //sets Platinum Variable from attribute var gold = findObjs({name: "GP", type: "attribute", characterid: cWho.id},{caseInsensitive: true})[0]; //sets Gold Variable from attribute var silv = findObjs({name: "SP", type: "attribute", characterid: cWho.id},{caseInsensitive: true})[0]; //sets Silver Variable from attribute var copp = findObjs({name: "CP", type: "attribute", characterid: cWho.id},{caseInsensitive: true})[0]; //sets Copper Variable from attribute Yes, I've created the necessary sheets already, and you are correct in stating that I am not using the Roll20 Character Builder Sheets, I'm using the Journal Tab&nbsp; "Character" sheets.
1564128431
GiGs
Pro
Sheet Author
API Scripter
roll20 character sheets are also on the Journal Tab, they are officially called Journals, The difference is, they have a middle tab "Character Sheet" and you are using the "Attributes and Abilities" tab. So for that, yeah, you dont need setwithworker. Those attribute definitions look correct. So things are fine :)
Hello again, I've made a lot of progress with my script, but sadly when I try to test it, it still doesn't do anything. The "Spend" command is theoretically complete, but does not function. I'm not sure why this is. Once again, I don't get any errors from the API console. I'm going to add some debugging logs to see if I can find out anymore info, but in the meantime if anyone else can find the issue, I've posted the code on codeshare here: <a href="https://codeshare.io/GLoQMg" rel="nofollow">https://codeshare.io/GLoQMg</a> For reference, my chat message is: !Spend ?{Currency|Platinum|Gold|Silver|Copper} ?{Amount|0} I both apologize to and graciously thank anyone willing to look through my code. I've added a lot of comments to make sure anyone reading it, including myself, understands what's happening (or is at least supposed to happen). EDIT: It seems like the script isn't even recognizing that a chat event is happening.
1564485544
GiGs
Pro
Sheet Author
API Scripter
I'd lie to first mention, Aaron's script earlier is a drop in replacement that will handle all the currency conversions, and get rid of all the special case handling youre using for plat, gold, etc. That said, assuming you continue with this approach, from a quick scan of your script I see at least one problem: if (plat &lt; amount) { plat here is an attribute object, not a numerical value. You need to get the attribute's current value, and convert it to a number. You could use&nbsp; plat. get ( "current" ) Or as an alternative to getting the objects at the start of the script, you could use getAttributeByname to get their values directly, and create the objects only when you want to update the character sheet. e.g. var plat = getAttrByName( cWho.id, 'PP');
1564487427
The Aaron
Roll20 Production Team
API Scripter
I didn't see where you're using it, but the Javascript Spread Operator is only partially implemented in the Roll20 API sandbox. You can use it with Arrays, but not Objects.&nbsp;
GiGs said: plat here is an attribute object, not a numerical value. You need to get the attribute's current value, and convert it to a number. You could use&nbsp; plat. get ( "current" ) Or as an alternative to getting the objects at the start of the script, you could use getAttributeByname to get their values directly, and create the objects only when you want to update the character sheet. e.g. var plat = getAttrByName( cWho.id, 'PP'); So then would this work? if (plat.get("current") &lt; amount) Or would I have to save it as another variable? GiGs said: I'd like to first mention, Aaron's script earlier is a drop in replacement that will handle all the currency conversions, and get rid of all the special case handling you're using for plat, gold, etc. I understand that Aaron has a script has conversions in it. Not to discredit Aaron in any way, my players want the conversions to happen in a specific order and I want there to be as little user input as possible. That's why I have these conversion checks written the way they are. Due to my lack of experience, I have absolutely zero understanding of Aaron's conversion code. That's why I haven't even attempted to implement it. I'm not trying to be rude or hard-headed. I'm overjoyed at your help, but some things are a bit over my head at the moment, but I'm sure I'll get there with enough time. The Aaron said: I didn't see where you're using it, but the Javascript Spread Operator is only partially implemented in the Roll20 API sandbox. You can use it with Arrays, but not Objects.&nbsp; I'm not completely sure what you're referencing. I did a quick google search of "Javascript Spread Operator" and I'm pretty sure I'm not using it. Should I be? If so, why? Where I'm at in my goals for this script : Before I do any more development on the commands, I need to get what I currently have into a functional state for testing. This is my #1 priority right now, because if we don't get that resolved I can't fully test and resolve other issues. To reiterate the issue, nothing seems to happen in my script. It seems &nbsp;to not recognize that a chat even has passed. I've added log("Event that should be happening"); &nbsp;lines into the script for debugging purposes, but none of them appear in the API console. Perhaps taking a step back and making a very simple script that responds to "on:chat" with a "sendChat" line is necessary? That way I more fully understand how to write it, and potentially figure out why nothing is happening in my script. I know that there's already "on:chat" code in my script, but once again that was borrowed from DXWarlock.
1564503714
GiGs
Pro
Sheet Author
API Scripter
Shelby K. said: GiGs said: plat here is an attribute object, not a numerical value. You need to get the attribute's current value, and convert it to a number. You could use&nbsp; plat. get ( "current" ) Or as an alternative to getting the objects at the start of the script, you could use getAttributeByname to get their values directly, and create the objects only when you want to update the character sheet. e.g. var plat = getAttrByName( cWho.id, 'PP'); So then would this work? if (plat.get("current") &lt; amount) Or would I have to save it as another variable? That would work, but if its something you are going to be using a lot, its good practice to put it in its own variable. My approach would be to make a special money variable, like var money = {plat: plat.get('current'), gold: gold.get('current'), silv: silv.get('current'), copp: copp.get('current') }; This is an object variable. It's a way of grouping several different variables together in one object, to make it easier to handle.&nbsp; With that, you can use if (money.plat &lt; amount) and you can change its value with&nbsp; money.plat = 23 or&nbsp; money.plat = money.plat - 12 I chose plat, gold, silv, copp as the keys there so that you could implement them in your code with the smallest amount of changes. wherever gold is, use money.gold, for copp use money.copp, etc. Just remember when saving the values in the end, plat is attribute object on the character sheet, money.plat is the number you've saved to this variable.
1564507437
The Aaron
Roll20 Production Team
API Scripter
Whoops. I was up till 1:00am making a VR thing for work...I claim sleep deprivation causing me to read "Spend" as "Spread". =D I'd suggest calling parseInt() on the value you get from the attribute. Since Javascript variables can take on any type, and the language relies heavily on implicit type coercion, you can avoid heartache by forcing the value to a known type. Just like the arguments to a command come in as text representation of numbers, attributes will ALWAYS be text representation unless they are set by the API. That will bite you when you get to adding money and "3" + 1 = "31".&nbsp; let money = { plat: parseInt(plat.get('current') || 0, ... BTW, GiGs mentioned always supplying a base to parseInt() earlier (and I almost mentioned it then...), but you don't need to for base 10 as of Javascript ES6 &nbsp;=D
1564509997
GiGs
Pro
Sheet Author
API Scripter
The Aaron said: BTW, GiGs mentioned always supplying a base to parseInt() earlier (and I almost mentioned it then...), but you don't need to for base 10 as of Javascript ES6 &nbsp;=D Just to clarify, I said it wasnt needed, but if I left it out the conversation would be by someone pointing out it was needed. Can't win either way, hehe.
1564522498
The Aaron
Roll20 Production Team
API Scripter
Right, that's what I meant. =D
Ok, thank you for the recommendations on fixing/optimizing my wallet variables. However, the issue still remains that my script seems to not detect that my chat event has happened. How do I go about fixing this?
1564540787
The Aaron
Roll20 Production Team
API Scripter
I'd make hefty use of the log() function to verify it's doing what you think it's doing. Start with adding: log({curType,amount,plat,gold,silv,copp}); at line 25.
1564540914
The Aaron
Roll20 Production Team
API Scripter
OH! You've misspelled "message" as "mesage": on('chat:mesage', function(msg) {
1564541124
The Aaron
Roll20 Production Team
API Scripter
You'll want to add a check around cWho before line 9.&nbsp; If RollRight() fails to find a character, you'll dereference .get() on an undefined object: TypeError: Cannot read property 'get' of undefined Something like: if(!cWho) { return; }
1564541238
The Aaron
Roll20 Production Team
API Scripter
Next up, line 27's sendChat() needs the speaking as parameter set: "Error: When using sendChat() you must specify a speakingas and input property." You can leave it blank, or put something like "Spend" in there: sendChat( "Spend", "A wallet value is blank. Please set it to 0 [zero] or the correct value");
1564541601
The Aaron
Roll20 Production Team
API Scripter
Then fix all the: plat*10 type stuff to&nbsp; (parseInt(plat.get('current'))||0) * 10 or grab a variable for the value.
Oh, I didn't realize that was misspelled. Maybe that'll fix it. Thank you Aaron! :D EDIT: I'm just going to give them their own parse variables which will use the name already assigned to them. The object variable names will turn into "oPlat" "oGold" and so on.
Ok, we've made progress! The script now recognizes the chat events but now I get a new error: TypeError: plat.set is not a function It's referencing line 120 which reads: plat.set({current: plat - amount}); GiGs said: I'd lie to first mention, Aaron's script earlier is a drop in replacement that will handle all the currency conversions, and get rid of all the special case handling youre using for plat, gold, etc. That said, assuming you continue with this approach, from a quick scan of your script I see at least one problem: if (plat &lt; amount) { plat here is an attribute object, not a numerical value. You need to get the attribute's current value, and convert it to a number. You could use&nbsp; plat. get ( "current" ) Or as an alternative to getting the objects at the start of the script, you could use getAttributeByname to get their values directly, and create the objects only when you want to update the character sheet. e.g. var plat = getAttrByName( cWho.id, 'PP'); I'm going to update my codeshare again. I don't understand why I'm getting this error. The curType, amount, and currencies are properly parsed. But for some reason the sandbox doesn't like " currency .set"
1564717110
The Aaron
Roll20 Production Team
API Scripter
Probably should be oPlat.set() ?
hmm, ok. I'll try that
Wonderful News! After reaching the point of being able to successfully test the script in the Roll20 Sandbox, the "Spend" command is fully functional! There's one more hurdle to cross for this script to reach completion. As the DM, I'd like to be able to use a "Give" command which targets a player in the game and adds an amount of currency to their wallet. I also would like a "Loot" command to divide an amount I designate of each currency between all players (will probably be the biggest challenge). Since these commands are all on my end rather than my players, I'm much more welcoming to a longer set of inputs for these commands (specifically "Loot").&nbsp; I assume some variation of the code from DXWarklock will be needed along with something else. I'll try to see if I can't come up with something on my own, but in the meantime I'm willing to receive any suggestions. I've updated my codeshare to reflect the changes:&nbsp; <a href="https://codeshare.io/GLoQMg" rel="nofollow">https://codeshare.io/GLoQMg</a>
1564855314

Edited 1564855894
DXWarlock
Sheet Author
API Scripter
For the loot, if you are only wanting to give it to players online, in case someone is missing that day and you dont want it giving them loot. You could look at the Player object, and read it's&nbsp; _online property.. make an array of only the ones online. Count the length of that array and divide the loot by it, and then 'for each' that array for that amount. IE: You have 8 players, 5 are actually here. You tell it give 7500 gold. 7500/5= 1500 each to those 5. I myself use one of the handouts that everyone can see by it being set to "All Players" and read its " inplayerjournals " property, to get a list of all my players, to lookup who of those are online..probably a more elegant way but to me that's the simplest. I have a function sort of like that, as one of my 'not quite kosher global functions' since I use it for various things across multiple scripts like calculating party CR each game, total value of items they own they can sell, who gets picked for random events (dont want to pick someone not here)..etc.
Ok, that makes sense. So then how would I go about writing that? Something I should mention: I think someone said it before in an early reply, but the script only functions when my players select the proper character in the chat dropdown (who they're "Speaking As"). I'll probably keep it this way unless other things require me to change it, but that's an issue for later.
1564860313

Edited 1564861222
DXWarlock
Sheet Author
API Scripter
I was looking at mine, been a while since i made it. It seems I changed it at some point to be less 'crappy', It just looks for players directly and sees who's online: If you want a function: function GetOnline() { var CharactersOnline = []; var players=findObjs({_type:'player'}); _.each(players,function (obj){ if(obj.get('_online') == true) { var player = obj.get('id'); var character = findObjs({ _type: "character", controlledby: player}); CharactersOnline.push(character); } }); return CharactersOnline; }; (You dont need a function, can just put it right inside your code without wrapping it in function you call..I only have a function since I use it in 5 other places in different scripts.) Then call it with like var PCCharacters =&nbsp;GetOnline(); PCCharacters will be an array of all the characters the players online control:&nbsp; PCCharacters[0],&nbsp; PCCharacters[1], etc... And&nbsp; PCCharacters.length will tell you the number of people online. (This is assuming they each only control one character, if not you need to figure out a way to filter it to the 'main' character inside the _.each). So can use it as: var PCCharacters = GetOnline(); _.each(PCCharacters,function (obj){ //do whatever you want with each character });
Ok, so then how would this interact with the character sheets I've got?&nbsp; How will the script identify which player is which character? EDIT: Never mind, I missed the line where you find the character
After some consideration, I think for the time being the current state of the script is adequate for the objective I set out to accomplish. I'm still doing some QOL fine-tuning for my players, but more or less the script has been successfully written. Thank you so much for your help identifying issues and guiding my learning for Roll20 scripting. I cannot thank you enough for how much you've accelerated the process of writing this script. I'm truly blown away by your kindness!
1565735742
The Aaron
Roll20 Production Team
API Scripter
Cool! &nbsp;Glad you got to where you want to be. The next one will be even better! =D