ExecuteScript from within an ExecuteScript - good idea? What about that "it has no delay" bit?

It currently seems to me that it would be the best solution - or at least, a solution that should probably work and lies within my ability - to script a certain cutscene that way, and I’m wondering if this is a problem or not.

What I want to do is have a script that fires a cutscene when the PC enters a certain area by ExecuteScript “cutscene_1.” And then have that script do several things that are the same no matter which cutscene variant is supposed to happen (mostly camera movement) and, after that, for that script to go "if (condition a) ExecuteScript “cutscene_1a” or "if (condition b) ExecuteScript “cutscene_1b” and so on.
(This mostly because the variants are different enough that I have no idea how to code them well in a single script, despite Tarot already trying to teach me that once before. Hi, Tarot! Sorry, Tarot!)

But there are a few problems. Firstly, I don’t really know whether this can work that way, and if it can, whether it’s a good idea or some CPU–destroying nightmare. Secondly, the lexicon says that the ExecuteScript function has no delay and fires immediately when called, not after the calling script ends. Does that mean it will be faster than the things that come above it in the calling code? If I put my camera movements above the ExecuteScript bit in my code, will they happen before calling the second level of scripts, like intended, or will it all get jumbled up and horrible?

Nested ExecuteScript commands are fine.

A special case - the recursive script - is where a script calls itself.

For example, a pseudo-heartbeat is a script that calls itself (say) once a second to do stuff that can’t wait for the regular heartbeat event.

Cutscenes, too, are easier to manage as a recursive script in my experience. Each stage can be coded within a switch > case, using a local int to determine which step is required next. This makes it easier to control the timing. It also gives the builder more control if the player escapes from the scene - you just set the int to a high value that tells the script to stop and clean up next time it runs.

Of course, you can nest a call to a different script, if convenient.

You do need to prevent scripts running new ones forever,. For a cutscene, it’s easy - when we reach the last step, or the players escapes, the script just needs to stop. Some builders have pseudo-heartbeats that run indefinitely, but, being a scaredy cat, I only allow mine to run until the next official heartbeat. In general, a recursive script needs to recognise some condition that prevents it running another instance.

You may need to delay the nested ExecuteScript. For, say, a 1 second pseudo-heartbeat, DelayCommand is perfect.

A neater method for cutscenes is often ActionDoCommand(ExecuteScript(…)). For example, if an NPC is given an action to walk to a waypoint, and the next action in their queue is to run the script, you don’t need to know how long the walking takes - the script just runs when they arrive.

Here’s a video of a cutscene in which literally every step is a recursive script call:
https://neverwintervault.org/video/dark-energy-courtly-dances

3 Likes

You don’t need to delay all ExecutedScripts. You can do up to, I think, six maybe seven levels of ExecuteScript.

Delaying them does allow you do do more than that.

It depends on what you are doing, though. If you need the original script to continue after the
executed one then you can’t delay it.

Edit: that was not clear… I meant that if you need the results of the executed script in the original script you can’t delay it. Firing off totally independent scripts with delays is fine… sorry about that.

In a cutscene you might as well delay them since there’s a lot of that already.

2 Likes

Thank you! Yet another stumbling point might now be cleared. Eventually. When I get to it.

@Proleric, that video is very impressive, and man, does it bring back some fantastic memories. Both the music (that’s BG, right?) and the dance itself. I did that sort of thing once or twice, it’s super fun.

Warning: long post. Tested with 1.69.

8 actually. After that, the recursion stops silently. Worst kind of error to handle:


Below are results of extra research of this subject. It shows how you can avoid TMI if you need to call a lot (thousands) of subroutines.

