Advanced Script Handling
Context: HL Kit … Advanced Authoring Concepts
The Kit provides a variety of more specialized control mechanisms for managing scripts. These mechanisms provide the ability to handle special-case situations that may arise when writing data files for some game systems. The topics below cover these various mechanisms.
Sequencing of Scripts with Identical Timing
When multiple tasks are assigned a common phase and priority, those tasks will be scheduled for evaluation at the exact same time. However, the task scheduler only invokes one task at a time. This means that there would be no guarantee about the order in which two tasks are evaluated that are assigned the same timing. From an authoring standpoint, it's much easier to assign a group of tasks the same phase and priority instead of having to micro-manage which ones occur before each other.
To address this, the Kit provides a set of rules for task scheduling. These rules govern the sequence in which tasks are evaluation that have the same phase and priority. The table below details the order that the Kit uses, with tasks being evaluated in the sequence given, based on their type.
- Existence tests that are assigned by tables or choosers
- Bootstrap condition tests that are assigned by root picks
- Secondary tests that are assigned by tables or choosers
- Condition tests that are assigned by components
- Condition tests that are defined explicitly on a thing
- Calculate scripts defined on any fields
- Bound scripts defined on any fields
- Eval scripts that are defined on components or things
- Evalrule scripts that are defined on components or things
- Gear scripts that are defined on components
The above rules don't handle a common situation that arises when component scripts are employed. The following topic address how this situation is handled.
Sequencing of Component Scripts
When an eval script or evalrule script is defined for a component, all things derived from that component possess the same script. Since the script is assigned a common phase and priority, all instances of that script will be scheduled for evaluation at the exact same time. As discussed above, the task scheduler only invokes one task at a time, so this means that there is no guarantee about the order in which these scripts are evaluated.
In most cases, there is no need to worry about the respective timing of these scripts, since they don't depend on each other. However, there are situations where it's important that all the scripts being evaluated in a guaranteed order. For example, consider the d20 System data files, where attribute bonuses are chosen every four character levels. It's critical that those bonuses be applied in the exact order that they are selected by the user, since those bonuses have ripple effects elsewhere on the character. Consequently, the Kit imposes rules on the evaluation sequencing of component scripts.
Every component must be assigned a "sequence" attribute. This attribute governs how picks are sequenced to the user by default. It also governs how their tasks will be sequenced during evaluation. Consequently, during task scheduling of eval scripts/rules that are associated with the same component, the corresponding tasks are sorted using the sequence assigned to the component. This ensures that tasks are always scheduled in a consistent fashion. It also extends to the case where things are added multiple times to a container.
In rare situations, you'll need to specify an alternate behavior for task scheduling. To accommodate these cases, individual scripts and rules have an attribute that overrides their behavior. This attribute allows to you specify a different component whose sorting rules must be used when scheduling all tasks for that script.
Limiting Evaluation and Reporting
By default, every eval script/rule is scheduled and processed separately for every pick added to an actor. That behavior is exactly what you'll want 99% of the time, but there are some situations where special handling is needed.
Consider the case where you only want a script to be invoked if a thing is added to the actor. However, what if you also only want the script to be invoked a single time, even if multiples of the thing are added to the actor? There may also be times when you want a rule to be tested for all picks, but you only want the error message to be reported a single time if any of them fail. To deal with these situations, you can specify limits on the evaluation and reporting of scripts and rules.
To limit the evaluation, you can use the "runlimit" attribute to specify the maximum number of times to evaluate the task. This limit is then imposed separately for each container into which the thing is added. If the thing is added ten times to a container, a "runlimit" of one will ensure that the task is only ever invoked a single time. If the thing is added ten times to one container and four times to another container, the task will be invoked once within each container.
To limit the reporting of a rule, you can use the "reportlimit" attribute, which dictates the maximum number of times the rule reports an error. This is critically different from the "runlimit" attribute, which controls the actual invocation. With a report limit, the rule is invoked for every pick, as normal. Only the error message is limited. For example, consider the d20 System data files. If you add multiple class levels to a character, you need to assign hit points for each. You only want to report the error once, but you want all of the picks to be processed so that they will be properly highlighted in red if invalid.
For both evaluation and reporting limits, each thing is normally treated as distinct, with its own limit tracking. Once in awhile, you may want to have all things derived from a given component all contribute to the same limit. In other words, the same limit is imposed whether the user adds the same thing multiple times or different things. This behavior is controlled via the "iseach" attribute on components.
Multiple Tasks with Identical Names
When naming scripts and tag expressions for use with timing dependencies, you can assign the same name to multiple tasks. The first question you're probably asking is why you'd even want to do this, so we'll start there. There will be times when you'll have two or more different scripts that all need to calculate the same field, but they do so for different situations.
A simple example is calculating the net attack value for weapons. Melee weapons base the calculation on a "fight" skill, while ranged weapons base the calculation on a "shoot" skill. As a result, you'll have one script for melee weapons and another for ranged weapons. Both calculate the same field, and they should both occur at the exact same time for consistency, so they should possess the same name. That way, scripts that must occur before or after the net attack value calculation don't have to distinguish between whether it's a melee attack or a ranged attack.
When you assign multiple scripts the same name, only one of the scripts is reported in the timing analysis for "errors", "dependencies", and "timing". This is because all identical tasks are assumed to the same for timing purposes, so including them all would be redundant. Identically named tasks are still included in the list of all named tasks.
The drawback of only listing one task is that there is normally no guarantee which task will be chosen by the Kit for use. In most cases, this isn't a problem, but there is one situation where it is. Consider the case where you have two tasks named "MyTask" and various tasks dependent on those tasks. This will work correctly with no difficulties. However, if you then assign a "before" or "after" dependency to one of those two tasks, you run the risk of having that dependency thrown away by the Kit. If the other task is randomly chosen to be kept from the two named tasks, the dependency will be lost.
In this situation, you could always assign the same dependency to both tasks, but that can be come a real maintenance headache. What you ideally need is a way to ensure that the Kit properly picks the task that has the additional dependency. This is achieved by designating a particular task as the "primary" task from among a group of tasks with the same name. When a task is "primary", the Kit will always choose that task instead of the others with the same name.