Plan for script multitasking

From OHRRPGCE-Wiki
Jump to: navigation, search

The thoughts here are messy and confused! Please help sort them out by providing feedback!

This will be implemented at the same time as moving to the new script interpreter. In fact, the aim is to get the new interpreter and multitasking usable before enabling things like arrays, so that they can be merged separately.

Each triggered script will be given its own call stack. So a map autorun script containing a loop will be able to check if the player has left the map so it should stop, even if another looping map autorun script was triggered. This will of course break a great many games, so it will be turned on by a general bitset which will be off for old games and on for new games.

Original thoughts started at this thread: http://www.castleparadox.com/ohr/viewtopic.php?t=4239

Contents

[edit] Terminology

Use of the term "thread" has already caused a lot of confusion and consensus is that it be abandoned. (This plan uses "thread" but also sometimes refers to threads as "scripts".) What are alternatives? "script" is not clear. Others are "script chain" and "script stack", or maybe even "script thread" (but not "thread" by itself).

For now the first script in a thread is called the "root", and the script that spawned a thread is the "thread parent".

The terms "called" and "spawned" should be used to distinguish normal function calls and spawning.

[edit] Blocking scripts

A general bitset will disable threading for old games, so backwards compatibility will not be affected. (Though some commands could still work; need to sort these out)

However, to ease the transition from blocking scripts to threading scripts for authors of existing games, we can add a new script command, blocking script (see above). By adding this command to the top of a script, it will behave like old scripts, and pause any other blockingscripts until it is finished.

A game author who wishes to convert their existing scripts to use threads can do the following:

  1. Add blocking script at the top of all their plotscripts
  2. Change the general bitset to enable threads for their RPG
  3. Remove blocking script one-at-a-time, with testing.

[edit] Execution order

New triggered threads should be set to run before all current ones. That way, onkeypress scripts are run first, and the current script order is preserved: the mapautorun script of the first map runs before the newgame script.

However, script threads started from other scripts are tricky.

My current thinking is this: firstly, any new threads should run that tick instead of having to wait. In fact, it would hopefully be least confusing if they ran immediately, in the order in which they are written in the script. In fact, they would be put before the parent thread in the thread list, and the interpreter would jump up the list by one script. This has the additional advantage that all spawned threads will run each tick in the order they were created from their parent.

(This idea gets the James seal of approval Bob the Hamster 20:48, 16 October 2008 (UTC))

[edit] Hazards

The script debugger was going to be rewritten from scratch because it was unusable and horribly written. Lately it's been shaping up, so now maybe only part of it will be rewritten.

Some commands, like teleporttomap, imply a wait and others set variables to trigger an effect when they finish, like showtextbox. Commands which communicate with want variables can be rewritten where it's not backwards incompatible to do so, other all threads could just continue to use the same want variables. Still uncertain-

Here is the list of want flags:

wantbox, wantdoor, wantbattle, wantteleport, wantusenpc, wantloadgame

(How about making want variables members of the script's UDT, that way each "thread" has its own copy of them and they cannot collide. I don't think this would break any compat. Bob the Hamster 20:50, 16 October 2008 (UTC))
(Also, most want variables will indeed be possible to eliminate. The main reason they exist is to provide a way for code inside subs to trigger jumps to module-level GOSUBs which is irrelevant now that many of those gosubs have already be SUBified Bob the Hamster 20:52, 16 October 2008 (UTC))

[edit] Opportunities!

Perhaps the implicit waits in commands like usenpc could be disabled when script multitasking is enabled? Rational: continuing with the current behaviour would require either forcing just the calling script to wait and allowing others to run (when a user who doesn't know about the implicit wait probably won't expect them to!) or jumping out of the interpreter completely, halting other scripts for a tick. Both seem illogical.

Waitfornpc currently acts up when you the leave the map! But wait. The obvious solution is the have any waits immediately broken when the map changes, but what if script multitasking is implemented, and the map is set to save NPC state? If an NPC is in the middle of a scripted movement when you leave the map, maybe you DO want the script to pause and wait for the map to be reentered? Maybe it would be reasonable to make this behaviour the default if script multitasking and NPC state saving are both enabled. Probably a better solution is "pause script on map change", see below.

[edit] Handles

After a couple years of thought, it has become apparent that you would want handles onto individual script instances. With those, handles onto script threads become redundant.

A script handle is a handle (opaquely typed object) on an individual script instance in some script thread. Becomes invalid if that script exits. Can be used to see if that script is still running, or force it to exit. This is the main type of handle.

