Code: Select all
This is a proposal for the first batch of post-1.0 scripting improvements. It should keep us (me) busy during 1.1, 1.2 and probably 1.3. It is not exhaustive and the scripting system can certainly be improved further in later versions. For this first batch we should stay focussed though. Most of these improvements either fill glaring holes or are motivated by my own work with the MW scripting language on a total conversion. The later improvements are aimed mostly at large projects with scripts of high complexity that add completely new features to the MW engine. I believe that these will be of use to more ordinary scripters too. I am not listing the following types of scripting enhancements in this document: - enhancements that are specific to the post-1.0 de-hardcoding effort (moving away from hardcoded features/settings and let ESXs decide about them instead) - enhancements that are required for new features - enhancements that are already part of 1.0 (improvements that were possible without additional effort by choosing a different implementation path). Contents 1. Compatibility 2. Missing Basic Functionality 3. Script types 4. Throttling 5. Variable scopes 6. Variable types 7. Additional control structures 1. Compatibility Each script can have an optional compatibility line. Proposed keyword and syntax: flavour x where x is a sequence of one or more compatibility settings. The line must be placed at the very beginning of the script. The scripting flavour of the console should be made globally configurable. If no flavour statement is given, OpenMW will default to flavour 0 which is the same as MW. This will allow scripts of different flavours to be used at the same time, e.g. a plugin can add a "flavour 5 sane" script, without the "flavour 0" scripts of the master needing to be rewritten. Settings: 1.1. Compatibility number This is an integer number that is increased each time we release a new version that introduces new keywords or syntactic enhancements and thus potentially could cause an incompatibility with existing scripts. 1.2 Sane mode Keyword: sane This removes some oddities from the MW scripting language, namely: 1.2.1 Disallow the use of keywords as variable names 1.2.2 Change the operator precedence to something more reasonable (details still need to be worked out) Note that the sane mode might become mandatory from a certain compatibility number on, in which case it would be added implicitly. 2. Missing Basic Functionality 2.1 Logical operators Keywords: and, or, not, xor 2.2 Additional loop control instructions Keywords: break, continue 2.3 Object deletion In vanilla MW objects in a cell can be deleted with the setdelete instructions, while objects in a container can be deleted with the removeitem instruction. Removeitem is mostly sensible, which is why we should keep it (with some addition for new variable types, see section 6). The same can not be said about setdelete. It has an awkward syntax and is very limited. The former is reason enough to declare it deprecated. As a replacement a new delete function shall be added. Examples: (i) Delete - This will delete the object the script is attached to (can only be used in local scripts). (ii) Laire -> Delete - This will delete the first reference to the ID "Lair" OpenMW can find. (i) is an example of an implicit reference and (ii) is an example of an explicit reference. This terminology is currently used in the OpenMW codebase and will also be used in several places in this document. Note that (i) will also work when the object is in a container. In this case the whole stack is deleted. 2.4 Variable initialisation at declaration A variable of local, in or out scope (see section 5) can be given a value at declaration. Example: local long x = 10 (for the local keyword see section 5) Variables without a explicit value are default initialised. Note that if a variable of scope in is explicitly initialised with a value all following variables of scope in need to be initialised too. 2.5 Increment and decrement New keywords: inc, dec, by For convenience the scripting language needs equivalents to the C++ operators += and -=. Since some hard to fix syntactic oddities make it impossible at this time to move from the "Set x to y" syntax to a "x = y" syntax, these need a different syntax too: inc x by y dec x by y The "by y" part is optional and defaults to 1 if not present. 3. Script types Currently we have 4 script types: local, global, dialogue and targeted (5 if we count console scripts, which we won't do here). 3.1 New script types (i) Region scripts: Executed while the player is in the respective region (ii) Cell scripts: Executed while the player is in the respective cell (iii) Late global scripts: same as global scripts (iv) Function scripts: Executed when called from another script 3.2 Script type execution order Each frame scripts shall be executed in the following order (i) global scripts (ii) region scripts (iii) cell scripts (iv) local scripts (v) late global scripts This extended script system will allow scripts of different types to work together and provide features that can only be implemented with ugly workarounds in MW. The region and cell scripts can also serve as repositories for region- and cell-specific state (in their member variables), which would go into global variables in vanilla MW. One area where this feature could be utilised is the much discussed music player feature. The general consent on the forum was that special slots should be added that determine what music track is played (region, cell, ...). But this would never be flexible enough. If we change the streammusic instruction so that it turns into a no-op, if the requested track is already playing and also only changes the music at the end of the frame we are getting this feature for free with the new script types. A script of each type could specify a desired track, which can be overridden by a more specific script type (if needed), with late global handling tracks of the highest priority (like combat music). There are too many areas where this new script structure can be utilised to list them all and modders will certainly come up with even more over time. 3.3 Script type ID Each script can optionally specify which type it is by giving a type name after the script name, e.g.: Begin SomeScript local Keywords: global, region, cell, local, lateglobal, function Dialogue scripts do not need a separate keyword, because they don't have a begin statement and also they are stored and handled separately from other scripts. Targeted scripts also don't have a separate keyword. This script type is a bit of an oddity and is made largely redundant by the enhanced scripting system. While we can't drop support for it without breaking compatibility, we should discourage its use by declaring it obsolete and not adding any improvements to it. The purpose of the script type ID is mostly to make it more clear how the script is used. But the editor could also offer filter functions for script types when listing scripts. It should also complain when a script is used in a way not compatible with the specified script type (e.g. attaching a global script to a MW-object). Later we might utilise the script type ID to perform special script optimisations. 4. Throttling MW performs a crude throttling on global scripts. This "feature" has to go. But throttling is required, especially since post 1.0 more hardcoded functionality will be replaced by scripts. Therefore each global-, region-, cell-, local- and late global-script will receive a priority (specified by an integer number in the range of 0-9; higher values mean higher priority). Each script can set its priority by the following line: priority x where x is a number between 0 and 9. Scripts that do not provide a priority setting shall get one assigned based on a script type specific GMST. The following guarantees are provided: (i) If one script of priority i is executed during a frame, all scripts of priority i are executed during this frame. (ii) Scripts of priority 9 are executed every frame. (iii) Given a high enough number of frames, a script of priority i will be executed at least as often as a script of priority i-1. (iv) If scripts of priority i have been executed and scripts of priority i-1 have been executed in a later frame, scripts of priority i-1 will not be executed again, until scripts of priority i have been executed. 5. Variable scopes MW provide two variable scopes: global and local. The term local is misleading though, since these variables behave more like member variables. Therefore we shall call this scope "member" in this document. The following scopes need to be added: 5.1 Local scope This is a true local scope, i.e. the variables exist only for the duration of the script execution and lose their value afterwards. This will allow variables like loop counters to be kept out of the world state (and therefore also out of the saved game files). Keyword: local Example: Local Short x 5.2. In/Out scope These scopes are related to the function calling mechanism and are explained in section 7. Keywords: in, out Example: In short x 5.3 Static scope This is the equivalent to the static keyword in C++ used on member variables. A variable declared with static scope will be shared by all references of the same ID. The static scope will help to reduce the flood of global variables in larger projects. Keyword: static Example: Static short x 6. Variable types Currently we have 3 variable types: short, long, float. 6.1 Global variables In MW all global variables are internally stored as float. This causes problems especially for longs. We can't fix this without extending the ESX format, so this needs to be addressed as a post-1.0 feature instead. 6.2 New variable types 6.2.1 Cell New keywords: cell, isinterior, isexterior, interior, exterior, nocell, cellof Example: Cell x This variable type stores a reference to a cell (the term reference here should not be confused with a regular MW-reference, a.k.a. object). Cells require a special treatment, because a cell record is the only addressable record type, that can not be specified by a string or an integer (interiors can be specified by a unique name, exteriors by unique coordinates). Newly declared local/member/static cell variables are implicitly set to nocell. The following functions needs to be added (c being a variable of type cell): (i) isinterior c - returns 1, if c is an interior cell, and 0 if c is not an interior cell (ii) isexterior c - returns 1, if c is an exterior cell, and 0 if c is not an exterior cell (iii) exterior x, y - returns the exterior cell specified by the given coordinates (iv) interior name - returns the interior cell specified by the given name (v) cellof - returns the cell a MW-reference is in (both implicit and explicit references are supported) Note that nocell is neither interior nor exterior. 6.2.2 List New keywords: list New operators: #,  Examples: List long x List y This variable stores a list of values. List of lists are not allowed. The first example creates a single type list. The second example creates a mixed type list. Newly declared local/member/static list variables are empty. A list literal can be specified by listing list items in square brackets. Example: Set x to [ 1, 2, 3, 4 ] The type of the list literal shall be automatically chosen based on the listed value. An empty list literal is a mixed list and can be implicitly casted to any other list type. The size of a list can be read by the # operator. Example: #x The n-th element of a list (starting at index 0), can be accessed by the  operator. Example: x This only works for single type lists. Since this scripting language is statically types, for mixed type list a separate syntax is required. Example: x[1, long] If the list item is not of the given type an implicit cast is attempted. Note: (i) This is a very basic list support. Additional features can be added later, but that is beyond the scope of this specification. Advanced list support might be worth a design document of its own. (ii) List of lists are not supported because they would be hard to implement. We may reconsider this at some point in the future. 6.2.3 String New keywords: string Example: String x This variable stores a piece of text. Newly declared local/member/static string variables are set to an empty string. String literals should be placed into pairs of quotation marks. If the string does not contain any spaces or other special characters, the quotation marks are optional. Strings can by concatenated by the + operator. Example: "abc" + "def" Any value of type short, long, float, cell, list or ref can be turned into a string by use of the string function. Example: string (1.3) When using the string function on a ref value, the name of the ID and not the ID itself will be returned. Using the string on nocell or noref will return an empty string. The length of a string can be determined by the # operator (see list type). Access to a single character in a string is possible through the  operator (see list type). The returned value is of type string again. String variables can be used in any place where an ID can be used. In this case the resulting ID will be the value of the string variable. Note: As with lists the initial implementation of strings will be somewhat basic. Additional features may be added later. Amongst others we might borrow the % operator from Python. 6.3.4 Ref New keywords: ref, noref. self, content, cellcontent, getid Example: Ref x This variable type stores a reference to a MW-reference (a.k.a. an object), which can either be located in a cell or a container. Newly declared local/member/static ref variables are set to noref. The following functions need to be added (r being a variable of type ref, c being a variable of type cell): (i) self - returns a ref to the object the script is attached to (can only be used in local scripts) (ii) content - returns a list of the references in the inventory of an object. Can be used either with an implicit or explicit reference. The object must be a container, creature or NPC. (iii) cellcontent c - returns a list of the references in c. (iv) getid r - returns a string containing the ID of r. The ID of a noref is an empty string. Ref variables can be used instead of IDs when using explicit references. Example: x -> enable Ref variables can also be used instead of IDs as function/instruction arguments. In cases where an individual ref doesn't make sense, the ref will automatically degrade to its ID. Note: Ref variables must be of local, in or out scope. Any other scope would be a nightmare to implement with OpenMW's current world model and also very error prone when put in the hand of an average modder. 7. Additional control structures 7.1 for loop New keywords: for, forend, in Iterates over the elements in a list. Example: local ref r local long count for r in content inc count by GetItemCount r endfor This little script will count the total number of items contained in the object this script is attached to (e.g. a NPC). Note: In case of the content/cellcontent function, since the for loop is operating on a list of refs and not on the data structure of the object used as argument for content, removing the current ref from the object/cell is perfectly valid. In other words cutting off the branch you are sitting on is not a problem. Example: local ref r for r in cellcontent player -> cellof if getid r == "iron dagger" r -> delete endif endfor This would delete any iron dagger in the cell the player is currently in. 7.2 switch/case New keywords: switch, endswitch, case, default Example: switch player -> getItemCount "Gold_001": case 0 MessageBox "no coins at all" case 1 MessageBox "a single coin" default MessageBox "filthy rich" endswitch The switch variable can have the type short, long, float, cell or string. 7.3 function calls New keywords: runscript This will execute a script as a function. Example: local long x local long y runscript double in x out y In this case the script double could look like this: Flavour 1 sane begin double function in long a out long b Set b to 2*a end or shorter: Flavour 1 sane begin double function in long a out long b = 2 * a end In and out variables must match in type and number the arguments of the runscript instruction. If no in or out variables are required, the respective in/out clause can be omitted. The caller is free to provide less out variables than defined in the function script, in which case the remaining return values are discarded.