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 .
×

Not Urgent: Can You Disable Chat Buttons After The Chat Message Has gone Past Them?

It would be nice if there was something I could put into various procedures to check if 1) there are other chat menus after it and if so, 2) prevent procs from firing. It's just to prevent someone scrolling back up their chat window, getting lost and pressing something that might not be appropriate for the moment. For example, this chat menu is this game's (AD&D 1e - don't think it matters) equipment list char window but in "transfer" mode with white buttons and Xfer in the title: Then you go on from that, in this case the normal equipment mode and then, afterwards, fiddling with money: It would be nice if there was a simple code I could associate (perhaps a global proc) with all three chat windows that would check to see if there are any chats occuring after it. In which case a message like "regenerate this chat window, first" could appear.  Not essential, but a nice to have. Has anyone seen or concocted anything like this?
1785966542

Edited 1785966703
Gauss
Forum Champion
Nothing I've seen will do that, but one emergent property you might be able to leverage is that when ChatSetAttr does a calculation, it is set into the Chat Menu button. So hitting the button multiple times will give the same answer and make the same change every time.  Example:  We see:  [Click me to deduct 5 points](!setattr --name @{character_name} --attribute|[[@{attribute}-5]]) Assume the attribute has 15 points,  Roll20 processes it then ChatSetAttr sees:  [Click me to deduct 5 points](!setattr --name @{character_name} --attribute|10) Every time the user pushes the button it will set the attribute to 10 points.  I assume you are using Scriptcards? If so there may be some equivalent method there.  Also, if using Scriptcards I'd ask the folks on the Scriptcards Discord server. 
1785971069

Edited 1785971380
Gauss said: Nothing I've seen will do that, but one emergent property you might be able to leverage is that when ChatSetAttr does a calculation, it is set into the Chat Menu button. So hitting the button multiple times will give the same answer and make the same change every time.  Example:  We see:  [Click me to deduct 5 points](!setattr --name @{character_name} --attribute|[[@{attribute}-5]]) Assume the attribute has 15 points,  Roll20 processes it then ChatSetAttr sees:  [Click me to deduct 5 points](!setattr --name @{character_name} --attribute|10) Every time the user pushes the button it will set the attribute to 10 points.  I assume you are using Scriptcards? If so there may be some equivalent method there.  Also, if using Scriptcards I'd ask the folks on the Scriptcards Discord server.  All my chattattrb things are associated with a roll query. :-) Every time you click one of the coin buttons, you are asked for an input,, so I think I'm safe there. I do need to comment out that debug statement, though. This screen shot shows the aftermath of subtracting 2 copper pieces from a character. Though, of course, you can use the up arrow on the chat menu and repeat what was done previously, though it will still run the roll query to ask how much to do.  No sciptcards for that, though I have a proc from daemonslye who helped me with an attack procedure on Macro a few months ago when I was relearning. The code for the above is not super long. TimGlobal functions ( I think there's only two) are for confirming a character is selected and for selecting character's selected colour (like the purple colour above). It's probably a dirty big mess, but here it is FWIW if you're interested. on('ready', () => { //Version information const ver = "Version 1.1" const verdate = "14 June 2026" //The following will be outputted after a restart sandbox or save: log("═╣ MyCoins.js !mycoins Tim, " + ver + ", " + verdate + " ╠═"); on("chat:message", function(msg) { if (msg.type !== "api") return; let chatcommand = msg.content.trim().toLowerCase(); if (!chatcommand.startsWith("!mycoins")) return; //log(chatcommand); let charID = TimGlobals.validatechar(msg);//Verify character is selected if (charID === "0") {return;} //OK, carry on! log (charID); let usercol = getAttrByName(charID,"color_option") || ""; let userhex = TimGlobals.hexColour(usercol); //ROTTO's CODE FOR panel AND btn //Altered background colour to #F1ECE5 by playing around on Affinity Photo 2. /* const panel = (usercolour, title, body) => '<div style="background:' + usercolour + ';border:1px solid #5b4632;border-radius:10px;padding:2px;max-width:760px;color:#f7f1e6;box-shadow:0 2px 6px rgba(0,0,0,0.35);">' + '<div style="font-size:22px;font-weight:bold;text-align:center;margin:0 0 10px 0;font-family:Georgia,serif;color:#fff8ec;line-height:1.15;word-break:break-word;">' + title + '</div>' + '<div style="background:#F1ECE5;color:#1b1712;padding:12px;border-radius:6px;">' + body + '</div>' + '</div>'; */ const panel = (usercolour, title, body) => '<div style="background:' + usercolour + ';border:1px solid #5b4632;border-radius:10px;padding:2px;max-width:760px;color:#f7f1e6;box-shadow:0 2px 6px rgba(0,0,0,0.35);">' + '<div style="font-size:22px;font-weight:bold;text-align:center;margin:0 0 10px 0;font-family:Arial;color:#fff8ec;line-height:1.15;word-break:break-word;">' + title + '</div>' + '<div style="background:#F1ECE5;color:#1b1712;padding:4px;border-radius:6px;">' + body + '</div>' + '</div>'; const btn = (label, cmd, width) => '<a style="display:inline-block;text-align:center;margin:1px 2px 1px 0;padding:3px 6px;min-width:' + (width || 90) + 'px;color:#ffffff;background:' + userhex + ';border:1px solid #6a4323;border-radius:6px;text-decoration:none;font-normal:100;" href="' + cmd + '">' + label + '</a>'; const btnBack = (label, cmd, width) => '<a style="display:inline-block;text-align:center;margin:1px 0px 1px 0;padding:0px 6px;min-width:' + (width || 90) + 'px;color:#202020;background:#CCCCCC;border:1px solid #6a4323;border-radius:6px;text-decoration:none;font-size:10px;font-normal:100;" href="' + cmd + '">' + label + '</a>'; let backTo = ""; let playerName = getAttrByName(charID,"character_name"); //----------------------------------------------------------------------- //Let's get the coin amounts let carCP = parseInt(getAttrByName(charID,"cp"),10) || 0; let carSP = parseInt(getAttrByName(charID,"sp"),10) || 0; let carEP = parseInt(getAttrByName(charID,"ep"),10) || 0; let carGP = parseInt(getAttrByName(charID,"gp"),10) || 0; let carPP = parseInt(getAttrByName(charID,"pp"),10) || 0; let coinsNo = carCP + carSP + carEP + carGP + carPP; let coinsWt = coinsNo/10; //Equivalent amounnts let gpvCP = parseInt((carCP/200).toFixed(2),10); //to fixed changes it to string, need to parseInt back to a number let gpvSP = parseInt((carSP/20).toFixed(2),10); let gpvEP = parseInt((carEP/2).toFixed(2),10); let gpvGP = parseInt((carGP),10); let gpvPP = parseInt((carPP*5),10); //Check to make sure there are no coins under total let totCP = parseInt(getAttrByName(charID,"cp-total"),10) || 0; let totSP = parseInt(getAttrByName(charID,"sp-total"),10) || 0; let totEP = parseInt(getAttrByName(charID,"ep-total"),10) || 0; let totGP = parseInt(getAttrByName(charID,"gp-total"),10) || 0; let totPP = parseInt(getAttrByName(charID,"pp-total"),10) || 0; let wealth = getAttrByName(charID,"wealthNotes") || '<div style="color:#6C1515;"><i>Nothing listed</i></div>'; let totalGBV = gpvCP + gpvSP + gpvEP + gpvGP + gpvPP; //totalGBV = totalGBV * 100; //totalGBV = totalGBV.toFixed(2) //totalGBV = totalGBV/100; //Title let strTitle = "$ " + playerName + " $" + "<p>Total GP Value: " + totalGBV + "<br>" + "Coins, no: " + coinsNo + "; Wt: " + coinsWt + " lbs</p>"; backTo = btnBack("Admin", "!admin", 10); let strBody = '<div style="background:#efe3cf;color:#6C1515;">Roll20 Character Sheet, ' + 'PLEASE list coins under "Carried Coins", only. <i>Do not use "Total Coins"</i>.' + ' ' + backTo + '</div>'; //<div style="background:#efe3cf let cpAdd = btn("CP", "!changecoins --?{Copper Pieces - " + playerName + " has " + carCP + " CP. Add or subtract (-)|0} --" + charID + " --CP --Copper Pieces --" + playerName + " --" + carCP , 20) + " <b>" + carCP + "</b> Copper (<b>" + (carCP / 200) + "</b> gp)"; let spAdd = "<br>" + btn("SP", "!changecoins --?{Silver Pieces - " + playerName + " has " + carSP + " SP. Add or subtract (-)|0} --" + charID + " --SP --Silver Pieces --" + playerName + " --" + carSP , 20) + " <b>" + carSP + "</b> Silver (<b>" + (carSP / 20) + "</b> gp)"; let epAdd = "<br>" + btn("EP", "!changecoins --?{Electrum Pieces - " + playerName + " has " + carEP + " EP. Add or subtract (-)|0} --" + charID + " --EP --Electrum Pieces --" + playerName + " --" + carEP , 20) + " <b>" + carEP + "</b> Electrum (<b>" + (carEP / 2) + "</b> gp)"; let gpAdd = "<br>" + btn("GP", "!changecoins --?{Gold Pieces - " + playerName + " has " + carGP + " GP. Add or subtract (-)|0} --" + charID + " --GP --Gold Pieces --" + playerName + " --" + carGP , 20) + " <b>" + carGP + "</b> Gold (<b>" + (carGP) + "</b> gp)"; let ppAdd = "<br>" + btn("PP", "!changecoins --?{Platinum Pieces - " + playerName + " has " + carPP + " PP. Add or subtract (-)|0} --" + charID + " --PP --Platinum Pieces --" + playerName + " --" + carPP , 20) + " <b>" + carPP + "</b> Platinum (<b>" + (carPP * 5) + "</b> gp)"; strBody += "<p><div>" + cpAdd + spAdd + epAdd + gpAdd + ppAdd + "</div></p>"; strBody += '<div style="background:#efe3cf;text-align:left;"><b>Wealth Notes</b> (jewels, banks, etc)  ' + btn("?", "!btnaboutwealthnotes --" + charID,10) + '<br>' + wealth + '</div></p>'; sendChat ("My Coins", panel(userhex,strTitle, strBody)); }); //////////////////////////////////////////////////////////////////////////////// on("chat:message", function(msg) { if (msg.type !== "api") return; let chatcommand = msg.content.trim().toLowerCase(); if (!chatcommand.startsWith("!changecoins")) return; let args = msg.content.trim().split(/\s*--/); let NewCoins = parseInt(args[1], 10) || 0 let CoinCharId = (args[2] || "").trim(); //CharID let CoinP = args[3].trim().toLowerCase(); //for example PP let CoinName = (args[4] || "").trim(); //eg Platinum Pieces let CoinCharName = (args[5] || "").trim(); //character name let Coins = parseInt(args[6], 10) || 0; //current number of coins let commandvalue = ""; if (isNaN(Coins)) { sendChat("Sorry, but...", "<div style='color:red;'>Added number of " + CoinP + " must be a positive number or possibly zero. " + numerr + "</div>"); return; } else if (Coins + NewCoins < -1) { sendChat("Sorry, but...", "<div style='color:red;'>" + NewCoins + " is more than the current number of " + CoinP + " " + "that " + CoinCharName + " currently has. </div>"); return; } //log(CoinP.toLowerCase() + " new coins " + Coins + " Current Coins " + NewCoins); //let FinalCoins = NewCoins; //sendChat("Operation", CoinCharName + " current " + CoinP + ": " + Coins + " to a 'FinalCoins' Total: " + NewCoins); sendChat(CoinCharName, "CP on Sheet: " + getAttrByName(CoinCharId,"cp") + "; Added " + NewCoins); let attrs = findObjs({ type: "attribute", characterid: CoinCharId, name: "cp" }); attrs.forEach(a => { sendChat("CP Debug", "attr id: " + a.id + "<br>name: " + a.get("name") + "<br>current: " + a.get("current") ); }); //using getAttrByName(CoinCharId,'cp') where 'CoinCharId' is Character ID: " + getAttrByName(CoinCharId,"cp")); if (CoinP.toLowerCase() === "cp") { commandvalue = `!modattr --charid ${CoinCharId} --cp|${NewCoins}`; } else if (CoinP.toLowerCase() === "sp") { commandvalue = `!modattr --charid ${CoinCharId} --sp|${NewCoins}`; } else if (CoinP.toLowerCase() === "ep") { commandvalue = `!modattr --charid ${CoinCharId} --ep|${NewCoins}`; } else if (CoinP.toLowerCase() === "gp") { commandvalue = `!modattr --charid ${CoinCharId} --gp|${NewCoins}`; } else if (CoinP.toLowerCase() === "pp") { commandvalue = `!modattr --charid ${CoinCharId} --pp|${NewCoins}`; } else { sendChat( "Error", `<div style='color:red;'>Unknown coin type: ${CoinP}</div>` ); return; } /*sendChat("Debug", "CoinCharName: " + CoinCharName + "<br>CoinCharId: " + CoinCharId + "<br>CoinP: " + CoinP + "<br>NewCoins: " + NewCoins + "<br>Command: " + commandvalue );*/ sendChat ("Change " + CoinP, commandvalue); sendChat("My Coins, Altered", "!forselected !mycoins"); }); });
1786275977
timmaugh
Pro
API Scripter
You can adapt a trick I use with the Metascript Toolbox...  (To be clear, this entails you working in the code of a mod script, not in the command line of a message.) Use the generateUUID() function (below) to get an ID for each button or group of buttons that have a limited number of uses: const generateUUID = (() => {     let a = 0;     let b = [];     return () => {         let c = (new Date()).getTime() + 0;         let f = 7;         let e = new Array(8);         let d = c === a;         a = c;         for (; 0 <= f; f--) {             e[f] = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(c % 64);             c = Math.floor(c / 64);         }         c = e.join("");         if (d) {             for (f = 11; 0 <= f && 63 === b[f]; f--) {                 b[f] = 0;             }             b[f]++;         } else {             for (f = 0; 12 > f; f++) {                 b[f] = Math.floor(64 * Math.random());             }         }         for (f = 0; 12 > f; f++) {             c += "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(b[f]);         }         return c;     }; })(); Generate the UUIDs as a part of generating the original menu. Incorporate each ID in the command line of the appropriate button, so that when they are clicked and they send their command line through chat, your script can capture and process the ID... either by an argument because you own the message with your script: !mycoins --uuid|-Pabcdef01234567890 --otherArgs|0 ...or by the use of a syntax marker (that you design) because your script doesn't own the message: !modattr ==-Pabcdef01234567890 --otherArgs... (working in someone else's script gets a little tricky; you can either operate at metascript speed -- to catch the message before the third party script does, or you can just forego the idea of "preventing" them from mucking about, and instead prompt a secondary message to the GM to know that someone was trying to use a script that no longer had a valid "use" available.) Also, when you generate the UUID, you want to store it. The best place to do that is in an object within the closure of your script (not in your chat event handler). Write the ID to this storage object where the ID is the key, and the value is the number of uses of that ID. Write the ID in the command line of the button. When the button is clicked, your script catches the ID, and checks it against the storage object. If the ID is there with a non-zero number of uses, decrement the uses in the storage object by 1 down to 0 and let the code continue If the ID is NOT there, stop the code (maybe give a message that "this button has already been used" Then, before your code exits, lob a delayed command to remove any keys on your storage object that have non-positive uses remaining: setTimeout(pruneEmpty,3000); That will remove your non-functional IDs, and the associated buttons will no longer work when the user scrolls back up the chat. Notice that I suggested "decrement[ing] the uses in the storage object by 1 down to 0 and let[ting] the code continue." This is for cases where you need to run the same command many times (perhaps using a !forselected handle to run it for each selected token). You don't want just the first instance of the run to work and the rest to fail. That's where the delay value of the setTimeout clean up command comes into play. Leave yourself enough time that all commands have a chance to resolve, but not so much that you worry that the user might choose to double-activate the button. Hopefully all of this makes sense and doesn't seem like too much overhead to implement. I can provide code examples, if necessary.
1786332482
The Aaron
Roll20 Production Team
API Scripter
I have several scripts that store the operation internally and the button command passes an index into that internal storage. Part of running the command removes it from the storage so that future clicks to the button are ignored (or tell the caller the command has expired). 
timmaugh said: You can adapt a trick I use with the Metascript Toolbox...  (To be clear, this entails you working in the code of a mod script, not in the command line of a message.) Use the generateUUID() function (below) to get an ID for each button or group of buttons that have a limited number of uses: const generateUUID = (() => {     let a = 0;     let b = [];     return () => {         let c = (new Date()).getTime() + 0;         let f = 7;         let e = new Array(8);         let d = c === a;         a = c;         for (; 0 <= f; f--) {             e[f] = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(c % 64);             c = Math.floor(c / 64);         }         c = e.join("");         if (d) {             for (f = 11; 0 <= f && 63 === b[f]; f--) {                 b[f] = 0;             }             b[f]++;         } else {             for (f = 0; 12 > f; f++) {                 b[f] = Math.floor(64 * Math.random());             }         }         for (f = 0; 12 > f; f++) {             c += "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(b[f]);         }         return c;     }; })(); Generate the UUIDs as a part of generating the original menu. Incorporate each ID in the command line of the appropriate button, so that when they are clicked and they send their command line through chat, your script can capture and process the ID... either by an argument because you own the message with your script: !mycoins --uuid|-Pabcdef01234567890 --otherArgs|0 ...or by the use of a syntax marker (that you design) because your script doesn't own the message: !modattr ==-Pabcdef01234567890 --otherArgs... (working in someone else's script gets a little tricky; you can either operate at metascript speed -- to catch the message before the third party script does, or you can just forego the idea of "preventing" them from mucking about, and instead prompt a secondary message to the GM to know that someone was trying to use a script that no longer had a valid "use" available.) Also, when you generate the UUID, you want to store it. The best place to do that is in an object within the closure of your script (not in your chat event handler). Write the ID to this storage object where the ID is the key, and the value is the number of uses of that ID. Write the ID in the command line of the button. When the button is clicked, your script catches the ID, and checks it against the storage object. If the ID is there with a non-zero number of uses, decrement the uses in the storage object by 1 down to 0 and let the code continue If the ID is NOT there, stop the code (maybe give a message that "this button has already been used" Then, before your code exits, lob a delayed command to remove any keys on your storage object that have non-positive uses remaining: setTimeout(pruneEmpty,3000); That will remove your non-functional IDs, and the associated buttons will no longer work when the user scrolls back up the chat. Notice that I suggested "decrement[ing] the uses in the storage object by 1 down to 0 and let[ting] the code continue." This is for cases where you need to run the same command many times (perhaps using a !forselected handle to run it for each selected token). You don't want just the first instance of the run to work and the rest to fail. That's where the delay value of the setTimeout clean up command comes into play. Leave yourself enough time that all commands have a chance to resolve, but not so much that you worry that the user might choose to double-activate the button. Hopefully all of this makes sense and doesn't seem like too much overhead to implement. I can provide code examples, if necessary. Thanks so much, I really appreciate this! I'm in the process of consolidating a number of my procs and functions into a fewer number of scripts as well as adjusting what sendChats I want to be whispered to the player initiating them, which ones I want to log in the chat archive, and making sure all chat window results can navigate back to submenus and the main menu. This will take me a couple more days but I expect by then to be in a good position to have a good look at what you've provided. :-) 
1786384751
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
The Aaron said: I have several scripts that store the operation internally and the button command passes an index into that internal storage. Part of running the command removes it from the storage so that future clicks to the button are ignored (or tell the caller the command has expired).  That's a nifty trick. <puts in rolodex of nifty tricks>