NOTE: Not to be confused with script objects (term used on this page) or whatever we'll call them, created by writing '@my script', which are callable 1st class function objects. See Plan for dynamic types in HamsterSpeak#Replace runscriptbyid with function call operator syntax?

Each script instance shall also have associated with it a table of metadata (accessible with "script instance metatable"), as this is likely to be useful for marking scripts and other internal use. Currently there are no proposals for default contents, so the table will start out empty.

[edit] Script forest

Currently running scripts will be organised in an ordered forest (list of rooted trees). The roots are the script instances which are triggered by in-game triggers. There are three different types of "child" scripts:

  • called scripts. Pretty obvious
  • spawned scripts. Created with "spawn script" and "whenever"
  • blocking scripts. Currently when a script is triggered, it is pushed on top of the crurent script on the script state. This emulates that, if enabled.

Proposal: A "spawn script" block of commands in a script counts as a separate script, unless it consists of only a single script call. So, in "child script" below, "parent script" returns 0.

script, child script, foo ( show value (parent script), wait (10) )

plotscript, master script, begin
  spawn script (child script (3))
  spawn script (
    child script (4)
    wait for npc (5)
  )
  child script (5)
end

Script tree:

  • master script
    • child script (5)
    • (spawned) child script (3)
    • (spawned) master script$SPAWNSCRIPT
      • child script (4)

[edit] End-user script commands

A tentative list of suggested new commands to handle threads.

Note that the command names are VERY open to change. I'm not happy with many of them.

Omitted are several potential commands to browse the script tree and manipulate the order threads run in.

[edit] spawn script (commands...)

(Alternatively "fork script".)

When this block (which is actually a flow control block (but is ACTUALLY implemented as a new type altogether), and NOT a top level statement like 'script') is encountered, execution appears to proceed into it, but in fact occurs in a newly created thread which is set to run BEFORE this thread in the thread list, but immediately gets focus. See #Execution order. When it waits or completes, the parent script is reentered --- all in the same tick.

Also acts like a command, returning a script handle.

The parent script can quit regardless of what spawned scripts do, and if any are still alive, then the parent script's local variables aren't freed.

You can access variables declared in the parent script, while variables declared inside this block could be in a different scope to the parent script. This can be achieved by putting all the variables in the parent script's variable list, but keeping track in HSpeak what scope variables have (probably with some behind the scenes name decoration). But is suddenly adding scoping to HS a good idea? I guess it's just not needed, and encourages people to put big messy scripts in spawnscript blocks instead of splitting them off.

Should be nestable.

(That sounds a bit unnecessary and painful now -The Mad Cacti 11:32, 16 June 2010 (PDT))

Example:

