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

Append to Notes in a Handout

1613838533
Nick O.
Forum Champion
I'm having trouble wrapping my head around how to update the notes in a handout. I see that I have to pass in a call back function in order for that to work, but I keep getting a "Maximum call stack size exceeded" message. I think what's happening is that when the notes value gets updated, the script fires again, but I'm not sure how to get around that. Any insight is appreciated. function addItemToHandout(handoutName,item){     var myHandout= findObjs({type:"handout",name:handoutName})[0] myHandout.get("notes",function(notes){ notes+= `<p>${item}</p>`; myHandout.set("notes",notes); }); }
1613839627

Edited 1613839712
The Aaron
Roll20 Production Team
API Scripter
It sounds like you are subscribing to change events on the handout elsewhere in your script.&nbsp; You'll need to keep a list of handout ids you are editing, and ignore the next update to them. You can see that technique in this script, where I deal with deletes: /* global GroupInitiative */ on('ready',()=&gt;{ const scriptName = 'MarkTurnStartLocation'; const version = '0.1.0'; const schemaVersion = 0.1; const lastUpdate = 1612845685; const getTurnArray = () =&gt; ( '' === Campaign().get('turnorder') ? [] : JSON.parse(Campaign().get('turnorder'))); const checkInstall = () =&gt; { log(`-=&gt; ${scriptName} v${version} &lt;=- [${lastUpdate}]`); if ( !state.hasOwnProperty(scriptName) || state[scriptName].version !== schemaVersion ) { log(` &gt; Updating Schema to v${schemaVersion} &lt;`); switch (state[scriptName] &amp;&amp; state[scriptName].version) { case 0.1: /* break; // intentional dropthrough */ /* falls through */ case "UpdateSchemaVersion": state[scriptName].version = schemaVersion; break; default: state[scriptName] = { version: schemaVersion, options: { markerImage: `<a href="https://s3.amazonaws.com/files.d20.io/images/4996490/Ii0ukmjd-IFyjKHtfRrG5w/thumb.png?1406962627`" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/4996490/Ii0ukmjd-IFyjKHtfRrG5w/thumb.png?1406962627`</a>, scale: 1.7 } }; break; } } }; const showImg = (img) =&gt; `&lt;img src="${img}" style="max-width: 3em;max-height: 3em;border:1px solid #333; background-color: #999; border-radius: .2em;"&gt;`; const getCleanImgsrc = (imgsrc) =&gt; { let parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/); if(parts) { return parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`); } return; }; on('chat:message',msg=&gt;{ if('api'===msg.type &amp;&amp; /^!mark-start(\b\s|$)/i.test(msg.content) &amp;&amp; playerIsGM(msg.playerid)){ let who = (getObj('player',msg.playerid)||{get:()=&gt;'API'}).get('_displayname'); let tokens = (msg.selected || []) .map(o=&gt;getObj('graphic',o._id)) .filter(g=&gt;undefined !== g) ; let msgs = []; let args = msg.content.split(/\s+--/).slice(1); if(0 === args.length){ args.push('help'); } args.forEach((a)=&gt;{ let parts = a.split(/\s+/); switch(parts.shift().toLowerCase()){ case 'set-marker': { if(1 === tokens.length){ let imgsrc = getCleanImgsrc(tokens[0].get('imgsrc')); if(imgsrc){ state[scriptName].options.markerImage = imgsrc; msgs.push(`&lt;div&gt;&lt;div&gt;${showImg(imgsrc)}&lt;/div&gt;Updated start marker.&lt;/div&gt;`); } else { msgs.push(`&lt;div&gt;&lt;div&gt;${showImg(imgsrc)}&lt;/div&gt;&lt;code&gt;Error: Cannot use Marketplace image.&lt;/code&gt;&lt;/div&gt;`); } } else { msgs.push(`&lt;div&gt;&lt;code&gt;Error: Select a single token to collect the image from.&lt;/code&gt;&lt;/div&gt;`); } } break; case 'set-scale': { let scale = parseFloat(parts.shift())||0; if(scale){ if(scale &gt; 5.0){ msgs.push(`&lt;div&gt;&lt;code&gt;Warning: Scale seams pretty large.&lt;/code&gt;&lt;/div&gt;`); } else if( scale &lt; 0.1 ) { msgs.push(`&lt;div&gt;&lt;code&gt;Warning: Scale seams pretty small.&lt;/code&gt;&lt;/div&gt;`); } state[scriptName].options.scale = scale; msgs.push(`&lt;div&gt;Updated start marker scale to &lt;code&gt;${scale}&lt;/code&gt;.&lt;/div&gt;`); } else { msgs.push(`&lt;div&gt;&lt;code&gt;Error: Scale should be a decimal number where 1 means the same size as the token, 1.5 means 50% larger, etc.&lt;/code&gt;&lt;/div&gt;`); } } break; case 'help': { msgs.push(`&lt;h3&gt;MarkTurnStartLocation v${version}&lt;/h3&gt;`); msgs.push(`&lt;div&gt;Creates a marker to show where a token started on when its turn began.&lt;/div&gt;`); msgs.push(`&lt;div&gt;&lt;code&gt;!mark-start [--set-marker] [--set-scale NUMBER]&lt;/code&gt;&lt;/div&gt;`); msgs.push(`&lt;ul&gt;&lt;li&gt;&lt;code&gt;--set-marker&lt;/code&gt; Set the marker image to the image source of the single selected token.&lt;/li&gt;&lt;li&gt;&lt;code&gt;--set-scale NUMBER&lt;/code&gt; Set the scale of the marker image to the supplied NUMBER. Default is &lt;code&gt;1.7&lt;/code&gt;. &lt;code&gt;1.0&lt;/code&gt; makes the image the same size as the token.&lt;/li&gt;&lt;/ul&gt;`); } break; } }); sendChat('',`/w "${who}" &lt;div style="border:1px solid #999;background-color: white;padding:.5em;"&gt;${msgs.join('')}&lt;/div&gt;`); } if('api'===msg.type &amp;&amp; /^!eot\b/.test(msg.content)){ setTimeout(()=&gt;handleTurnOrderChange(Campaign(),{initiativepage:false,turnorder:JSON.stringify([{id:-1}])}),1000); } }); let ClearedIDs = {}; const clearMarkers = ()=&gt;{ findObjs({ type: 'graphic', controlledby: scriptName }).forEach(g=&gt;{ ClearedIDs[g.id]=true; g.remove(); }); }; const addMarker = (tid) =&gt; { let t = getObj('graphic',tid); if(t){ let layer = (t.get('layer') == 'gmlayer') ? 'gmlayer' : 'map'; let props = [ 'pageid', 'left', 'top', 'width', 'height' ] .reduce((m,p)=&gt;({...m,[p]:t.get(p)}),{ imgsrc: state[scriptName].options.markerImage, controlledby: scriptName, layer }); props.width *= state[scriptName].options.scale; props.height *= state[scriptName].options.scale; let m = createObj('graphic', props); if('gmlayer' === layer){ toBack(m); } else { toFront(m); } } }; const handleTurnOrderChange = (obj,prev) =&gt; { let force = (obj.get('initiativepage') !== prev.initiativepage); if(obj.get('initiativepage')){ setTimeout(()=&gt;{ let t = getTurnArray()[0]; if(t){ let pto = (''===prev.turnorder ? [] : JSON.parse(prev.turnorder)); if(pto[0].id !== t.id || force){ clearMarkers(); addMarker(t.id); } } },100); } else { clearMarkers(); } }; const handleChangeGraphic = (obj,prev) =&gt; { if(scriptName === prev.controlledby){ obj.set(prev); } }; const handleDestroyGraphic = (obj) =&gt; { if(ClearedIDs[obj.id]){ setTimeout(()=&gt;delete ClearedIDs[obj.id],1000); } else { let prev = JSON.parse(JSON.stringify(obj)); if(scriptName === prev.controlledby){ prev.pageid=prev._pageid; let m = createObj('graphic',prev); if('gmlayer' === prev.layer){ toBack(m); } else { toFront(m); } } } }; const registerEventHandlers = () =&gt; { on('change:campaign:turnorder',handleTurnOrderChange); on('change:campaign:initiativepage',handleTurnOrderChange); on('change:graphic',handleChangeGraphic); on('destroy:graphic',handleDestroyGraphic); if('undefined' !== typeof GroupInitiative &amp;&amp; GroupInitiative.ObserveTurnOrderChange){ GroupInitiative.ObserveTurnOrderChange((o,p)=&gt;handleTurnOrderChange(Campaign(),{initiativepage: false, turnorder:p})); } }; checkInstall(); registerEventHandlers(); if(Campaign().get('initiativepage')){ handleTurnOrderChange(Campaign(),{initiativepage:false,turnorder:JSON.stringify([{id:-1}])}); } });
1613840994
Nick O.
Forum Champion
Thanks, Aaron. I'm still confused, though. Is accessing the notes field considered subscribing to the change events? Or do I need to include event handling in order to accomplish this? This script results in the error.&nbsp; on("ready",function(){&nbsp; &nbsp; &nbsp; &nbsp; on("chat:message",function(msg){ &nbsp; &nbsp; &nbsp; &nbsp; if(msg.type=="api" &amp;&amp; msg.content.indexOf("!updateTest")==0){ addItemToHandout("testHandout","testItem"); } }) }) function addItemToHandout(handoutName,item){ &nbsp; &nbsp; var myHandout= findObjs({type:"handout",name:handoutName})[0] &nbsp; &nbsp; myHandout.get("notes",function(notes){ notes+= `&lt;p&gt;${item}&lt;/p&gt;`; &nbsp; &nbsp; myHandout.set("notes",notes); }); }
1613841294
The Aaron
Roll20 Production Team
API Scripter
Ah, looks like I made an incorrect assumption.&nbsp;&nbsp; Ok forget all the above (at least, as it pertains to this), I looked back at my NoteLog script, which does basically what you're doing there, and found this block of code: let nl = getNoteLog(); nl.get('notes', function(n){ if(!_.isNull(n)){ setTimeout(function(){ let text=n+'&lt;br&gt;'+longtext; nl.set('notes',text); },0); } }); It looks like when I wrote it I must have had the same problem, and I solved it by deferring the set.&nbsp; This is probably a bug i reported back then which hasn't been fixed yet...
1613841520
The Aaron
Roll20 Production Team
API Scripter
So, something like: on("ready",() =&gt; { const addItemToHandout = (handoutName,item) =&gt; { let myHandout= findObjs({type:"handout",name:handoutName})[0] myHandout.get("notes",(notes) =&gt; { notes+= `&lt;p&gt;${item}&lt;/p&gt;`; setTimeout(()=&gt;myHandout.set("notes",notes),0); }); } on("chat:message",(msg) =&gt; { if("api" === msg.type &amp;&amp; 0 === msg.content.indexOf("!updateTest") ){ addItemToHandout("testHandout","testItem"); } }); });
1613842389
Nick O.
Forum Champion
Thanks, Aaron! To make sure I understand - the setTimeout is forcing it to only execute once, with no delay?
1613846164
The Aaron
Roll20 Production Team
API Scripter
Not quite. It is only running once, and specifying a delay of 0, but that's not immediate.&nbsp; Javascript is single threaded, and for asynchronous functionality it uses a system similar to cooperative multitasking. &nbsp;Effectively, when you schedule something to run asynchronously, it goes on a sorted priority queue of tasks. When a script returns from an event, the next sets of tasks are handled. The timeout 0 then says "run this after the current call stack exits." &nbsp;Effectively, it will run right after the chat event handlers have all been called.&nbsp;