Scripting patterns for fun and profit


#1

This thread is intended to be a place to document and discuss good (or at least working) scripting patterns for various HiFi uses. There are lots of examples at http://public.highfidelity.io/scripts as well as in the forum thread https://alphas.highfidelity.io/t/fun-and-useful-js-scripts

ONE SHOT SCRIPTS

To get the ball rolling, the typical “one shot” script seems to be:

function aOneShotScript() {
  // do lots of cool stuff here
  // ...
  // then stop the script when we're done
  Script.stop();
}
// the following starts aOneShotScript
Script.update.connect(aOneShotScript);

But, there is a major problem: if anything in the body of the script throws an exception, the Script.stop() will not get called, leaving the script alive.

For a more robust solution that still works with the original registration model, try this:

function myOneShotScript() {
  try {
    // do lots of cool stuff here
    // ...
  } catch (err) {
    // oops. something horrible happened
    print(err.message);
  }
  // then stop the script when we're done
  Script.stop();
}
// the following starts myOneShotScript
Script.update.connect(myOneShotScript);

Question: why not do the following instead?

function mySimplerOneShotScript() {
  // do lots of cool stuff here
}
try {
  mySimplerOneShotScript();
} catch (err) { print(err.message); }
Script.stop();

CONTINUOUSLY RUNNING SCRIPTS

Of course, you can let it continue. Note that update registered functions are passed an an argument that indicates how much time has passed (since the last invocation?).

function myResidentScript(timeDelta) {
  // this will run periodically, just before each data send to the server
  stunkyFluff();
}
Script.update.connect(myResidentScript);

You can also register a function to run when the script exits (this is from the example script count.js, add this to the myResidentScript above to get a log message when it exits:

function scriptEnding() {
    print("SCRIPT ENDNG!!!\n");
}

// register our scriptEnding callback
Script.scriptEnding.connect(scriptEnding);

QUESTIONS

  • The QT scripting engine used here seems to (by default, at least) have a much reduced stack size (and maybe total memory) limit than your typical browser. What are the practical limitations and tradeoffs? Perhaps interface execution should be less limited than particle execution?
  • Are world operations such as Voxel.setVoxel() immediate from the perspective of a running script? Edit: There’s something wrong there - I’m “losing” voxels seemingly randomly…

#2

The “alreadyRunning” stuff for the oneshot-Scripts is not necessary as new calls from update get delayed until the current call is left.

Example:

print("### START ###");

var alreadyRunning = false;

function myOneShotScript(lastTime) {
if (alreadyRunning)
{
print("already running "+lastTime);
return;
}
else
{
print("first run "+lastTime);
alreadyRunning = true;
}

try
{
for (var x=0;x<100000000;x++)
{
var a = 1/27*a+x;
}
}
catch (err)
{
print("ERROR: "+err.message);
}

Script.stop();
print("### STOP ###");
}

Script.update.connect(myOneShotScript);

This code just outputs a single “first run”, looping with some dummy load long enough to possibly receive several reinvocations. No “already running” output proves that they are not happening at all.

From my tests I could show that even if you connect several functions at the same time with the nonblocking call to connect they get executed one after another in the order they got connected. Looks like there is only a single thread behind the script doing the work, and therefore no real concurrency. So calling connect multiple times in fact queues the invocations having only one function running at a time.


Question: why not do the following instead?

Using the “connect”-pattern even for oneshot scripts makes IMHO reading of scripts easier, because there is a single pattern within all scripts for function invocation. Doing some initialisation, defining all functions and finally invoking the entry point seems to be a good standard pattern that fits all sizes.


#3

This example shows how to get some pseudo parallel execution within one script:

print("### START ###");

var time1 = 0;
var delta1 = 0.05;
var time2 = 0;
var delta2 = 1.0;

function myScript1(lastTime) {
try
{
time1=time1+lastTime;
if (time1>=delta1)
{
time1=time1-delta1;
print("TICK1 “+delta1+” / left over: "+time1);
}
}
catch (err)
{
print("ERROR: "+err.message);
}
}

function myScript2(lastTime) {
try
{
time2=time2+lastTime;
if (time2>=delta2)
{
time2=time2-delta2;
print("TICK2 “+delta2+” / left over: "+time2);
}
}
catch (err)
{
print("ERROR: "+err.message);
}
}

Script.update.connect(myScript1);
Script.update.connect(myScript2);

Its a bit like the pattern used for multiple interrupt functions that get run by a single processor: Have them doing a small and fast chunk of work, leaving their call fast while the “outer system” calls them again and again gives the illusion of parallelism.

If one wants more parallelism (or whatever the “outer system” supports of that) one has to run multiple scripts.


#4

•Are world operations such as Voxel.setVoxel() immediate from the perspective of a running script? Edit: There’s something wrong there - I’m “losing” voxels seemingly randomly…

That maybe due to calling them to fast. See: https://worklist.net/19875


#5

Is it wrong that I giggle when I consider that somewhere in SL, cubes are randomly APPEARING


#6

Looking back on my experiments, it appears that the issue I was having was due to uncaught exceptions. E.g. The following is an anti-pattern.

function foo() {
   print("Starting foo");
   doSomethingThatMightError();
   Script.stop();
}
Script.update.connect(foo);

I kinda think that an uncaught exception during execution of a connected script ought to stop the script, but this is not what happens. Instead you get continuous recalls of foo() until doSomethingThatMightError() happens to complete.

I’ll update my original post accordingly.


#7

That maybe due to calling them to fast. See: https://worklist.net/19875

There are certainly some networking bugs that need to be solved: voxel operations must be dependable. If we’re going to be using UDP, a dependable protocol needs to be implemented on top. That said, if the problem is triggered by “too fast”, it might not be the networking itself. If I’m reading the code right, updates get sent out every 200msec: I wonder what happens if your script is still queuing voxel operations after 200msec of runtime (especially with a mixture of creates, destroys, and examinations).