Plan for script multitasking

From OHRRPGCE-Wiki
Jump to navigation Jump to 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

Terminology[edit]

Use of the term "thread" has already caused a lot of confusion and consensus is that it be abandoned. Internally I've settled on the term "fibre" (which isn't 100% conventional: fibres are normally OS-level objects like threads, while "green threads" would be the term used for script fibres). MAybe it's possible to get away with calling fibres "scripts" in user documentation. (This plan also sometimes refers to fibres as "scripts".) Other alternatives are "script chain" and "script stack", or maybe even "script thread" (but not "thread" by itself).

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

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

Blocking scripts[edit]

A general bitset will cause triggered scripts/fibres to block each other in old games, so backwards compatibility will not be affected. But explicit fibre spawning, such as using spawnscript, and most commands will still work; need to sort these out.

However, to ease the transition from blocking scripts to fibre 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 fibres can do the following:

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

Execution order[edit]

New triggered fibres 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 fibres started from other scripts are tricky.

My current thinking is this: firstly, any new fibres 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 fibre in the fibre list, and the interpreter would jump up the list by one script. This has the additional advantage that all spawned fibres 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))

Hazards[edit]

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 fibres 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 "fibre" 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))

Opportunities![edit]

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.

Handles[edit]

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

A script handle is a handle (opaquely typed object) on an individual script instance in some script fibre. 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.

Script forest[edit]

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)

End-user script commands[edit]

A tentative list of suggested new commands to handle fibres.

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 fibres run in.

spawn script (commands...)[edit]

(Alternatively "new script" or "fork script".)

When this block (which is actually an embedded script like 'subscript') is encountered, execution appears to proceed into it, but in fact occurs in a newly created fibre which is set to run BEFORE this fibre, but immediately gets focus. See #Execution order. When it waits or completes, the parent script is re-entered --- all in the same tick.

Unlike 'subscript', this also acts like a command, returning a script handle.

Like subscript, creates a new scope from which variables declared in the parent script can be accessed. 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. Will be nestable.

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 fibre instead of a bunch of commands, you would just write

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

What if you don't want the script to run immediately, but just want create it and get the handle to manipulate it? (But why would you want to do that anyway?) You could write

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

but this is slightly unpleasant. Perhaps we need another command like "create fibre".

get script handle[edit]

