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

Using die roller functionality in an API script _without_ using chat functions?

April 25 (9 years ago)
So, I have been searching for the past few days, both on this forum and in Github and I have not been able to find the answer to a very specific question, so I am hoping posting this will help.

Background: I am what you might consider an advanced amateur with Roll20 and JavaScript. Most of my programming background comes from C++, so I am able to read and understand code, if not necessarily the best Java/Javascript specific tips and tricks.

Goal: I am trying to find a way, completely in an API script and without using chat, to be able to process complicated die rolls using the same format as what is used with the /roll chat function and inline rolls (I.e. /roll 4d6k3 to get the sum of the top 3 results of 4 d6s).

Reason: While I have found a way to send inline rolls to the chat and get the result back into variable for later use in the script, the process seems very clunky and causes lag with multiple players. I would like to cut out what I believe the major time crunch of passing/getting info from the chat window.

Example:
function diceprocessor (diceformula) {
   var diceresult;
 
   // something here that would take diceformula, as a String or an array as needed, process it to diceresult and returning diceresult as Integer or String, as needed.

   return diceresult;
}

The best method I can think of right now is parsing diceformula, character by character and use a loop of case statements, but this feels like I am reinventing the wheel, since I know the chat system can already do this.

Any help would be appreciated.

- Dan
April 25 (9 years ago)
The Aaron
Pro
API Scripter
There is nothing created for parsing die rolls without sendChat(). You'd need to write your own recursive descent parser for dice expressions. 

That said, perhaps you're using sendChat() in a less optimal manner. It can be used as an asynchronous function by passing a callback as the third parameter. If you were calling it with only 2 parameters and picking up the results from a 'chat:message' event, it would be painfully slow. If you post some code, we can help you with it. 

Also, C++ FTW!  I'm a C++ programmer trapped in a web world! =D
April 25 (9 years ago)
Thanks,

Below is the code that I found for process the inline rolls that are entered from the chat window and then stored in a msg.content. I added some log functions to see what was going on as the code progressed.

on("chat:message", function (msg_orig) {
	//log(msg.content);
	var msg = _.clone(msg_orig);

	if(_.has(msg,'inlinerolls')){
		msg.content = _.chain(msg.inlinerolls)
			.reduce(function(m,v,k) {
				m['$[['+k+']]']=v.results.total || 0;
				return m;
			} ,{})
			.reduce(function(m,v,k) {

				log("K: "+k);
				log("V: "+v);


				return m.replace(k,v);
			} ,msg.content)
			.value();
	}
	var args = msg.content.split(" ");
	var command = args.shift();


	log ("Command: "+command);
	log ("Arguments: "+args);
    	log ("Message Content: " + msg.content);
});
Below is the code I have written so far to send requests back to the chat. Personal preference for making function definitions with as few repeating variables.

var Chat = Chat || {};                                                          // Namespace for Chat Interface class
Chat.new_class = function() {                                                   // Declaring Chat Interface class
    this.AsGM = function(message) {sendChat("GM",message);};                    // Send to Chat as GM
    this.AsAPI = function(message) {sendChat("API",message);};                  // Send to Chat as API
    this.AsPlayer = function(msg,message) {sendChat(msg.who,message);};         // Send to Chat as Player
};
var APIChat = new Chat.new_class();  
And here is an example the code style that gets called later on.
function ParseDiceFormula(diceformula) {                                          // Parses the die formula

    var testdiceformula = "[[4d6k3+3d6+15]]";
    var testdiceresult = 0;

// somehow get the string testdiceformula to resolve like an inline roll and put that in testdiceresult.

    APIChat.AsGM("Test Result: "+testdiceresult);
    return testdiceresult;
}


If I am understanding where you are going, I would probably would be adding a new function to Chat class, something like??
this.AsInlineProcessor = function(message) {
	sendChat("InlineProcessor", message, function(op) {
    	// ops will be an ARRAY of command results.
   	 var rollresult = ops[0];
    	//Now do something with rollresult, just like you would during a chat:message event...
	}
});

April 25 (9 years ago)
Lithl
Pro
Sheet Author
API Scripter

tzizimine said:

