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

TypeScript anyone?

Are there any other Roll20 API Scripters out there using TypeScript?   I had really hoped there would be some interest in the stuff I have been sharing, but have gotten zero traction.  If there is no interest, I will discontinue work on the framework.   Also, it would have been smarter if I had asked first if there was any interest, but I really wanted to write some code! :)
1541966799

Edited 1541969229
Ammo
Pro
<a href="https://github.com/derammo/der20" rel="nofollow">https://github.com/derammo/der20</a> &nbsp;has current code. &nbsp; Here are release notes to show what I have been up to: ========================================================== der20 TypeScript Framework Release v0.23 ========================================================== [ github commit 7d6efd1 by Ammo Goettsch on 2018-11-11 ] implemented optional command echo executed commands are now shown in chat this feature is on by default and can be disabled by plugin option echo false [ github commit 1da21ac by Ammo Goettsch on 2018-11-10 ] fixed interactive selection from array ========================================================== der20 TypeScript Framework Release v0.22 ========================================================== [ github commit 572180d by Ammo Goettsch on 2018-11-10 ] implemented interactive selection of anonymous character [ github commit da5190e by Ammo Goettsch on 2018-11-10 ] added support for dialogs on end of command [ github commit e2d4a6b by Ammo Goettsch on 2018-11-10 ] added selection group to dialogs ========================================================== der20 TypeScript Framework Release v0.21 ========================================================== [ github commit f822958 by Ammo Goettsch on 2018-11-10 ] reset all configuration now updates running plugin correctly [ github commit c38daac by Ammo Goettsch on 2018-11-10 ] removed indirect calling of event handlers events are now handled in code for readability more configuration collapse added to hide unset subtrees in JSON [ github commit b6444d7 by Ammo Goettsch on 2018-11-08 ] refactoring contexts eliminated magic command string substitution implemented interactive array item select without create new item now calling change event handlers after load to configure dependent data ========================================================== der20 TypeScript Framework Release v0.20 ========================================================== [ github commit dcba45e by Ammo Goettsch on 2018-11-08 ] improved exception handling and error reporting ========================================================== der20 TypeScript Framework Release v0.19 ========================================================== [ github commit 0d3b629 by Ammo Goettsch on 2018-11-07 ] errors now reported to plugin errors are whispered to GM and shown in a dialog the dialog contains a button to directly populate a github issue [ github commit a9f488d by Ammo Goettsch on 2018-11-07 ] stack traces now rewritten to show names and correct line numbers ========================================================== der20 TypeScript Framework Release v0.18 ========================================================== [ github commit 1f4dc45 by Ammo Goettsch on 2018-11-06 ] common option verbose added plugins no longer send success response messages by default ========================================================== der20 TypeScript Framework Release v0.17 ========================================================== [ github commit 8225ef7 by Ammo Goettsch on 2018-11-06 ] added stack traces to console output on errors [ github commit a24ed20 by Ammo Goettsch on 2018-11-06 ] implemented debug output option for all plugins [ github commit 0e5b3b2 by Ammo Goettsch on 2018-11-06 ] implemented optional validation on configuration input ========================================================== der20 TypeScript Framework Release v0.16 ========================================================== [ github commit fc77f36 by Ammo Goettsch on 2018-11-02 ] missing attributes can now be read and return empty string [ github commit 880e0e5 by Ammo Goettsch on 2018-11-02 ] implemented player characters scan ========================================================== der20 TypeScript Framework Release v0.12p ========================================================== [ github commit 3a29523 by Ammo Goettsch on 2018-11-01 ] implemented query generation for log import [ github commit 95d19b7 by Ammo Goettsch on 2018-11-01 ] &nbsp;&nbsp;&nbsp;&nbsp;implemented enumerated string configuration [ github commit a64cb1a by Ammo Goettsch on 2018-11-01 ] blank strings are now considered unset and revert to defaults [ github commit e8ea72f by Ammo Goettsch on 2018-10-31 ] implemented plugin option command [ github commit 8f3dce3 by Ammo Goettsch on 2018-10-31 ] implemented help generation added decorators for meta information added prototype meta information added help web page under docs ========================================================== der20 TypeScript Framework Release v0.07p ========================================================== [ github commit a106f7f by Ammo Goettsch on 2018-10-28 ] plugins now identify themselves by their commands on startup, each supported command is logged to console [ github commit 372ab51 by Ammo Goettsch on 2018-10-28 ] all commands now use queue infrastructure this completes the conversion to async processing commands are now held until all other work is done debug output now optional and disabled by default debug.log is now available globally and defaults to ignore output [ github commit e22e94a by Ammo Goettsch on 2018-10-28 ] queues now survive exceptions thrown from scheduled work functions [ github commit e1dd852 by Ammo Goettsch on 2018-10-28 ] implemented simple help command for all plugins ========================================================== der20 TypeScript Framework Release v0.05p ========================================================== [ github commit 93e1dd0 by Ammo Goettsch on 2018-10-28 ] rewrite of plugin execution to use multi level queue implemented plugin extension mix in handouts support is now an optional mix in ported anonymous plugin to asynchronous loaders parser and loader contexts are now fully typed loader contexts now support scheduling async commands and more loading ========================================================== der20 TypeScript Framework Release v0.04 ========================================================== [ github commit 167ff5f by Ammo Goettsch on 2018-10-23 ] implemented status messages [ github commit be2afd7 by Ammo Goettsch on 2018-10-23 ] chooser dialog implemented [ github commit 8cc103d by Ammo Goettsch on 2018-10-23 ] reset command implemented toplevel command 'reset all configuration' is injected by plugin [ github commit a8f4bde by Ammo Goettsch on 2018-10-23 ] handout change events now cause commands to be read [ github commit 731c2eb by Ammo Goettsch on 2018-10-23 ] makefile improvements scripts now separate from plugins [ github commit 91144aa by Ammo Goettsch on 2018-10-23 ] dump now displays JSON in a dialog for easy copy/paste [ github commit 34c34d0 by Ammo Goettsch on 2018-10-23 ] dump command added [ github commit b001148 by Ammo Goettsch on 2018-10-23 ] implemented reading of configuration from handouts ========================================================= der20 TypeScript Framework Release v0.03 ========================================================== [ github commit 31e2749 by Ammo Goettsch on 2018-10-21 ] implemented persistence auto selects roll20 state if available fallback to local file if available, for local testing fallback to no persistence at all, for browser testing [ github commit 7d8fdac by Ammo Goettsch on 2018-10-21 ] implemented defaults handling added UI support for defaults added rules to rewards defines added more league module configuration [ github commit ca13bec by Ammo Goettsch on 2018-10-20 ] now supports loading simple modules when testing under Node ========================================================== der20 TypeScript Framework Release v0.01 ========================================================== [ github commit efb7380 by derammo on 2018-10-19 ] ui now handles undefined values gracefully [ github commit a7ba89a by derammo on 2018-10-19 ] date configuration added [ github commit 81cdb89 by derammo on 2018-10-18 ] implemented Roll20 interface implemented Roll20 Dialog implemented glue to run in Roll20 implemented date configuration implemented show command implemented start and stop commands [ github commit e9d060c by derammo on 2018-10-18 ] added console.debug that goes nowhere on roll20 server [ github commit 1162928 by derammo on 2018-10-18 ] continued development of configuration lib make publish implemented configuration alias added configuration chooser added parser result added auto select of only defined item added [ github commit bb5f3ac by derammo on 2018-10-18 ] initial checkin
I see a huge wall of code with no indication what it does.
These are just change notes (i.e. the list of changes made to the code) which I was hoping would cause other developers to maybe say "hey that looks interesting, I want to do better exception handling in my scripts too" or something like that. &nbsp; &nbsp;I did make a pass to remove about half of the entries just now, to eliminate the truly uninteresting ones. &nbsp;&nbsp;
1542045620
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
While I appreciate what you're doing and think that you are probably more knowledgeable about general coding than I am, I see two problems with this route - using your anonymous script as my test case: The compiled code is extremely difficult to read. This is because of all the extraneous functions that your typescript wrapper puts in. Following the flow of the code is nearly impossible. This then feeds into my second issue If we look at lines of code the code is extremely inefficient. Anonymous is almost 2.4k lines of code. This is a huge script for something that is actually relatively straightforward to do in the API. As some comparison here's how anonymous relates to some of my scripts. This is right about the length of Roll20AM which controls the entire Jukebox and adds features to it. It's roughly 2/3 the length of the Pathfinder Companion script which does quite a few things, one piece of which is conceptually the same as anonymous. For what anonymous does, I would expect it to be roughly a quarter as long as it is (maybe less). I am not simply harping on script length because of some OCDness about what makes good code; because all API scripts are concatenated into one massive script which is then run&nbsp;altogether, having efficient code for your scripts ensures that all your scripts will be performant. Additionally, users take a risk when installing an API script; it would be relatively simple for an unethical scripter to write what seems like a useful script, but is actually something harmful to your campaign (thankfully we don't need to worry about scripts actually damaging our computers or personal files). Having scripts that are excessively long and difficult to make sense of makes it difficult for users not intimately familiar with your code to ensure that it does what they want it to do. I have enjoyed your contributions to the Roll20 scripting community, and I look forward to what else you'll contribute (not least because I've learned from many of your posts). Scott
Thanks for your kind words. I agree that the resulting output code is hard to read and this is a security problem, since users can't inspect the code before allowing it in their sandbox. &nbsp; As a professional developer (retired now,) I have never worked on a platform where the users end up reading the code, so I think that's where I went wrong. &nbsp; I might create a dependency thing where one module that is trusted provides all the basic functionality (error reporting, help generation, etc.) and the other plugins are just the small pieces that are plugin-specific. I did not go this route because it introduces version problems, if the "framework" module isn't the same version. &nbsp; When you suggested last time that I break it into dependent modules, that's why I didn't want to do it. &nbsp;I will give it some thought and maybe just make the output readable by stripping all the 'module' stuff in the meantime. &nbsp; It would mean that I have to check for collisions among globals, but I think I only have 3 globals so that should be fine :) Regarding performance, I don't agree at all. &nbsp;The only performance that is hurt by long scripts is maybe start up time, and even that is almost instantaneous. &nbsp; Script engines like Node.js and the embedded JavaScript used at Roll20 simply don't care about a few thousand lines of code. &nbsp;You should assume they can do 100,000 - 500,000 operations per second. &nbsp;The performance of scripts is actually entirely due to design. &nbsp; Do you store things in an indexed collection or do linear searches? &nbsp;Do you access attributes multiple times (fetching from Roll20 each time) instead of caching them during an operation etc. &nbsp; There is no way you will outperform a very long but well-engineered script with a shorter script, if the shorter script has less efficient design... Regarding "short scripts are better" that was simply never my design objective. &nbsp;I wanted to make a bunch of helpful stuff appear in every plugin "for free" so that it will actually be there. &nbsp;In my experience, scripts have no error reporting or survivability, because that simply isn't worth spending time on when you are scripting. &nbsp;So I wanted to create the opposite, where the scripter of a specific plugin still only write a small amount of code but gets a metric ton of other stuff for free. &nbsp;For example, if you access an undefined variable in one of my scripts, it unrolls the stack to a safe place so you don't crash the entire sandbox, it reports the stack (real names and line numbers like you taught me) and then presents a dialog that automatically fills in a github issue for you if you click on it. &nbsp; Also, you get options like debug output, verbose output, command echoing, dumping the entire state, etc. &nbsp;That requires 0 lines of code in the specific script. &nbsp;It isn't worth writing every time or even copy/pasting in. &nbsp; I know, this is over-engineering but that's what I like. &nbsp; I am pretty miffed that this project might fail because I did not consider the security impact of less readable scripts.
1542048833