(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.

calling script ([script handle])[edit]

(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.

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

(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 fibre was spawned, or 0 if that script has quit, or if the fibre was triggered, not spawned.

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

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

(Look for better name)

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

script is alive (script handle)[edit]

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.

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

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...). fibre 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 fibres (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 fibre with that script anywhere, at the bottom of, or on top of the script stack? Other commands for other cases?

get script id (script handle)[edit]

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?)

pause script (script handle)[edit]

A script handle, temporarily stops the fibre which that script is part of. For example, if you want both the current and another fibre 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 fibre you use.

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

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

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

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

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

resume script (script handle)[edit]

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

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

wait for script (script handle)[edit]

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.

wait for my spawned scripts[edit]

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

pause all scripts[edit]

Pause all other scripts.

resume all scripts[edit]

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

kill script (script handle)[edit]

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

You can write kill script (root of script fibre (handle)) to kill a whole script fibre 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.

suspend script triggers[edit]

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

resume script triggers[edit]

See "suspend script triggers"

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

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

spawn script, begin
  unwaitable script fibre
  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?

Internal script commands[edit]

hidden script fibre[edit]

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

unpauseable script fibre[edit]

Prevents this script fibre from being paused.

unwaitable script fibre[edit]

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

script instance metatable (script handle)[edit]

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.

Directive script commands[edit]

See #Special Script Types for description of tie commands.

set as blocking script[edit]

(See #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.

exit script on map change[edit]

(See #tie to map (map) instead. While it's cool that this can be implemented as a script, building this functionality in seems much better)

script, exit script on map change, begin
 spawn script, begin
   hidden script fibre
   unpauseable script fibre
   unwaitable script fibre
   variable (hsd:this map, hsd:parent)
   hsd:this map := current map
   hsd:parent := calling script (parent of script fibre)  # 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


exit script when parent exits[edit]

(Alternative name: "tie to script")

Exit a script when the script from which this fibre 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 fibre
   unpauseable script fibre
   unwaitable script fibre
   variable (hsd:parent, hsd:grandparent)
   hsd:parent := calling script (parent of script fibre)
   hsd:grandparent := parent of script fibre (parent of script fibre)
   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.

npc waits for script[edit]

(It might be better to add a new NPC setting which causes this behaviour instead of adding this command. (EDIT: now I much more strongly think so --TMC) Also, don't like the name since it doesn't mention the facing behaviour.)

A fix for Bug #468.

When placed in a script spawned from an NPC, that NPC waits for the script to finish, and faces back to its original direction afterwards depending on its When Activated... setting, just as if it had triggered a textbox.


Special Script Types[edit]

Some new script trigger types will be added to the existing 'script' and 'plotscript'. plotscripts will still be attachable to any trigger, but using the new trigger types in the right locations will cause the scripts to be tied to the appropriate object. The idea is that using the new triggers will Do The Right Thing with the absolute minimum required effort.

They are optional; the 'tie' directives can be used instead. However the tying happens as soon as the script is triggered rather than being delayed until it actually starts running. Also it's intended that the triggers be the main 'interface', and the 'tie' commands relegated to an Advanced section.

I think new triggers would have no effect if scripts using them are run from another command. In the case of NPC scripts there isn't an NPC to use (unless we make assumptions about the arguments, or the script fibre was originally triggered from an npc). Map scripts could still use the current map, and blocking scripts could temporarily switch the fibre to blocking until they complete, but it seems bad to have npc scripts act differently.

tie commands[edit]

The "tie" directives would affect the whole script fibre, until, I think, the script that called the command exits -- so when control is returned to the parent script, it stops being affected. What should happen if a script is called from another script, it ties itself to something, and gets killed? Should the parent script/whole fibre also get killed, or should it return to parent?

Other names of the tie commands to consider: "tie to npc", "attach to npc", "set as npc script", "mark as npc script", "make npc script", "toggle npc script", "is npc script"


tie to npc (npc)[edit]

(See also #npc script)

Tying a script to an NPC has the following effects:

  • If the NPC is deleted the script is stopped
  • If the NPC is hidden by tags, the script is suspended
  • The script is effectively also tied to the map the NPC is on, i.e. if the map changes...
    • ...and is set to remember NPCs, the script is suspended until the map is reentered. (Ideally, resuming NPC scripts across saved games would even work, if possible)
    • ...otherwise the script's killed

tie to npc movement (npc)[edit]

Identical to #tie to npc (npc), but in addition the script is suspended if the NPC is suspended (ie. no movement AI)

tie to map[edit]

(Was "pause script on map change")

  • If the map changes...
    • ...and is set to remember scripts (a new mapstate remembering setting, which I think would be needed), the script is suspended until the map is reentered. (Ideally, resuming map scripts across saved games would even work, if possible)
    • ...otherwise the script is killed

However, using a "tie to map" command instead of having the engine automatically tie a script to the current map is a bad solution, because what if a script is triggered but is delayed before running, during which time the map changes?

Also, does it make sense to allow tying to a map other than the current?

New script trigger types[edit]

npc script[edit]

NPC scripts only appear in Custom in options for NPC Use triggers, and any other NPC-related triggers that are added in future, like NPC movement/AI scripts and zone triggers for NPCs (whether NPC ID-specific or global).

Equivalent to running #tie to npc (npc), except (in future) for NPC movement scripts, which are #tie to npc movement (npc).

map script[edit]

I think map scripts would be attachable to most or all triggers in Custom rather than only map-specific ones, because stopping a script automatically when you leave the map is useful all over the place. They get tied to the current map.

blocking script[edit]

Special trigger to help with migrating a game to multitasking. Also a subtype of plotscript. When a blockingscript is triggered, it blocks previous blockingscripts from running.

It is intended that doing the following steps:

  • enabling the script multitasking bitset
  • doing a search/replace of "plotscript" with "blocking script"
  • finding all "script"s triggered by timers and making them "blocking script"s

will cause no changes to script behaviour. You will then be free to gradually change some scripts into other types.

I think "blocking script" would have no effect if run from another command.

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

intercept keypress[edit]

(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)

New Triggers[edit]

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.

See Also[edit]