script, split up!, begin
 variable (Sean, Sarah, Sam)
 Sean := npc reference (...)
 #... NPC manip

 show text box (49)  # everyone split up!
 wait for textbox
 variable (count down)
 count down := 300

 spawn script, begin
  walk npc (Sean, left, 4)
  wait for npc (Sean)
  walk npc (Sean, up, 10)
  #disappear off map...
 end

 variable (Sarah's path)
 Sarah's path := spawn script, begin
  walk npc (Sarah, right, 8)
  wait for npc (Sarah)
  #...
 end

 #continue main script here...
 # some special condition: Sarah stops and comes back
  kill script (Sarah's path)
  wait for npc (Sarah)
  #walk Sarah back...
 
end

But normally you would split things up into other scripts if they are nontrivial:

To run a script in a new thread instead of a bunch of commands, you would just write

handle := spawn script( falling chimney animation(npc) )

Since this would be common, and we won't want the parent script's variables and data kept around so unnecessarily, this should be optimised as separate command internally.

What if you don't want the script to run immediately, but just want create it and get the handle to manipulate it? You could write

handle := spawn script( wait, falling chimney animation(npc) )
pause script (handle)

but this bypasses the above proposed optimisation and is slightly unpleasant. Prehaps we need another command. We can leave that for the future - maybe by then we will want closures :)

[edit] get script handle

(Or "this script"? "this script handle"?)

Returns a handle for the current script. Notice that the code inside a spawnscript block counts as a separate script instance.

[edit] calling script ([script handle])

(obscure name, maybe caller of script or script caller?)

Given a script handle, returns a script handle for the script that called that script, or 0 if the script was either spawned or triggered.

script handle is optional; it defaults to the handle for the current script.

Note: if this script was called from within a spawnscript block, then... see #Script tree. In particular it is assumed that spawnscript blocks which contain only a call to another script do not count as a separate script.

[edit] parent of script thread ([script handle])

(Probably need a better name, but I think "parent script" is confusing)

Given a script handle, returns a script handle for the script from which that thread was spawned, or 0 if that script has quit, or if the thread was triggered, not spawned.

With no argument, defaults to finding the script from which this one was spawned.

[edit] root of script thread ([script handle])

(Look for better name)

Given a script handle, returns a script handle for the first script in that thread. This can be used as a handle onto the whole script thread, because it won't be invalidated until the thread ends.

[edit] script is alive (script handle)

Given a script handle, returns true if that script hasn't exited yet; ie. whether that handle is still valid. Paused scripts are considered to be alive.

[edit] find script (script object, [count], [thread root only], [unpaused only])

Given a script object (as returned by @ operator) or script ID (as in definescript), returns a script handle to a script with that id. count is optional (defaults to 0), and the number of the script to return (0 is the first matching one, 1 the second...). thread root only is optional (defaulting to false) that indicates whether any script anywhere in the #Script tree should be found, or only the roots of threads (those which were spawned or triggered). unpaused only (defaulting to false) indicates whether scripts in paused should be ignored.

Other ideas:...should it return the first thread with that script anywhere, at the bottom of, or on top of the script stack? Other commands for other cases?

[edit] get script id (script handle)

Return the script ID (as in definescript) of the given script.

An instance of a spawnscript block has ID number equal to the script containing it (or should be a unique ID?)

[edit] pause script (script handle)

A script handle, temporarily stops the thread which that script is part of. For example, if you want both the current and another thread to wait for something, you could do:

pause script (handle)
wait for npc (npc)
resume script (handle)

Note that it doesn't matter which script from a thread you use.

[edit] pause spawned scripts ([script handle])

Given a script handle, pauses threads spawned just from that script. Without script handle, pause scripts spawned from the current script.

[edit] pause scripts spawned from thread ([script handle])

Given a handle for any script in a thread, pause all threads spawned from that thread. Without script handle, pause scripts spawned from the current thread.

Note that it doesn't matter which script from a thread you use.

[edit] resume script (script handle)

Given a handle for any script in a thread, resumes the thread (regardless of how it was paused).

Note that it doesn't matter which script from a thread you use.

[edit] wait for script (script handle)

Waits for the given script to return. It should be clear that calling this with a handle for a script in the current script is crazy, and not allowed.

[edit] wait for my spawned scripts

Waits for all scripts spawned from this one to finish. (That is, this doesn't wait for scripts spawned from other scripts in this thread.) (We could include grand-children, but I think we would be less surprising to have people explicitly wait for grandchildren in their child scripts.)

[edit] pause all scripts

Pause all other scripts.

[edit] resume all scripts

Resumes all paused script thread. See also "pause all scripts"

[edit] kill script (script handle)

Forces a script and any scripts it has called to exit, so its parent script is now the current/topmost script in that thread. In this way, you can perform multiple exit scripts at once. You can use this on a script in the current thread, or even on the current script! (kill script (script handle) is equivalent to exit script)

You can write kill script (root of script thread (handle)) to kill a whole script thread containing a script.

The return value of an killed script is its current return value. (But this command does not return a value.)

This could also be useful if multitasking is not enabled.

[edit] suspend script triggers

Stop triggering of scripts (by game events). This would also be useful if multitasking were disabled.

[edit] resume script triggers

See "suspend script triggers"

[edit] whenever (condition) do (stuff...) [until (exit condition)]

Flow control. until is optional. Equivalent to (perhaps translated by macro):

spawn script, begin
  unwaitable script thread
  while (exit condition == false) do, begin
    if (condition) then (stuff...)
    wait
  end
end

whenever would NOT return a script handle, unlike spawnscript.

Maybe there should be an extra wait(1) before the while loop?

[edit] Internal script commands

[edit] hidden script thread

Marks the current script thread as hidden so that it does not show up in the debugger, etc, by default, to prevent clutter.

[edit] unpauseable script thread

Prevents this script thread from being paused.

[edit] unwaitable script thread

"wait for child scripts" and similar commands do not wait for this thread.

[edit] script instance metatable (script handle)

Returns the metatable for a script instance, which can be directly modified (that part might have to be changed).

I thought this up because I was sure it would be useful in implementing some commands, but I haven't gone through the whole page to look for them.

[edit] Directive script commands

[edit] blocking script

Pauses any other script which has run this command (there will normally be at most one unpaused one), and automatically resumes it when the script in which this appears quits. Perhaps this should override the other script pausing/resuming commands.

[edit] exit script on map change

script, exit script on map change, begin
 spawn script, begin
   hidden script thread
   unpauseable script thread
   unwaitable script thread
   variable (hsd:this map, hsd:parent)
   hsd:this map := current map
   hsd:parent := calling script (parent of script thread)  # we want the parent of "exit script on map change"
   while (hsd:this map == current map) do (
     wait
     if (script is alive (hsd:parent) == 0) then (exit script)  # stop if the parent exits
   )
   kill script (hsd:parent)
 end
end

[edit] pause script on map change

This could be implemented just like "exit script on map change", however I think there could be benefits (eg. when saving/loading games) from making the engine aware that certain scripts have the intention of being tied to a map, so currently suggest making this a real command.

This command would affect the whole script thread, until the script that called this command exits -- so when control is returned to the parent script, it stops pausing on map change.

Alternatively, we could add a "map script" trigger.

[edit] exit script when parent exits

Exit a script when the script from which this thread was spawned exits (for example, you'd often want to use this inside a spawnscript block).

script, exit script when parent exits, begin
 spawn script, begin
   hidden script thread
   unpauseable script thread
   unwaitable script thread
   variable (hsd:parent, hsd:grandparent)
   hsd:parent := calling script (parent of script thread)
   hsd:grandparent := parent of script thread (parent of script thread)
   while (script is alive (hsd:grandparent)) do (
     wait
     if (script is alive (hsd:parent) == 0) then (exit script)  # stop if the parent exits
   )
   kill script (hsd:parent)
 end
end

If it actually matters that someone inspecting the script tree will not see a script using this directive quit until the next tick, then this could also be a command instead of a plotscr.hsd script.

[edit] Planned script commands which don't really belong here

[edit] intercept keypress

(Need a better name?)

When run in an onkeypress script, stops any other onkeypress script from being triggered by this keypress, and stops the engine from seeing it (eg, can block ESC from bringing up the menu). Should this also "unpress" the key, or is it useful to be able to continue to check the key with keyispressed?

(Implementation detail: actually all onkeypress script would be triggered at once, so this would kill any later scripts that have not started executing yet)

[edit] New Triggers

Not all of these actually require multitasking. Looking for further ideas...

  • Global:
    • Duplicated: If an object also has one of these set, then that could override the global script (they can always call the global script from their specific handler). We will also want a way to disable the global script in certain cases (distinguish "Use default", and "None")
      • On keypress
      • After battle(won battle)
      • Instead of battle(formation, formation set)
      • Each step(hero X, hero Y, hero dir)
      • Map autorun(user argument)
      • Display textbox(slice handle): It would be really awesome if you could modify the appearance of textboxes, such as by sliding in the textbox slice, or sliding in its child slices from different directions. What about the fade-in of text? That is definitely something I want customisable without needing to write a script, but maybe you want to script something exotic. I guess the Display Textbox script could handle that, if it was set to "No text fade-in".
    • Screen fades. Overrides default screen fades. Assumeably these will paint some slices (alternatively some kind of screen pixel-level access), then call a special "do transition" function. I am also imagining things like zooming off the edge of a script into an infinite landscape and then back onto another map.
      • Battle fade-in(): Requires battlescripting.
      • Battle fade-out(): as above. Also tricky.
      • Map transition(new map, new X, new Y): (If we had commands to read/write door data, then we could pass in a door number instead.) Unlike battle fades, we could ditch the requirement for a "do transition", and have people just use "teleport to map". Then they can override the door if they want.
  • Menus:
    • On open(handle)
    • On keypress(handle): Alternatively, "On cursor movement". But this lets you add additional functionality. If there is also a global/map keypress script, then they are both run, the menu script runs first (see also intercept keypress above).
  • NPCs:
    • Movement(user argument, NPC reference): Every tick, for each NPC which is not moving and has Movement Type set to 'script', its Movement script is invoked if it is not already running. So you can either write a script which uses wait commands, or one which tells an NPC to walk and then exit. Probably Movement scripts should automatically be set to "pause script on map script".
    • Each-tick(user argument, NPC reference)?: I think we'd want a separate each-tick script, in case you don't want scripted movement, but still want to do something else on a per-NPC basis. OR only add Each-tick but not Movement.
  • Textboxes
    • Display textbox(slice handle, user argument): See above.
  • Doors. Should we let people override the global map transition script? Seems a bit redundant to step-on NPCs.

[edit] See Also

Personal tools