If I am understanding where you are going, I would probably would be adding a new function to Chat class, something like??
this.AsInlineProcessor = function(message) {
	sendChat("InlineProcessor", message, function(op) {
    	// ops will be an ARRAY of command results.
   	 var rollresult = ops[0];
    	//Now do something with rollresult, just like you would during a chat:message event...
	}
});
Something like that, although since sendChat is an asynchronous function this wouldn't be able to do something like return a value. I would probably write it something like this:
this.AsInlineProcessor = function(message, callback) {
sendChat('', message, function(op) { callback(op[0]); });
};
You would then later use it like this:
APIChat.AsInlineProcessor(testdiceformula, function(rollresult) {
// rollresult is the result of testdiceformula being rolled
});
April 25 (9 years ago)
The Aaron
Pro
API Scripter
(What Brian said. =D )
April 25 (9 years ago)
Thanks.

Do we know what format the rollresult will be after this is ran? String, array of strings, etc? Is there anything special I need to know get to get to the actual integer result of the testdiceresult?
April 25 (9 years ago)

Edited April 25 (9 years ago)
The Aaron
Pro
API Scripter
It will be just as if it were the msg argument from:
on('chat:message', function(msg){ /*...*/ });

April 25 (9 years ago)
Unfortunately, I haven't figured out for certain what that is. That first function that I found to process inline roll confuses me, to be honest. I know it works, but I don't know how it works. I know the final integer result comes out as msg.content. Would that mean that the final integer result I need from this would be rollresult.content?
April 25 (9 years ago)

Edited April 25 (9 years ago)
Lithl
Pro
Sheet Author
API Scripter
The parameter in the callback function to sendChat is an array of message objects; by and large, it only has a single element (and I think the use-case for when it would have multiple elements is actually bugged, I haven't checked in a while), which is why my example code simply used the first element from it. The message objects it gets are just the like the message object for chat events, as Aaron said.

The structure of the message object is listed on the wiki: https://wiki.roll20.net/API:Chat#Chat_Events
April 25 (9 years ago)
The Aaron
Pro
API Scripter
I actually wrote this function, so I can walk you through it.
	if(_.has(msg,'inlinerolls')){

		// _.chain() starts a call chain of methods, the output of one is the input to the next
		msg.content = _.chain(msg.inlinerolls)

			// .reduce() builds a new state for the chain, in this case, an object
			.reduce(function(m,v,k) {

				// m is the memo, the current object being built
				// v is each of the rolls in the message
				// m will have properties for each of the inline rolls which 
				// maps the embedded key to the total for the roll. example:
				//   $[[0]] -> 5
				m['$[['+k+']]']=v.results.total || 0;

				// you must always return the memo in reduce for the next iteration
				return m;
			} ,{}) // {} is the starting m

			// this reduce replaces each of the $[[#]] in the original message content with
			// the rolled total.
			.reduce(function(m,v,k) {

				log("K: "+k);
				log("V: "+v);

				// m is the msg.content with each prior $[[#]] replaced.
				// k is the $[[#]] for the current iteration
				// v is the corresponding total
				return m.replace(k,v);
			} ,msg.content) // msg.content is the starting m

			// .value() takes whatever is in the chain and returns it, in this case
			// setting msg.content to the copy with all the inline rolls substituted.
			.value();
	}
You can read about _.chain() and it's ilk on http://underscorejs.org
April 25 (9 years ago)
hmmm, ok, I think I have found something else then I am overlooking.

Here is the code now. The first on("chat... function I posted remains unchanged:
var Chat = Chat || {};                                                          // Namespace for Chat Interface class
Chat.new_class = function() {                                                   // Declaring Chat Interface class
    this.AsGM = function(message) {sendChat("GM",message);};                    // Send to Chat as GM
    this.AsAPI = function(message) {sendChat("API",message);};                  // Send to Chat as API
    this.AsPlayer = function(msg,message) {sendChat(msg.who,message);};         // Send to Chat as Player
    this.AsInlineProcessor = function(message, callback) {
        sendChat('', message, function(op) { callback(op[0]); });
    };
};
var APIChat = new Chat.new_class();


function ParseDiceFormula(diceformula) {                                          // Parses the die formula
    var testdiceformula = "[[4d6+15]]";
    var testdiceresult = 0;
    
    APIChat.AsInlineProcessor(testdiceformula, function(rollresult) {
        // rollresult is the result of testdiceformula being rolled
        log("1 Roll Result Type: " + typeof rollresult);
        log("2 Roll Result Content:: " + rollresult.content);
        log("3 Roll Result" + rollresult);
    });
    
    log("4 Test Dice Result:" + testdiceresult);
    return testdiceresult;
}

However, the log result comes like.

"4 Test Dice Result:0"
"1 Roll Result Type: object"
"2 Roll Result Content:: $[[0]]"
"3 Roll Result[object Object]"

I am assuming that since it is asynchronous, there must be something else necessary to make the rest of the program wait for the inline roll result to come back?
April 25 (9 years ago)

Edited April 25 (9 years ago)
Lithl
Pro
Sheet Author
API Scripter
You can't really make a function that returns a value which also depends on the result of an asynchronous process to calculate that return value. That's why the AsInlineProcessor function I wrote takes a callback, instead of returning a value. Your ParseDiceFormula function could do the same thing.

I mean, I suppose you could have a busy-wait loop, but with very few exceptions that's a bad idea.
April 25 (9 years ago)
Hmm, then I think this might be where I am running into a stumbling block.

The on("chat... function I first posted seems to do everything in a single call, so even if it is asynchronous, it is processed internally sequentially, so I always get a die result.

Doing the same thing from inside of a script function, being asynchronous, means that the die result comes too late to be processed by the rest of the function.

So, I guess the next question is just how bad of idea is the busy-wait loop? And how would that be done within the limits of an API script?
April 25 (9 years ago)

Edited April 25 (9 years ago)
The Aaron
Pro
API Scripter
In the original function you posted, the dice roll already took place:
1) command executed in the chat window
2) << Async processing >>
3) 'chat:message' event occurred. 

