Session Rule Implementation
This page seeks to clearly define expected script behavior for Session Rule assets. This focuses specifically on correct behavior from the code perspective, rather than addressing how rules should be designed for best gameplay outcomes.
Legacy Behavior Note
Please note that many existing rules do not behave correctly as defined here. This is considered a bug in the individual session rules. N3V Games has a policy of updating session rules to behave correctly even where that may lead to changes of function in existing session assets. Where reasonable to achieve, emulation of the existing (buggy) behavior may occur for existing rule instances in existing sessions, however any newly created rule instances will use the corrected behavior.
These expectations were codified at the end of TS12 development due to an increasing inability of session rules to work together resulting from the unpredictable nature of their responses to various common use-cases. The systems as a whole, including most of the core rules discussed in this document, have existed for far longer than this document. The document does not redefine any existing systems, but simply presents the only reasonable implementation approach possible if rules are to be expected to work properly in all use-cases. In some cases we also choose to mandate specific points of behavior relating to user expectations rather than technical necessities.
Certain additional API mechanisms were introduced alongside the development of this document, to allow better communication with session authors regarding the differences in behavior between certain types of rule. The correct usage for these mechanisms is also included in this document.
The Rule Hierarchy
All rules within a given session form a simple tree structure.
Rule State Flags
Each rule in the tree may have certain associated state flagged, including a combination of the following:
The rule is currently paused, meaning that it does not affect the game world in any way and does not respond to any changes in the game world. The parent rule must cause the rule to become unpaused for the rule to have any effect. It is not correct behavior for a rule to unpause itself.
This flag indicates that rule has fully completed its operation (including any child rule operations) and does not affect the game world in any way. This flag is set by the rule itself, not by an outside entity such as the parent. This state should not be changed while the rule is PAUSED.
Being COMPLETE does not imply being PAUSED. In many scenarios, the parent rule may respond to a child becoming COMPLETE by causing it to also become PAUSED, however this is certainly not guaranteed in all cases, and is not guaranteed to be immediate in the cases where it does happen.
The rule may optionally respond to specific changes in the game world by de-flagging the COMPLETE status. For example, a rule which specifically implements a condition check may choose to become COMPLETE when that condition is met, but return to an incomplete state when the condition is no longer met (again, remembering that a change in either direction should only happen while the rule itself is not PAUSED).
When a rule's COMPLETE state changes, a "ScenarioBehavior", "Touch" message is sent (TBD: BY WHAT MECHANISM?) to its parent rule (if any), unless the parent is in the PAUSED state. This allows the parent to react to changes in its child rules' states. It is important to note that the message indicates a possible change which the parent may be interested in, but it is the parent rule's responsibility to determine whether the state change is worth reacting to.
This is typically set when a rule first becomes flagged as COMPLETE, and will stay set even if the rule later flags as incomplete. This can be used by a parent which only cares that the child rule has been flagged as COMPLETE at some point, rather than caring what its current completion status is.
This indicates whether the rule can ever reach the COMPLETE state. Its value is constant for a given rule configuration, and does not change based on the runtime rule state or other game state. Rules which are flagged as DOES COMPLETE are expected to become complete after some discreet operations are completed. Rules which are not flagged are expected to run indefinitely unless PAUSED by an external agent (such as the parent rule).
All rules are initially in the PAUSED state when in Surveyor, and only become unpaused after gameplay begins in a Driver session. Since a PAUSED rule is not permitted to unpause itself, this is a stable state which will persist until the native code takes step to begin program flow.
Top Level Rules
All rules at the top level (ie. rules which are not a child of any other rule) simultaneously become unpaused after the session has loaded into Driver. Some of these rules may take initial setup steps and then become COMPLETE. Some of these rules may begin monitoring for specific game conditions before taking any further action. Some of these rules may begin taking action immediately. The rules are not sequenced against each other in any way, and it is the session author's responsibility to ensure that this does not cause any conflicts.
Since the script VM is a cooperatively-threaded environment, it is technically true that rule evaluation will occur in an ordered manner rather than truly simultaneously. However, no guarantees are made regarding the order in which the simultaneously-running rules will perform their checks or outcomes. Since rules may wait internally rather than fulfilling their entire purpose in an atomic operation, it is also not guaranteed that a first-running rule will fully complete before some other simultaneously-executing rule begins completion. In short, if order-of-execution is critical to the correct behavior of a session, then the order should be enforced by the session creator using the rule hierarchy rather than by making assumptions about execution order of simultaneously-running rules.
(Note: with the introduction of Asynchronous Route Streaming, this is even more true than previously. What may have been a near-instantaneous operation in older builds may now require a wait of several seconds while the necessary data is streamed in.)
It is important to note that some conditional rules may partially or completely rely on a polling behavior to evaluate their conditions. (An example of partial reliance is a rule which waits for an event, but then evaluates a condition to clarify whether the event should be acted upon.) In these cases, it is possible that the condition can rapidly become true and then become false again without the rule responding. This results in an uncertainty as to whether the conditional rule will correctly detect a given instance of the condition. This type of scenario should be avoided completely where possible, and should most definitely be limited to outcomes that do not significantly affect gameplay.
For a hypothetical example, if the player's train speeds for 0.5sec, and a speeding check rule was implemented that polled every second, then the overspeed event may or may not trigger on any given occurrence. A session creator should not use this outcome as a failure condition, since some players may be able to speed without penalty, whereas other players would be penalised on the first attempt. A sensible solution in this case might be to increment a counter while speeding is detected, and only take action if the counter reaches a certain threshold. If the only penalty was a warning to the player which had no actual gameplay impact, then it might be acceptable to forgo this check and simply accept that some players will receive a warning where others may not.
As with all other rules, child rules (ie. those that have parent rules in the hierarchy) start out in the PAUSED state. They do not become unpaused until the parent is ready for the children to start operating. Exactly when this occurs depends on the specific parent rule, but the general flow is the same for all rules:
1. Parent rule becomes unpaused and begins operation. 2. Parent rule configures any necessary world state changes. 3. Parent rule optionally waits for a specific condition to occur. 4. Parent rule begins executing its children. This involves unpausing one or more of the children (as discussed below in "Parent Rule Styles"). 5. Parent waits for its children to complete (as discussed below in "Parent Rule Styles") and then pauses the children. 6. Parent flags itself as COMPLETE.
This process allows program control flow to pass from a top-level rule, to its immediate children, to their immediate children, and so on. As the bottom-level children become complete, completion flows back to the higher level children until finally the top-level rules become COMPLETE.
It is worth noting that there is nothing special about a top-level rule being flagged as COMPLETE. This does not indicate an end-of-session or any other fundamental gameplay mode change. Since the pausing of rules on completion is handled by the parent rule (see step 5 above), the top-level rules never become PAUSED on completion.
Parent Rule Styles
There are a few common techniques with which parent rules interact with their children. Rules should always fit one of these descriptions. The vast majority of rules should follow either the "Does Not Support Children" style, or the "Ordered Execution of Children" style.
Does Not Support Children
The rule is incapable of supporting children and will always ignore them.
This is used for trivial rules which perform a set function and then become permanently COMPLETE. Such rules are not conditional and do not wait for any world state changes except those which they initiate internally. The rule should be configured to validate and/or enforce that no child rules can be added.
Simultaneous Execution of Children
When the rule is ready to begin executing children, it unpauses all children simultaneously. The rule then waits for all children to flag as WAS COMPLETE. Once this is detected, the rule pauses all children and flags itself as COMPLETE. This is used for rules which have a specific need to simultaneously execute children, such as the "Simultaneous List" rule.
The rule editor interface should be explicit that child rules will be executed simultaneously. Child rules should be tagged appropriately in the rule editor list to denote unordered execution.
Ordered Execution of Children
When the rule is ready to begin executing children, it iterates through the children in top-to-bottom order. If the child is flagged as WAS COMPLETE, the child is paused and iteration continues. If the child is not flagged as WAS COMPLETE, the child is unpaused and iteration ends. Whenever a child state change is detected, this process is repeated from the start. Once iteration passes over all children without leaving any child unpaused, the rule flags itself as COMPLETE.
This style is used for all rules which do not explicitly fall under one of the other styles described here. Almost all third-party rules will follow this style. Implementing one of the other styles without there being a fundamental requirement to do so is considered a programming error.
Rules which follow this style should derive from ConditionalScenarioBehavior in order to avoid re-implementing the control flow logic, as it is exceedingly complex to implement correctly. Any rules which do not derive from ConditionalScenarioBehavior should be accompanied by a clear explanation of the reasons that ConditionalScenarioBehavior is an unsuitable base class for the intended behavior. Rules which do derive from ConditionalScenarioBehavior should avoid complex overrides of the default logic.
Child rules should be tagged appropriately in the rule editor list to denote execution order.
Controlled Selection of a Single Child
When the rule is ready to begin executing children, it selects a single child and unpauses it. All other children remain paused. Once the selected child becomes flagged as WAS COMPLETE, the child is paused and the rule flags itself as COMPLETE.
This style is used where complex conditional selection of child rules is required. This can be used to implement an "if-else" logic flow, a "switch-case" logic flow, or the "Random List" rule. This style should not be used to implement a simple "if" logic flow, because that would unnecessarily restrict the rule to a single child where the "Ordered Execution of Children" style would provide the same functionality while allowing multiple children. The rule editor interface should be very explicit about how the child rules will be selected. Child rules should be tagged appropriately in the rule editor list to avoid the user attempting to fit a child rule into the wrong role.