Edited 1542049473
Ammo
Pro
Regarding size, just because I agree it is a bit crazy. &nbsp;Here is cloc for the code base ('make cloc' in the Makefile): ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- TypeScript 68 555 461 3785 JavaScript 6 47 45 426 JSON 17 2 0 170 make 1 3 0 83 CSS 1 15 2 67 Bourne Shell 1 3 0 29 HTML 3 0 0 24 Markdown 1 9 0 15 ------------------------------------------------------------------------------- SUM: 98 634 508 4599 ------------------------------------------------------------------------------- This is the code specific to 'anonymous': ./src/anonymous/character.ts 3 2 43 ./src/anonymous/configuration.ts 5 5 18 ./src/anonymous/images.ts 1 0 12 ./src/anonymous/main.ts 1 0 3 ./src/anonymous/reveal.ts 1 1 22 ./src/anonymous/set.ts 3 0 38 ./src/anonymous/tsconfig.json 0 0 1
[edit: had posted some code but will do that again when it is done and I can test again after the servers stop crashing]
1542314534

Edited 1542333590
Ammo
Pro
Ok Scott, I got carried away again... Some tens of hours of work later, I have the plugins compiling as little tiny blobs that import all the shared behavior from a single library script. &nbsp;There was a lot of stuff I needed to make this work while still keeping all the source code in many many TypeScript source files for source control purposes. This was a fun experiment to learn more TypeScript, but otherwise I just wasted some thousands of dollars in time :) Here is the resulting "anonymous" plugin in its entirety (compiled JavaScript output.) &nbsp; Note you may still not enjoy reading it, because it is written in a declarative style rather than the straight script style. &nbsp;That is so that I can keep upgrading the parser and make it better and more survivable without having to change every place in every script where it implements some command. &nbsp;This is particularly important in the "rewards" plugin which has a pretty big command language. /* * der20 anonymous plugin DER20 DEVELOPMENT BUILD * * Copyright 2018 Ammo Goettsch * * Discord: ammo#7063 * Roll20: <a href="https://app.roll20.net/users/2990964/ammo" rel="nofollow">https://app.roll20.net/users/2990964/ammo</a> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * <a href="http://www.apache.org/licenses/LICENSE-2.0" rel="nofollow">http://www.apache.org/licenses/LICENSE-2.0</a> * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var der20_anonymous = { version: 'DER20 DEVELOPMENT BUILD' }; ( function () { const match = new Error ( 'beginning of file marker on line 4' ). stack . match ( /apiscript.js: ( \d + ) / ); const markerLine = match ? parseInt ( match [ 1 ], 10 ) : 4 // adjust for license file and 2 version string lines let preambleLength = 18 + 2 ; if ( markerLine &lt; preambleLength ) { // license file is not prepended yet and we are running alone for testing return markerLine - 4 ; } der20_anonymous . scriptOffset = markerLine - ( 4 + preambleLength ); })(); on ( 'ready' , function () { // we only use simple decorations on properties, so we can use this simple implementation function __decorate ( decorators , target , key ) { for ( var i = decorators . length - 1 ; i &gt;= 0 ; i --) { let decorator = decorators [ i ]; if ( decorator !== undefined ) { decorator ( target , key ); } } }; let exports = {}; "use strict" ; /* ========================================================================= * src/plugins/anonymous/set.ts */ var defaultToken ; function setDefaultToken ( token ) { defaultToken = token ; } class SetCommand extends der20_library . SelectedTokensSimpleCommand { handleToken ( token , parserContext , tokenIndex ) { let character = token . character ; if ( character === undefined ) { return new der20_library . Success ( `selected token does not represent any journal entry and won't be changed` ); } if (! character . isNpc ()) { return new der20_library . Success ( `' ${ character . name } ' is not an NPC/Monster and won't be changed` ); } let anonymousName = character . attribute ( 'npc_type' ). get ( 'current' ); if ( anonymousName . length &gt; 0 ) { anonymousName = anonymousName . split ( / [ ,( ] / )[ 0 ]; } else { anonymousName = '' ; } if ( defaultToken === undefined ) { token . raw . set ({ name: anonymousName , showname: true , showplayers_name: true }); return new der20_library . Success ( `token for ' ${ character . name } ' changed to show only creature type; anonymous character to use as icon is not configured` ); } let midnight = new Date (); midnight . setHours ( 0 , 0 , 0 , 0 ); let cacheDefeat = ` ${ midnight . valueOf () / 1000 } ` ; let anonymousIcon = ` ${ defaultToken } ? ${ cacheDefeat } ` ; debug . log ( `setting token to: ${ anonymousIcon } ` ); token . raw . set ({ imgsrc: anonymousIcon , name: anonymousName , showname: true , showplayers_name: true }); debug . log ( `result after set: ${ token . image . url } ` ); if ( anonymousIcon !== token . image . url ) { return new der20_library . Failure ( new Error ( `token for ' ${ character . name } ' could not change image; the anonymous character has a marketplace image or otherwise restricted image` )); } return new der20_library . Success ( `token for ' ${ character . name } ' changed to show only creature type and anonymous icon` ); } } /* ========================================================================= * src/plugins/anonymous/character.ts */ class CharacterConfiguration extends der20_library . ConfigurationString { handleEndOfCommand ( context ) { // no item selected, present a dialog let dialog = new context . dialog ( ` ${ context . command } ` ); dialog . addTitle ( `Choose a character to provide the anonymous icon:` ); dialog . addSeparator (); let names = der20_library . Der20Character . all (). map (( character ) =&gt; { return { label: character . name , result: character . name }; }); dialog . addSelectionGroup ( 'character' , context . rest , names ); return new der20_library . DialogResult ( library_5 . DialogResult . Destination . Caller , dialog . render ()); } parse ( line , context ) { const imageKey = 'CharacterConfiguration_image' ; let imageSource = context . asyncVariables [ imageKey ]; if ( imageSource !== undefined ) { setDefaultToken ( imageSource . url ); // XXX we need some way to display this message even if verbose is not set, without introducing a new result type return new der20_library . Change ( `loaded anonymous icon from character ' ${ this . value () } '` ); } let result = super . parse ( line , context ); if (! result . isSuccess ()) { return result ; } let source = der20_library . Der20Character . byName ( this . value ()); if ( source === undefined ) { return new der20_library . Failure ( new Error ( `plugin requires a character named ' ${ this . value () } ' to provide default token` )); } return new der20_library . Asynchronous ( 'loading default token' , imageKey , source . imageLoad ()); } load ( json , context ) { super . load ( json , context ); let source = der20_library . Der20Character . byName ( this . value ()); if ( source === undefined ) { context . addMessage ( `plugin requires a character named ' ${ this . value () } ' to provide default token` ); return ; } context . addAsynchronousLoad ( source . imageLoad (), ( value ) =&gt; { setDefaultToken ( value . url ); }); } } /* ========================================================================= * src/plugins/anonymous/images.ts */ function makeImageSourceURL ( imageSource ) { if ( imageSource . includes ( '?' )) { return thumbify ( imageSource ); } let midnight = new Date (); midnight . setHours ( 0 , 0 , 0 , 0 ); let cacheDefeat = ` ${ midnight . valueOf () / 1000 } ` ; return thumbify ( ` ${ imageSource } ? ${ cacheDefeat } ` ); } function thumbify ( imageSource ) { return imageSource . replace ( / \/ [ a-z ] + \. png ( \? [ 0-9 ] + ) $ / , '/thumb.png$1' ); } /* ========================================================================= * src/plugins/anonymous/reveal.ts */ class RevealCommand extends der20_library . SelectedTokensSimpleCommand { handleToken ( token , context , tokenIndex ) { let character = token . character ; if ( character === undefined ) { return new der20_library . Success ( `selected token does not represent any journal entry and won't be changed` ); } if (! character . isNpc ()) { return new der20_library . Success ( `' ${ character . name } ' is not an NPC/Monster and won't be changed` ); } // because of request fan-out (selected tokens) we may have many images for which we are waiting const imageKey = `RevealCommand_image_ ${ tokenIndex } ` ; let imageSource = context . asyncVariables [ imageKey ]; if ( imageSource === undefined ) { return new der20_library . Asynchronous ( `loading default token info from ' ${ character . name } '` , imageKey , character . imageLoad ()); } token . raw . set ({ imgsrc: makeImageSourceURL ( imageSource . url ), name: character . name , showname: true , showplayers_name: true }); return new der20_library . Success ( `setting token to show its default name and image from ' ${ character . name } '` ); } } /* ========================================================================= * src/plugins/anonymous/configuration.ts */ class Configuration { constructor () { // standard plugin options this . options = new der20_library . Options (); // name of a character in the journal that will provide its default token image for anonymous tokens this . character = new CharacterConfiguration ( 'Anonymous' ); // set the selected tokens as anonymous this . set = new SetCommand (); // reveal the selected tokens' real identites this . reveal = new RevealCommand (); } // on reset (built in command provided by plugin), clear global state handleChange ( token ) { if ( token === 'reset' ) { setDefaultToken ( undefined ); } } } __decorate ([ der20_library . keyword ( 'option' ) ], Configuration . prototype , "options" , void 0 ); /* ========================================================================= * src/plugins/anonymous/main.ts */ new der20_library . Plugin ( 'anonymous' , Configuration ). start (); // redirect output to library console = console || {}; console . log = der20_library . consoleOutput ; debug = debug || {}; debug . log = der20_library . debugOutput ; // publish any exports we have Object . assign ( der20_anonymous , exports ); });
1542315887

Edited 1542315937
Ammo
Pro
I wrote a a new version of the stack trace rewriting thing that supports multiple source scripts being on the stack. &nbsp; See this stack trace (I threw a message containing the line number to check that it is correct, just for testing) in a release build, it would say the version number instead of "DER20 DEVELOPMENT BUILD".
Nice work. While a framework is overkill for anything I'd like to do, I think there's a lot of interesting pipelining code/scripts and utility code in here for people looking to make modular API scripts from TypeScript sources. Thanks!
jmlane said: Nice work. While a framework is overkill for anything I'd like to do, I think there's a lot of interesting pipelining code/scripts and utility code in here for people looking to make modular API scripts from TypeScript sources. Thanks! That's kind of you to throw me some love :). &nbsp; There are still zero other people interested...
1544383762
GiGs
Pro
Sheet Author
API Scripter
Ammo said: That's kind of you to throw me some love :). &nbsp; There are still zero other people interested... Here is my unfortunate assessment: The vast majority of people coding for roll20 are self-taught novices to whom javascript is already new and mysterious, and adding another approach on top of it is an unwanted extra step. Scott mentioned how hard the code is to read - for this group (me among them), code needs to be easy to understand. Most of the remainder are very competent coders who already have their own ways of doing things and a catalog of scripts they have already to draw on for new projects, and switching to a new approach just doesnt offer them an advantage for the time cost of adapting.
I see it the same way. &nbsp; &nbsp;The only way someone else is gonna want to collaborate with me is if they are a Java/Swift or maybe C++ programmer and they are just starting out with Roll20 API Scripts but want to build huge applications in them for some reason :) Statistically, &nbsp;I am probably going to continue to be the only one who matches that description. &nbsp; I have made my peace with that, and I am instead just working on my largest-UI-ever-in-an-API-script :) GiGs said: Here is my unfortunate assessment: The vast majority of people coding for roll20 are self-taught novices to whom javascript is already new and mysterious, and adding another approach on top of it is an unwanted extra step. Scott mentioned how hard the code is to read - for this group (me among them), code needs to be easy to understand. Most of the remainder are very competent coders who already have their own ways of doing things and a catalog of scripts they have already to draw on for new projects, and switching to a new approach just doesnt offer them an advantage for the time cost of adapting.