All the above function is doing is taking what's already provided and merging it into one place, convenient for supporting inline rolls as arguments to a command.  msg is something like:
{
  content: 'some string with $[[0]] and $[[1]] inline',
  inlinerolls: {
    0: { /* all the actual rolls and a result with a total },
    1: { /* all the actual rolls and a result with a total },
  }
 
Despite Brian saying you could do a busy wait, it's more of a "technically" sort of thing.  Javascript is single threaded and can only be fast by having asynchronous behavior; Once you add in a busy wait, it kills performance to the point where your API script would likely cause the Sandbox to die and at a minimum would cause everything else running on it to hang until that one result finished.

You are far better off with the asynchronous behavior.  It's not so bad once you get used to it (it took me a while to get the hang of it from C++, to be sure!).  This should do what you want without needing a busy wait:
function ParseDiceFormula(diceformula) {                                          // Parses the die formula
    var testdiceformula = "[[4d6+15]]";
    var testdiceresult = 0;

    var done=function(testdiceresult){
    	log("4 Test Dice Result:" + testdiceresult);
    	return testdiceresult;
    }
    
    APIChat.AsInlineProcessor(testdiceformula, function(rollresult) {
        // rollresult is the result of testdiceformula being rolled
        log("1 Roll Result Type: " + typeof rollresult);
        log("2 Roll Result Content:: " + rollresult.content);
        log("3 Roll Result" + rollresult);

	done(rollresult);
    });
}
Though generally, you'd either just inline that functionality where done() is called, or you'd put the entire functionality in a function and pass that into AsInlineProcessor().

Anonymous functions are the idiomatic way to get many things done in Javascript.
April 25 (9 years ago)
Ok, good to know. I have added the code you presented and I am getting the log events in the right order. However, I am still running into a problem I am not getting the final result in the right format.

"1 Roll Result Type: object"
"2 Roll Result Content:: $[[0]]"
"3 Roll Result[object Object]"
"4 Test Dice Result:[object Object]"
I am certain it has something to do with how the information comes back in rollresult and how I am referencing it. Would it be safe to use the code from the on("chat... function with rollresult?
April 25 (9 years ago)
The Aaron
Pro
API Scripter
It should be. 

Try using this log statement:
log("4 Test Dice Results: "+JSON.stringify(testdiceresult));
April 25 (9 years ago)
The Aaron
Pro
API Scripter
JavaScript objects effectively have:
operator std::string() {
  return "[object Object]";
}
April 25 (9 years ago)
Awesome!! That produced:

"4 Test Dice Results: {\"who\":\"\",\"type\":\"general\",\"content\":\"$[[0]]\",\"playerid\":\"API\",\"avatar\":false,\"inlinerolls\":[{\"expression\":\"4d6+15\",\"results\":{\"type\":\"V\",\"rolls\":[{\"type\":\"R\",\"dice\":4,\"sides\":6,\"mods\":{},\"results\":[{\"v\":5},{\"v\":5},{\"v\":4},{\"v\":1}]},{\"type\":\"M\",\"expr\":\"+15\"}],\"resultType\":\"sum\",\"total\":30},\"signature\":\"aaae9dbba8d14ef53feb7efb47a8a28c36541493d93f7177cbedc593ac8254ae1fbd52bbd0b27080720fc30b3a5b4236abca4197dee1ea47a7150185a02aeb30\",\"rollid\":\"-KGDe886UFm4sk17Wy3O\"}]}"	
Can I assume that with a few replacing of variables, the _chain function could read this and through iterative reduce(), return the final result? Or would stringify-ing it cause an issue with it?
April 25 (9 years ago)
The Aaron
Pro
API Scripter
No, you only need to do that to make it show up in the log concatenated. That above function is already accessing those properties. 

You could also do:
log(testdiceresult);
to see the same sort of thing without the escapes. 
April 25 (9 years ago)
Awesome. Thank you both very much. I have what I needed.

Happy gaming. 
April 25 (9 years ago)
The Aaron
Pro
API Scripter
Thanks!  Happy Rolling to you as well!

Definitely post back if you have any questions.  =D
April 25 (9 years ago)
Actually I spoke too soon... Running into that asynchronous issue, but in a different way now. See the bolded areas of code.

function ParseDiceFormula(diceformula) {                                          // Parses the die formula
    var testdiceformula = "[[4d6+15]]";
    var testdiceresult = 0;

    var done=function(testdiceresult){
    	log("4 Test Dice Result:" + testdiceresult);
    	return testdiceresult; 							// <---- Here is fine
    }
    
    APIChat.AsInlineProcessor(testdiceformula, function(rollresult) {
        // rollresult is the result of testdiceformula being rolled
        log("1 Roll Result Type: " + typeof rollresult);
        log("2 Roll Result Content:: " + rollresult.content);
        log("3 Roll Result" + rollresult);

	done(rollresult);
    });

    return testdiceresult;							// <---- Here is still 0
}


//.... separate section of code in a different function
	Damage_Formula = "[[4d6+15]]";
	Damage_Result = ParseDiceFormula(Damage_Formula);			// This still comes back with 0.
//....

So I am still running into a situation where by the end of the function, testdiceresult is still 0 because the asynchronous call is still working. Any ideas?
April 25 (9 years ago)

Edited April 25 (9 years ago)
The Aaron
Pro
API Scripter
You won't be able to return a value from ParseDiceFormula.  You must instead pass in the work to be done with the result, in the form of a function to call.  That function then picks up the thread of execution at such time as the value is available.  If you're calling this from a chat:message handler, the last thing you'll do in the handler function with regard to processing that roll is passing your callback into that function.  Unrolled, this is what's happening:
function ParseDiceFormula(diceformula) {                                          // Parses the die formula
    var testdiceformula = "[[4d6+15]]";
    var testdiceresult = 0;

    // Request APIChat.AsInlineProcessor() be called
    return testdiceresult;							// <---- Here is still 0

    // some time later... dice results are available 
    {
        // rollresult is the result of testdiceformula being rolled
        log("1 Roll Result Type: " + typeof rollresult);
        log("2 Roll Result Content:: " + rollresult.content);
        log("3 Roll Result" + rollresult);

    	log("4 Test Dice Result:" + testdiceresult);

        // this goes nowhere as the function calling it doesn't use the return result and it happens much later anyway.
    	return testdiceresult; 							// <---- Here is fine
    }
}
If it helps, think of this as the Command Pattern you might use in C++.  Sometimes referred to as the Hollywood Pattern (don't call us, we'll call you!).  You're passing in an operation to be performed when the data is available, rather than requesting the data and operating on it.
April 25 (9 years ago)
Ok, I think I understand what you are referring to. But since that's the case, would the following work?

function ParseDiceFormula(diceformula,diceresult) {                          // Parses the die formula
    var testdiceformula = "[[4d6+15]]";
    var testdiceresult = 0;

    var done=function(testdiceresult){
    	log("4 Test Dice Result:" + testdiceresult);
    	diceresult = testdiceresult; 						// give the final value
    }
    
    APIChat.AsInlineProcessor(testdiceformula, function(rollresult) {
        // rollresult is the result of testdiceformula being rolled
        log("1 Roll Result Type: " + typeof rollresult);
        log("2 Roll Result Content:: " + rollresult.content);
        log("3 Roll Result" + rollresult);

	done(rollresult);
    });

    return testdiceresult;							// <---- Here is still 0
}


//.... separate section of code in a different function
	Damage_Result = 0;
	Damage_Formula = "[[4d6+15]]";
	ParseDiceFormula(Damage_Formula, Damage_Result);			
	log("Damage Result: "+Damage_Result);					// <---- Would this result with 0? or Result?
//....
Something about the scope of the variable seems like this might not work, but I haven't had a lot of practice with Command Patterns.
April 25 (9 years ago)
The Aaron
Pro
API Scripter
That would not work.  When you call ParseDiceFormula(), it returns immediately, then the log function will be called.  If there were a significant amount of time between ParseDiceFormula() and the log() call, Damage_Result would eventually have the value (pretty sure all things are passed by reference in Javascript), but you don't want to wait around for that.  Using a callback will allow you to take advantage of it the instant it's ready.

Maybe it would help to consider ParseDiceFormula() as sending a message to another process (or thread).  There'd be no point in busy waiting the first thread to wait for the second to finish (why be multi threaded at that point?).  Give the other thread something to execute when it's ready, and then let the current thread pass on to other tasks.
April 25 (9 years ago)
The only problem with handling other tasks while the script is waiting is that there will be other functions that really shouldn't be called until we have Damage_Result holding the value of that Damage_Formula. Is there a way of knowing when Damage_Result has been updated so we know when it is safe to call those other functions?

i.e. Don't call us, we'll call you. then you can call the director? 
April 25 (9 years ago)
The Aaron
Pro
API Scripter
So long as you encapsulate those other things in a function, and pass it as a callback, or call it from your passed in callback, there isn't a problem.

I remember this being a struggle for me when I first started working with Javascript in any great depth.  Asynchronous calls drove me crazy for a long time as it's a definite paradigm shift from the traditional Procedural Programming I'd done in C++ to get into the more Functional Programming style in Javascript.  Saddly, I don't remember what finally gave me the ah-ha moment, or I'd share it. =/

One thing that helps you out is that functions have access to everything in the scope that created them, so you could write this and it works just fine, unlike doing something similar in C++:
on('ready',function(){
    "use strict";
    
    on('chat:message',function(msg){
        var cmds,others;
        if(msg.type !== 'api'){
            return;
        }

        cmds = msg.content.split(/\s+/);
        if('!dostuff'===cmds[0]){
            others=function(result){
                sendChat('DoStuff','Hey, I got a result: '+result);
            };
            sendChat('','[[1d20+3]]',function(msg){
                res=msg[0].inlinerolls[0].results.total;
                sendChat('DoStuff-Callback','In do stuff!');
                others(res);
            });
        }
    });
});

Incidentally, I can't recommend highly enough the book Javascript: The Good Parts by Douglas Crockford.  I found it immensely helpful in learning the ins and outs of Javascript.  It's a pretty quick read and has some great reference material.
April 26 (9 years ago)

Edited April 26 (9 years ago)
Lithl
Pro
Sheet Author
API Scripter

The Aaron said:

If there were a significant amount of time between ParseDiceFormula() and the log() call, Damage_Result would eventually have the value (pretty sure all things are passed by reference in Javascript)
Primitives are pass-by-value. Objects are also pass-by-value, but the value being passed is the value of the reference (essentially passing the pointer).
function example(p, o1, o2) {
p = p + 1;
o1 = { v: o1.v + 1 }; o2.v = o2.v + 1;
}

var a = 1, b = { v: 2 }, c = { v: 3 };
example(a, b, c);
console.log(a, b, c);
// 1 => unchanged
// { v: 2 } => unchanged
// { v: 4 } => changed
In short, you can mutate object parameters. You cannot assign object or primitive parameters (and have that assignment be meaningful outside of the function).