I'm using this MWE for demonstration (used as OnPlayerChat handler) - click to show.
void main()
{
    int i;
    int j;

    i = GetLocalInt(OBJECT_SELF, "recursion") + 1;

    SendMessageToPC(GetFirstPC(),
        "Current recursion depth is: " + IntToString(i));

    if(i >= 1000)
    {
        // Prevent the game from hanging up
        return;
    }

    while(j++ < 10000)
    {
        // Do a lot of instructions to raise up to TMI level
        // Note: this was not present on picture above
        GetModule();
    }

    SetLocalInt(OBJECT_SELF, "recursion", i);

    // Try any of these recursive call methods:
    ExecuteScript("chat", OBJECT_SELF);
    //DelayCommand(0.0, ExecuteScript("chat", OBJECT_SELF));
    //DelayCommand(0.01, ExecuteScript("chat", OBJECT_SELF));
}

The script just executes itself 1000 times once started.

The ExecuteScript("your_script", OBJECT_SELF) method

You can chain script execution without delays up to 8 times. From the VM perspective they are treated as one call. Or at least from programmer’s perspective the outcome is:


You hit the TMI. End of line. Corollary: don’t use this method with loop-heavy scripts or if you have more than 8 of them.

The DelayCommand(0.0, ExecuteScript("your_script", OBJECT_SELF)) method

Using delay 0.0 (no delay) you are absolved from TMI limits and can run the script recursively as “one call”. Of course this will slow the game down considerably if each script call carries a large computational load and may hang the game if not stopped at all, so handle with care:


Stopped at artifical limit of 1000 - safe to assume it can run forever.

The DelayCommand(0.01, ExecuteScript("your_script", OBJECT_SELF)) method

Works just like the previous one, but is slower due to the delay. It’s the safest approach when completion time is not an issue.

2 Likes

8… thanks :slight_smile:

The failure mode is horrible. It took me a fair bit of debugging the first time I hit that.

Yes, direct executes count as one script for TMI. But on the other hand as I said, if you are executing the script to calculate a value or whatever that you need in the current script you can’t delay.

Yeah, not much of improvement from 6 :stuck_out_tongue: I agree on your second paragraph. Silent errors are the antithesis of good programming. But there is something interesting…

And the Lexicon is correct: when the script is ran via bare ExecuteScript("blah", ...) it is executed before calling script terminates. Running it through DelayCommand(0.0, ExecuteScript("blah", ...)) will execute it after calling script terminates. Yes even with 0.0 delay (it’s treated as “different” script entity - as shown above).

However it seems the 8 limit is related only to recursive self-calls (@meaglyn), because you can run any number of non-delayed ExecuteScripts in a flat loop (subject to TMI of course):

MWE used to generate this figure:

Click to show master script.
void main()
{
    int i;
    int j;

    // Will execute BEFORE master terminates
    while(j++ < 100)
    {
        ExecuteScript("slave", OBJECT_SELF);
    }

    // Will execute AFTER master terminates
    DelayCommand(0.0, ExecuteScript("slave", OBJECT_SELF));

    i = GetLocalInt(OBJECT_SELF, "recursion");

    SetLocalInt(OBJECT_SELF, "recursion", i + 1);

    SendMessageToPC(GetFirstPC(), "Variable value from master script: " + IntToString(i));
}
Click to show slave script.
void main()
{
    int i;

    i = GetLocalInt(OBJECT_SELF, "recursion");

    SetLocalInt(OBJECT_SELF, "recursion", i + 1);

    SendMessageToPC(GetFirstPC(), "Variable value from slave script: " + IntToString(i));
}

It does not cause race condition, but it’s trivial to run into one when one is not careful.

1 Like

I have no idea what any of this means, but maybe one day I will come back to this post and get it.
Life goals!

It’s not limited to “self” calls. i.e. does not have to be a script recursively calling itself. But yes it has to be a chain. Just doing one in a loop is not the same thing.

ExecuteScript(“foo”);

foo:
ExecuteScript (“bar”);

bar:
ExecuteScript(“fubar”);

fubar:
ExecuteScript(“snafu”);

etc.

Naturally. It was only a simplification to avoid confusing the reader since the first example script calls itself.