This is a script for firemonkey to pass API logging through to the browser console, where it can be logged properly. It also passes through to the Campaign tab console, so you can get your API logs in the same tab as the game, and with objects logged properly to allow drilling down. I haven't tested the script with violent/tamper/othermonkeys, it might need some header tweaks, or it might just work. Not sure. If you do want a hand getting it to work for a different monkey, let me know. The GM API is a real pig, to be honest. I don't have much good to say about GM, or about firemonkey. Writing an actual extension is much easier than dealing with a mangled set of API accessors. BAD MONKEY! There are two parts to the tool: 1) the firemonkey script - this will enable relogging of all API log() output to the API tab and the Campaign tab. Note that these must be in the same browser instance, userscripts do not have access to other browser windows. 2) an optional API construct to pass through regular console functions, enabling console.log(), console.info(), console.warn() and console.error(). console.error will also pass through a current API stack trace. There's also a variable called debugLevel, allowing you to disable logging of less serious log types - e.g. setting it to 2 will only log console.error() and console.warn() events. Setting it to 0 will disable all console() logs. The API's own log() function is not affected by any of this - these will always be passed out as console.log() to the browser. This also allows logging more than one parameter, however since it still needs to pass through the API/Ace's log() function, the parameters will come out in the browser as separate lines. Moneyshot: There's some settings at the top of the script that can be tweaked: bufferSize : set this higher if you have a lot of logs coming out very rapidly (or you notice any are missing) bufferDecay : set this higher if you're getting duplicate logs campaignPassthrough : set to false to disable passing the logs to the campaign tab prefixText/prefixStyle : the styled prefix for each log in the browser window to denote API logs charMap : edit the control characters used to pass through info/warn/error. Changes here will need to be mirrored in the API construct. The monkey script: Important: this must be 'enabled' in FM to function. Running a script
manually in firemonkey places it in a different context to an enabled
script, where, frustratingly, it doesn't have access to the GM API.
There is another method using sessionStorage listeners that works for a
manual script, if anyone needs. // ==UserScript== // @name apiconsole-relogger // @match <a href="https://app.roll20.net/*" rel="nofollow">https://app.roll20.net/*</a> // @version 1.0 // ==/UserScript== /* globals GM */ ( async () => { // Settings to modify if required const bufferSize = 30 ; const bufferDecay = 100 ; const campaignPassthrough = true ; const prefixStyle = `color:cyan; border:1px blue solid; padding: 0px 3px 0px 3px; border radius: 2px; background-color: #444444` ; const prefixText = `API` ; const charMap = { //log: ``, info : `·` , warn : `°` , error : `¡` , }; const location = window . location . href ; let bufferIndex = 0 ; const outputLog = ( newLog ) => { if ( newLog . length < 3 ) return ; const firstChar = newLog [ 2 ][ 0 ]; if ( newLog [ 2 ]. stack ) { const style = `color: pink; font-weight: bold; background: #4e2c34; padding: 0px 6px 0px 6px; border: 1px solid darkred; border-radius: 2px;` ; console . groupCollapsed ( `%cAPI stack.trace` , style ); console . error ( newLog [ 2 ]. stack ); console . groupEnd (); } else if ( Object . values ( charMap ). includes ( firstChar )) { newLog [ 2 ] = newLog [ 2 ]. slice ( 1 ); if ( firstChar === charMap . info ) console . info (... newLog ); else if ( firstChar === charMap . warn ) console . warn (... newLog ); else if ( firstChar === charMap . error ) console . error (... newLog ); } else console . log (... newLog ); } // Emitter path if ( /campaigns \/ scripts/ i . test ( location )) { const passthroughLog = ( log ) => { const logKey = `newlog ${ bufferIndex } ` ; bufferIndex = ( bufferIndex + 1 ) % bufferSize ; GM . setValue ( logKey , JSON . stringify ( log )); } const setupEventsElement = ( elementName ) => { const eventElement = document . createElement ( 'div' ); eventElement . style . display = 'none' ; eventElement . id = elementName ; document . body . append ( eventElement ); return eventElement ; } const reloggerEvents = setupEventsElement ( 'relogger-events' ); reloggerEvents . addEventListener ( 'apilog' , ( ev ) => { outputLog ( ev . detail ); if ( campaignPassthrough ) passthroughLog ( ev . detail ); }); const contentScript = ` (() => { const editSession = window.ace.edit('apiconsole'); editSession.$blockScrolling = 'Infinity'; editSession.session.on('change', (ev) => { let content = ev.lines.filter(v=>v); content = content.reduce((acc, line, i) => { if (!line.trim()) return acc; acc += (i>0) ? (' \\\\ n'+line) : line; return acc; }, ''); try { content = JSON.parse(content) } catch(e) { e } const output = ['%c ${ prefixText } ', ' ${ prefixStyle } ', content]; document.querySelector('#relogger-events').dispatchEvent(new CustomEvent('apilog', { detail: output })); }); })(); ` ; const injectContentScript = ( scriptString ) => { const newScript = document . createElement ( 'script' ); newScript . type = `text/javascript` ; newScript . innerText = scriptString ; document . head . append ( newScript ); } injectContentScript ( contentScript ); } // Receiver path else if ( /net \/ editor/ . test ( location )) { const busy = Array ( bufferSize ). fill ( false ); await new Promise ( res => setTimeout (() => res (), 7000 )); const decayChannel = async ( channel ) => { setTimeout (() => busy [ channel ] = false , bufferDecay ) } const logListener = ( index , log ) => { if ( busy [ index ]) return ; busy [ index ] = true ; let logArray ; try { logArray = JSON . parse ( log ) } catch ( e ) { } if ( logArray ) outputLog ( logArray ); decayChannel ( index ); } for ( let i = 0 ; i < bufferSize ; i ++) { GM . addValueChangeListener ( `newlog ${ i } ` , ( channel , old , log ) => logListener ( i , ` ${ log } ` )) } console . info ( '%c=== API listener online ===' , prefixStyle ); } })(); And the API construct to pass console() functions out. Throw it somewhere out of the way in your script, or if you really like it, just make it its own script that others can access. Don't wait for the ready event to declare it, or you'll miss any console.logging from scripts before it. If you really really like it, you could write a mini-handleInput to allow changing debugLevel from in campaign. That would allow on-the-fly, sandbox-wide debug control without the need to comment stuff out or edit individual scripts (though they would all need to be written using console logging instead of API log() ). let debugLevel = 4 ; const console = (() => { const charMap = { // log: ``, info : `·` , warn : `°` , error : `¡` , } const passthrough = (... args ) => args . forEach ( a => log ( a )); const nope = () => {}; return { log : ( log , ... args ) => debugLevel > 3 ? passthrough ( ` ${ charMap . log }${ log } ` , ... args ) : nope , info : ( log , ... args ) => debugLevel > 2 ? passthrough ( ` ${ charMap . info }${ log } ` , ... args ) : nope , warn : ( log , ... args ) => debugLevel > 1 ? passthrough ( ` ${ charMap . warn }${ log } ` , ... args ) : nope , error : ( log , ... args ) => debugLevel > 0 ? passthrough ( ` ${ charMap . error }${ log } ` , ... args , { stack : ( new Error ( 'API stack' )). stack }) : nope , } })();