Session Rule Implementation

From TrainzOnline
(Difference between revisions)
Jump to: navigation, search
Line 1: Line 1:
 
This page seeks to clearly define expected script behavior for [[KIND Scenariobehavior|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.
 
This page seeks to clearly define expected script behavior for [[KIND Scenariobehavior|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 =
 
= Legacy Behavior Note =
Line 7: Line 8:
  
 
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.
 
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 =
 
= The Rule Hierarchy =
Line 22: Line 24:
 
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.
 
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).
+
The rule may optionally respond to specific changes in the game world by deflagging 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.
 
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.
Line 90: Line 92:
 
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.
 
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.
 
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.
 +
 +
=== Custom Execution with Multiple Children ===
 +
This style is used for very specific custom-purpose rules, such as a passenger station stop handler. Such rules should give a clear and complete explanation of their operation and their interaction with any child rules in the rule editor interface.
 +
 +
Individual child rules may be executed in an ad-hoc manner by the rule in response to changes in game state. Child rules may or may not be executed simultaneously and may or may not be executed repeatedly depending on the rule; any limitations or gotchas deriving from this should be carefully explained in the rule editor interface.
 +
 +
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.
 +
 +
 +
= Early Termination via Pause =
 +
A rule must accept being paused by its parent at any time. This should cause the rule to cease any game state monitoring or other direct effects. The rule must pause all child rules immediately.
 +
 +
In cases where the rule is maintaining some reversible effect on the game world, pausing the rule should end this effect. For example, a rule which displays a message to the user should typically hide the message upon becoming paused, and a rule which causes a rain storm in a specific area should return the weather to normal upon becoming paused. Rules which make a single permanent change to the game world and then immediately flag as COMPLETE should not attempt to reverse the change upon becoming paused. Examples of these rules include configuration of the global weather conditions, or a rule which increments the player's score or penalty counter.
 +
 +
It is important to note that any pause is resumable, although the parent may not choose to take advantage of this. Pausing a rule followed by unpausing it should result in the rule continuing operation where it left off. The notes on Saving and Loading Driver Sessions (below) are worth considering when evaluating techniques for implementing pause.
 +
 +
 +
= Early Termination via Internal Logic =
 +
In some cases, a rule may begin executing child rules but then make an internal decision to abort execution. This typically occurs in conditional rules where the condition may be re-evaluated as false after it has already been evaluated as true. Such behavior should be made explicit in the rule editor interface and should usually be made optional by providing a checkbox.
 +
 +
There are in fact several possible behavioral variants affecting how condition-deflag is interpreted:
 +
* Once the condition is flagged, the rule starts executing child rules and no further evaluation is performed. The rule eventually flags itself as COMPLETE and does not later deflag this.
 +
* Once the condition is flagged, the rule starts executing child rules. If the condition becomes deflagged, the child rules are paused. If the condition becomes flagged again, the child rules are continued. Once the child rules have fully completed execution, the rule flags itself as COMPLETE and does not later deflag this.
 +
* Once the condition is flagged, the rule starts executing child rules. If the condition becomes deflagged, the child rules are paused. If the condition becomes flagged again, the children are reset and execution starts anew. The rule flags itself as COMPLETE if all children successfully complete, but it will deflag if the condition deflags.
 +
* Once the condition is flagged, the rule starts executing child rules. If the condition becomes deflagged, the child rules continue executing. If the condition becomes flagged again, the children are reset and execution starts anew. The rule will flag as COMPLETE if all children successfully complete, but it will deflag if the condition deflags.
 +
* Once the condition is flagged, the rule starts executing child rules. The child rules continue execution with no regard for whether the condition changes between flagged or deflagged in either direction. If the condition transitions from deflagged to flagged again after the children have completed, the children are reset and execution starts anew. The rule will flag as COMPLETE if all children successfully complete, but it will deflag itself if the condition deflags.
 +
 +
The complexity here derives from the fact that it is not possible to use the same set of rules multiple times concurrently for different trigger conditions. For example, if a set of rules is used to give demerit points and provide user notifications, and those rules live under a single conditional check which determines whether a train is speeding, it may be difficult to define what should happen if two trains are detected as speeding at the same time, or if a train stops speeding while the notification process is still in progress. It is not always possible to achieve a perfect solution to tracking multiple objects using a single set of rules, but correct selection from the above behaviors can minimise the possibility of serious side-effects.
 +
 +
It is important to note that these behaviors describe how the rule reacts to internal condition checks. The rule's reaction to being paused by its parent is discussed above under "Early Termination via Pause" and is not affected by the description here.
 +
 +
In short, there are many ways of handling this, and the important thing is for your (parent) rule to clearly explain which approach it is going to use.
 +
 +
 +
= Rule Reset =
 +
A rule may be ''reset'' by its parent. This causes both the COMPLETE and WAS COMPLETE states to be deflagged. Individual rules may also react to the reset by modifying their internal state to forget past or present actions. Parent rules must also cause their children to be reset in turn.
 +
 +
This mechanism is used to permit rules to be used in a repeating scenario, such as responding to a condition which can occur multiple times, or in a repeating loop. At some point after a rule has become flagged as WAS COMPLETE, it will typically become paused and will not be unpaused again. If the rule is to be included in some repeating behavior, this would normally prevent the behavior from operating successfully after the first run through. To avoid this, the parent rule responsible for implementing the repeating behavior will ''reset'' all children when a repeat occurs. This effectively cleans the rules back to their initial state ready to perform their roles again.
 +
 +
It is important to note that a reset should cause the rule to return to its configured settings. For example, if a timer value is configured and is decremented during rule operation, the timer must be reset back to its initial value during rule reset.
 +
 +
It is important to note that there is no requirement to pause a rule prior to resetting it.
 +
 +
 +
= Saving and Loading Driver Sessions =
 +
For better or worse, the script VM does not persist across a save/load operation. This means that each rule must correctly save both its configuration state and its runtime state, and must correctly reload those such that the state after a save/load operation is indistinguishable from the original state. The following aspects of the script state must be considered:
 +
 +
* Member variables in the Rule instance.
 +
* Which threads are running on the Rule instance, and where they are up to in the code flow.
 +
* Local variables on the stack of each running thread.
 +
* Non-persistent changes made to the state of other script objects (such as active 'sniff' statements).
 +
* Script Messages which might be in flight at the time of save (these are not persisted, so the net effect is that they are simply deleted between the save and the load.)
 +
 +
Since the game state continues normal operation after a Save, no code flow or other behavioral changes should result from the Save operation itself. Instead, the Save operation (and the design of the rule as a whole) is responsible for ensuring that sufficient data is persisted in the save file such that the Load operation is able to restore to the correct running state.
 +
 +
The rule base class, ScenarioBehavior, is responsible for saving and loading the rule state flags (COMPLETE, PAUSED, WAS COMPLETE, etc.) A rule which was paused at Save time will still be paused after the Load. A rule which was complete at Save time will still be complete after the Load. During a Load operation, the rule should inherit the parent class' SetProperties() method so that these flags are correctly configured, then honor the paused state during the rest of the loading process.
 +
 +
It is rarely appropriate for an unpaused rule to simply restart execution from the beginning. Care must be taken to ensure that execution resumes from the point that it left off at the time of saving. This process can be simplified by keeping as much state as possible in the rule member variables at all times, and only keeping reconstructible temporary values on the stack. If the member variables are correctly stored and restored, this approach will allow the loading process to detect the runtime state of the rule and resume any necessary threads, which will in turn be able to jump forward to the correct area of the code based on the stored state.
 +
 +
Finally, the COMPLETE, WAS COMPLETE, and PAUSED flags on child rules should be honored. This is typically sufficient information to determine the running state of child rules, so no further state will generally need to be maintained. More accurately, the child rules take care of their own state at all times, and the parent only needs to react to changes in the child state. Since no child state change occurs across a Save/Load operation, the parent does not need to become involved.
 +
 +
For rules which derive ConditionalScenarioBehavior, the majority of load/save logic is handled automatically. The only state information that such a rule should typically preserve is the instance-specific configuration data. The condition-monitor thread will be automatically restarted as required, and there is generally no need to distinguish between a first-startup and any subsequent startup.
 +
 +
 +
= Spurious Events =
 +
There are various events (typically script messages) that a rule may listen for in order to detect state changes. It is important to note that spurious events are often possible, and that an event handler should often be coupled with a condition check. As a specific example, the "ScenarioBehavior", "Touch" message typically indicates that a child state has changed, but the message may in fact be sent at any time. This message should prompt the rule to evaluate whether any state changes are necessary, but the rule should not simply assume that a state change is necessary without performing an evaluation.
 +
 +
Similarly, calls to a function such as Pause() may in some cases have no effect. For example, if a rule is already executing and its parent calls Pause(false), the rule should not pause, reset, or change state in any way at all. Similarly, if a rule is already paused and its parent calls Pause(true), the rule should not unpause, reset, fire any events, or otherwise change state in any way at all.
 +
 +
The short version is: do not assume that every event and every method call is supposed to achieve something meaningful. Check first, and if the event is meaningless to you then do nothing at all.
 +
 +
 +
= Future Considerations =
 +
Rules should base their functionality on the state flags such as the PAUSED state, rather than attempting to detect and vary their functionality based on the state of the game.
 +
 +
For example, we currently (in development builds) expose the rules editor in Driver for debugging purposes. This, or some mechanism like it, is likely to be included in public builds in the future. Rules should not assume that it is impossible to be reconfigured during gameplay. This will of course introduce some side effects, such as when an unpaused rule is modified during execution. It is the rule's responsibility to ensure that these side effects do not lead to script exceptions or to leaked state. For example, if a single thread is supposed to be running, reconfiguring the rule should not cause a second thread to run simultaneously. If a rule sniffs a target vehicle, and the target vehicle is redefined, then the rule should unsniff the original vehicle rather than now sniffing two vehicles.
 +
 +
Likewise, we may in future allow some form of partial state swap between Driver and Surveyor. Rules should not assume that it is impossible to change from any one game state or module to any other game state or module, just because it is not possible at the current time. APIs already exist to detect such state changes where necessary, and in most cases it's simply not necessary to care whether your rule is in Surveyor or Driver - you should act based on the PAUSED state instead. Rules should deal with the game state as they find it rather than assuming that certain states will not ever occur.
 +
 +
 +
= Gotchas =
 +
There are a few potential gotchas in the script environment that are worth considering when writing any non-trivial rule.
 +
 +
== Response Latency ==
 +
Scripts tend to rely heavily on message passing and threading. In both cases, there is a delay between triggering a certain behavior (eg. posting an event, or calling a threaded function) and having the receiving code react. Other code may execute during this delay.
 +
 +
<code><pre>
 +
// Calling this might be expected to start and then stop the thread; in reality it
 +
// will likely simply start the thread and leave it running, because the message
 +
// is sent prior to the wait() statement starting.
 +
void DemonstrateBug1(void)
 +
{
 +
  Pause(false);
 +
  Pause(true);
 +
}
 +
 +
// In this case, we use a Sleep() statement to "ensure" that the thread has
 +
// finished starting. This isn't actually reliable, but it works as long as the
 +
// system isn't under too much load. We follow this up with a rapid pause-
 +
// unpause sequence. As above, this doesn't work - the message is sent,
 +
// then the function immediately checks whether the thread is still running
 +
// (which it is) and thus the thread isn't restarted. The function completes,
 +
// and finally the thread receives the message and exits.
 +
void DemonstrateBug2(void)
 +
{
 +
  Pause(false);
 +
  Sleep(1.0);
 +
  Pause(true);
 +
  Pause(false);
 +
}
 +
 +
// Helper code for the above functions.
 +
void Pause(bool bIsPaused)
 +
{
 +
  if (bIsPaused)
 +
  {
 +
    PostMessage(me, "ThisRule", "Pause");
 +
  }
 +
  else if (!m_bIsThreadRunning)
 +
  {
 +
    m_bIsThreadRunning = true;
 +
    StartThread();
 +
  }
 +
}
 +
thread void StartThread(void)
 +
{
 +
  while (true)
 +
  {
 +
    wait ()
 +
    {
 +
    on "ThisRule", "Pause":
 +
      break;
 +
    ...
 +
  }
 +
  m_bIsThreadRunning = false;
 +
}
 +
</pre></code>
 +
 +
 +
== Multiple Messages in Flight ==
 +
It's possible to post multiple identical messages. This can be useful, but if you're expecting only one then it can be a source of errors.
 +
 +
In the following pseudocode, each call to SetProperties() will place an additional "Timer", "Tick" message into the cycle, effectively increasing the rate at which the IdleFunction is called. This will seem to be working for a single call to SetProperties(), which may be all that is called during a trivial testcase. It will probably give reasonable results for a few calls to SetProperties(), although if the function is time-sensitive then the results might be different to expectations. After a larger number of calls to SetProperties(), the object's message queue may become full, causing it to lose messages at random (perhaps the tick messages, or perhaps other unrelated messages that are more problematic).
 +
 +
<code><pre>
 +
void Init(void)
 +
{
 +
  inherited();
 +
 
 +
  AddHandler(me, "Timer", "Tick", "IdleFunction");
 +
}
 +
 +
void EnterIdleState(void)
 +
{
 +
  IdleFunction();
 +
}
 +
 +
void IdleFunction(void)
 +
{
 +
  PostMessage(me, "Timer", "Tick", 1.0);
 +
  .. do something here ..
 +
}
 +
 +
void SetProperties(Soup soup)
 +
{
 +
  inherited(soup);
 +
 
 +
  EnterIdleState();
 +
}
 +
 +
</pre></code>
 +
 +
A similar problem can occur if you start new threads rather than post new messages.
 +
 +
 +
== Thread Guard Variables ==
 +
It's common to prevent a singleton thread from being started twice by using a boolean guard variable. Care must be taken to ensure that the guard variable is set and cleared at the correct locations, or they may be ineffective.
 +
 +
The following pseudocode demonstrates one such correct usage. The key points to note are:
 +
* We set 'm_bIsThreadedFunctionRunning' prior to calling the threaded function. Setting it within the threaded function is too late; multiple threads might have already been started before the first thread reaches that point.
 +
* We don't confuse whether the thread is actually running with whether we want the thread to be running. Once we ask a thread to start, we can't stop it starting. If we change our minds and want to stop it again, we must inform it in a way that it will realise after it finishes starting. In the worst-case scenario, we might flipflop on this multiple times before the thread actually starts.
 +
* Similarly, we don't rely on messages to control the thread. Enqueuing a separate start or stop message with each StartThread()/StopThread() call might be possible, and we could clear any unwanted prior messages from the object's queue, but if the thread hasn't finished starting then it won't receive the messages anyway.
 +
* We do rely on messages to wake the thread if it's waiting on messages, but we don't use those messages to imply any state - we just have it recheck the current state boolean to get the very latest info.
 +
* We clear 'm_bIsThreadedFunctionRunning' once the decision to exit has become unconditional. At this point, even though the thread is technically still running, we'll have to start a new thread if we want the thread to continue running.
 +
 +
<code><pre>
 +
bool m_bIsThreadedFunctionRunning = false;
 +
bool m_bWantThreadedFunctionRunning = false;
 +
 +
void StartThread(void)
 +
{
 +
  m_bWantThreadedFunctionRunning = true;
 +
 
 +
  if (m_bIsThreadedFunctionRunning)
 +
    return;
 +
 
 +
  m_bIsThreadedFunctionRunning = true;
 +
  ThreadedFunction();
 +
}
 +
 +
void StopThread(void)
 +
{
 +
  if (!m_bWantThreadedFunctionRunning)
 +
    return;
 +
 
 +
  m_bWantThreadedFunctionRunning = false;
 +
  PostMessage(me, "Thread", "Touch");
 +
}
 +
 +
thread void ThreadedFunction(void)
 +
{
 +
  while (m_bWantThreadedFunctionRunning)
 +
  {
 +
    wait()
 +
    {
 +
      on "Thread", "Touch":
 +
        break;
 +
      .. whatever else..
 +
    }
 +
  }
 +
  m_bIsThreadedFunctionRunning = false;
 +
}
 +
</pre></code>
 +
 +
 +
== Not Waiting For Messages ==
 +
As seen above, it's possible for a thread to fail to receive posted messages because it wasn't waiting for them at the time. A more commonly problematic example follows.
 +
 +
<code><pre>
 +
thread void MyThread(void)
 +
{
 +
  wait ()
 +
  {
 +
    on "Some", "Condition":
 +
      HandleSomeCondition();
 +
      continue;
 +
    on "Thread", "Exit":
 +
      break;
 +
  }
 +
}
 +
 +
void HandleSomeCondition(void)
 +
{
 +
  wait ()
 +
  {
 +
    on "Some", "End-Condition":
 +
      break;
 +
  }
 +
}
 +
</pre></code>
 +
 +
In this example, the thread is waiting on both "Some", "Condition" and also "Thread", "Exit". Either one will be handled correctly assuming that the thread is already running at the time they are posted. Even if multiple are posted, the thread will simply queue them and run each in turn.
 +
 +
The problem occurs when HandleSomeCondition() is triggered and the thread begins waiting on "Some", "End-Condition". In this scenario, the thread is no longer waiting on "Thread", "Exit", and any attempt to post that message will simply be ignored. To reiterate: the message dispatch is NOT postponed until the function returns; the message is dropped entirely.

Revision as of 02:44, 28 November 2018

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.


Contents

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:

PAUSED

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.

COMPLETE

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

WAS COMPLETE

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.

DOES COMPLETE

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

Initial State

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.

Scheduling

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.

Child Rules

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.

Custom Execution with Multiple Children

This style is used for very specific custom-purpose rules, such as a passenger station stop handler. Such rules should give a clear and complete explanation of their operation and their interaction with any child rules in the rule editor interface.

Individual child rules may be executed in an ad-hoc manner by the rule in response to changes in game state. Child rules may or may not be executed simultaneously and may or may not be executed repeatedly depending on the rule; any limitations or gotchas deriving from this should be carefully explained in the rule editor interface.

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.


Early Termination via Pause

A rule must accept being paused by its parent at any time. This should cause the rule to cease any game state monitoring or other direct effects. The rule must pause all child rules immediately.

In cases where the rule is maintaining some reversible effect on the game world, pausing the rule should end this effect. For example, a rule which displays a message to the user should typically hide the message upon becoming paused, and a rule which causes a rain storm in a specific area should return the weather to normal upon becoming paused. Rules which make a single permanent change to the game world and then immediately flag as COMPLETE should not attempt to reverse the change upon becoming paused. Examples of these rules include configuration of the global weather conditions, or a rule which increments the player's score or penalty counter.

It is important to note that any pause is resumable, although the parent may not choose to take advantage of this. Pausing a rule followed by unpausing it should result in the rule continuing operation where it left off. The notes on Saving and Loading Driver Sessions (below) are worth considering when evaluating techniques for implementing pause.


Early Termination via Internal Logic

In some cases, a rule may begin executing child rules but then make an internal decision to abort execution. This typically occurs in conditional rules where the condition may be re-evaluated as false after it has already been evaluated as true. Such behavior should be made explicit in the rule editor interface and should usually be made optional by providing a checkbox.

There are in fact several possible behavioral variants affecting how condition-deflag is interpreted:

  • Once the condition is flagged, the rule starts executing child rules and no further evaluation is performed. The rule eventually flags itself as COMPLETE and does not later deflag this.
  • Once the condition is flagged, the rule starts executing child rules. If the condition becomes deflagged, the child rules are paused. If the condition becomes flagged again, the child rules are continued. Once the child rules have fully completed execution, the rule flags itself as COMPLETE and does not later deflag this.
  • Once the condition is flagged, the rule starts executing child rules. If the condition becomes deflagged, the child rules are paused. If the condition becomes flagged again, the children are reset and execution starts anew. The rule flags itself as COMPLETE if all children successfully complete, but it will deflag if the condition deflags.
  • Once the condition is flagged, the rule starts executing child rules. If the condition becomes deflagged, the child rules continue executing. If the condition becomes flagged again, the children are reset and execution starts anew. The rule will flag as COMPLETE if all children successfully complete, but it will deflag if the condition deflags.
  • Once the condition is flagged, the rule starts executing child rules. The child rules continue execution with no regard for whether the condition changes between flagged or deflagged in either direction. If the condition transitions from deflagged to flagged again after the children have completed, the children are reset and execution starts anew. The rule will flag as COMPLETE if all children successfully complete, but it will deflag itself if the condition deflags.

The complexity here derives from the fact that it is not possible to use the same set of rules multiple times concurrently for different trigger conditions. For example, if a set of rules is used to give demerit points and provide user notifications, and those rules live under a single conditional check which determines whether a train is speeding, it may be difficult to define what should happen if two trains are detected as speeding at the same time, or if a train stops speeding while the notification process is still in progress. It is not always possible to achieve a perfect solution to tracking multiple objects using a single set of rules, but correct selection from the above behaviors can minimise the possibility of serious side-effects.

It is important to note that these behaviors describe how the rule reacts to internal condition checks. The rule's reaction to being paused by its parent is discussed above under "Early Termination via Pause" and is not affected by the description here.

In short, there are many ways of handling this, and the important thing is for your (parent) rule to clearly explain which approach it is going to use.


Rule Reset

A rule may be reset by its parent. This causes both the COMPLETE and WAS COMPLETE states to be deflagged. Individual rules may also react to the reset by modifying their internal state to forget past or present actions. Parent rules must also cause their children to be reset in turn.

This mechanism is used to permit rules to be used in a repeating scenario, such as responding to a condition which can occur multiple times, or in a repeating loop. At some point after a rule has become flagged as WAS COMPLETE, it will typically become paused and will not be unpaused again. If the rule is to be included in some repeating behavior, this would normally prevent the behavior from operating successfully after the first run through. To avoid this, the parent rule responsible for implementing the repeating behavior will reset all children when a repeat occurs. This effectively cleans the rules back to their initial state ready to perform their roles again.

It is important to note that a reset should cause the rule to return to its configured settings. For example, if a timer value is configured and is decremented during rule operation, the timer must be reset back to its initial value during rule reset.

It is important to note that there is no requirement to pause a rule prior to resetting it.


Saving and Loading Driver Sessions

For better or worse, the script VM does not persist across a save/load operation. This means that each rule must correctly save both its configuration state and its runtime state, and must correctly reload those such that the state after a save/load operation is indistinguishable from the original state. The following aspects of the script state must be considered:

  • Member variables in the Rule instance.
  • Which threads are running on the Rule instance, and where they are up to in the code flow.
  • Local variables on the stack of each running thread.
  • Non-persistent changes made to the state of other script objects (such as active 'sniff' statements).
  • Script Messages which might be in flight at the time of save (these are not persisted, so the net effect is that they are simply deleted between the save and the load.)

Since the game state continues normal operation after a Save, no code flow or other behavioral changes should result from the Save operation itself. Instead, the Save operation (and the design of the rule as a whole) is responsible for ensuring that sufficient data is persisted in the save file such that the Load operation is able to restore to the correct running state.

The rule base class, ScenarioBehavior, is responsible for saving and loading the rule state flags (COMPLETE, PAUSED, WAS COMPLETE, etc.) A rule which was paused at Save time will still be paused after the Load. A rule which was complete at Save time will still be complete after the Load. During a Load operation, the rule should inherit the parent class' SetProperties() method so that these flags are correctly configured, then honor the paused state during the rest of the loading process.

It is rarely appropriate for an unpaused rule to simply restart execution from the beginning. Care must be taken to ensure that execution resumes from the point that it left off at the time of saving. This process can be simplified by keeping as much state as possible in the rule member variables at all times, and only keeping reconstructible temporary values on the stack. If the member variables are correctly stored and restored, this approach will allow the loading process to detect the runtime state of the rule and resume any necessary threads, which will in turn be able to jump forward to the correct area of the code based on the stored state.

Finally, the COMPLETE, WAS COMPLETE, and PAUSED flags on child rules should be honored. This is typically sufficient information to determine the running state of child rules, so no further state will generally need to be maintained. More accurately, the child rules take care of their own state at all times, and the parent only needs to react to changes in the child state. Since no child state change occurs across a Save/Load operation, the parent does not need to become involved.

For rules which derive ConditionalScenarioBehavior, the majority of load/save logic is handled automatically. The only state information that such a rule should typically preserve is the instance-specific configuration data. The condition-monitor thread will be automatically restarted as required, and there is generally no need to distinguish between a first-startup and any subsequent startup.


Spurious Events

There are various events (typically script messages) that a rule may listen for in order to detect state changes. It is important to note that spurious events are often possible, and that an event handler should often be coupled with a condition check. As a specific example, the "ScenarioBehavior", "Touch" message typically indicates that a child state has changed, but the message may in fact be sent at any time. This message should prompt the rule to evaluate whether any state changes are necessary, but the rule should not simply assume that a state change is necessary without performing an evaluation.

Similarly, calls to a function such as Pause() may in some cases have no effect. For example, if a rule is already executing and its parent calls Pause(false), the rule should not pause, reset, or change state in any way at all. Similarly, if a rule is already paused and its parent calls Pause(true), the rule should not unpause, reset, fire any events, or otherwise change state in any way at all.

The short version is: do not assume that every event and every method call is supposed to achieve something meaningful. Check first, and if the event is meaningless to you then do nothing at all.


Future Considerations

Rules should base their functionality on the state flags such as the PAUSED state, rather than attempting to detect and vary their functionality based on the state of the game.

For example, we currently (in development builds) expose the rules editor in Driver for debugging purposes. This, or some mechanism like it, is likely to be included in public builds in the future. Rules should not assume that it is impossible to be reconfigured during gameplay. This will of course introduce some side effects, such as when an unpaused rule is modified during execution. It is the rule's responsibility to ensure that these side effects do not lead to script exceptions or to leaked state. For example, if a single thread is supposed to be running, reconfiguring the rule should not cause a second thread to run simultaneously. If a rule sniffs a target vehicle, and the target vehicle is redefined, then the rule should unsniff the original vehicle rather than now sniffing two vehicles.

Likewise, we may in future allow some form of partial state swap between Driver and Surveyor. Rules should not assume that it is impossible to change from any one game state or module to any other game state or module, just because it is not possible at the current time. APIs already exist to detect such state changes where necessary, and in most cases it's simply not necessary to care whether your rule is in Surveyor or Driver - you should act based on the PAUSED state instead. Rules should deal with the game state as they find it rather than assuming that certain states will not ever occur.


Gotchas

There are a few potential gotchas in the script environment that are worth considering when writing any non-trivial rule.

Response Latency

Scripts tend to rely heavily on message passing and threading. In both cases, there is a delay between triggering a certain behavior (eg. posting an event, or calling a threaded function) and having the receiving code react. Other code may execute during this delay.

// Calling this might be expected to start and then stop the thread; in reality it
// will likely simply start the thread and leave it running, because the message
// is sent prior to the wait() statement starting.
void DemonstrateBug1(void)
{
  Pause(false);
  Pause(true);
}

// In this case, we use a Sleep() statement to "ensure" that the thread has
// finished starting. This isn't actually reliable, but it works as long as the
// system isn't under too much load. We follow this up with a rapid pause-
// unpause sequence. As above, this doesn't work - the message is sent,
// then the function immediately checks whether the thread is still running
// (which it is) and thus the thread isn't restarted. The function completes,
// and finally the thread receives the message and exits.
void DemonstrateBug2(void)
{
  Pause(false);
  Sleep(1.0);
  Pause(true);
  Pause(false);
}

// Helper code for the above functions.
void Pause(bool bIsPaused)
{
  if (bIsPaused)
  {
    PostMessage(me, "ThisRule", "Pause");
  }
  else if (!m_bIsThreadRunning)
  {
    m_bIsThreadRunning = true;
    StartThread();
  }
}
thread void StartThread(void)
{
  while (true)
  {
    wait ()
    {
    on "ThisRule", "Pause":
      break;
     ...
  }
  m_bIsThreadRunning = false;
}


Multiple Messages in Flight

It's possible to post multiple identical messages. This can be useful, but if you're expecting only one then it can be a source of errors.

In the following pseudocode, each call to SetProperties() will place an additional "Timer", "Tick" message into the cycle, effectively increasing the rate at which the IdleFunction is called. This will seem to be working for a single call to SetProperties(), which may be all that is called during a trivial testcase. It will probably give reasonable results for a few calls to SetProperties(), although if the function is time-sensitive then the results might be different to expectations. After a larger number of calls to SetProperties(), the object's message queue may become full, causing it to lose messages at random (perhaps the tick messages, or perhaps other unrelated messages that are more problematic).

void Init(void)
{
  inherited();
  
  AddHandler(me, "Timer", "Tick", "IdleFunction");
}

void EnterIdleState(void)
{
  IdleFunction();
}

void IdleFunction(void)
{
  PostMessage(me, "Timer", "Tick", 1.0);
  .. do something here ..
}

void SetProperties(Soup soup)
{
  inherited(soup);
  
  EnterIdleState();
}

A similar problem can occur if you start new threads rather than post new messages.


Thread Guard Variables

It's common to prevent a singleton thread from being started twice by using a boolean guard variable. Care must be taken to ensure that the guard variable is set and cleared at the correct locations, or they may be ineffective.

The following pseudocode demonstrates one such correct usage. The key points to note are:

  • We set 'm_bIsThreadedFunctionRunning' prior to calling the threaded function. Setting it within the threaded function is too late; multiple threads might have already been started before the first thread reaches that point.
  • We don't confuse whether the thread is actually running with whether we want the thread to be running. Once we ask a thread to start, we can't stop it starting. If we change our minds and want to stop it again, we must inform it in a way that it will realise after it finishes starting. In the worst-case scenario, we might flipflop on this multiple times before the thread actually starts.
  • Similarly, we don't rely on messages to control the thread. Enqueuing a separate start or stop message with each StartThread()/StopThread() call might be possible, and we could clear any unwanted prior messages from the object's queue, but if the thread hasn't finished starting then it won't receive the messages anyway.
  • We do rely on messages to wake the thread if it's waiting on messages, but we don't use those messages to imply any state - we just have it recheck the current state boolean to get the very latest info.
  • We clear 'm_bIsThreadedFunctionRunning' once the decision to exit has become unconditional. At this point, even though the thread is technically still running, we'll have to start a new thread if we want the thread to continue running.
bool m_bIsThreadedFunctionRunning = false;
bool m_bWantThreadedFunctionRunning = false;

void StartThread(void)
{
  m_bWantThreadedFunctionRunning = true;
  
  if (m_bIsThreadedFunctionRunning)
    return;
  
  m_bIsThreadedFunctionRunning = true;
  ThreadedFunction();
}

void StopThread(void)
{
  if (!m_bWantThreadedFunctionRunning)
    return;
  
  m_bWantThreadedFunctionRunning = false;
  PostMessage(me, "Thread", "Touch");
}

thread void ThreadedFunction(void)
{
  while (m_bWantThreadedFunctionRunning)
  {
    wait()
    {
      on "Thread", "Touch":
        break;
      .. whatever else..
    }
  }
  m_bIsThreadedFunctionRunning = false;
}


Not Waiting For Messages

As seen above, it's possible for a thread to fail to receive posted messages because it wasn't waiting for them at the time. A more commonly problematic example follows.

thread void MyThread(void)
{
  wait ()
  {
    on "Some", "Condition":
      HandleSomeCondition();
      continue;
    on "Thread", "Exit":
      break;
  }
}

void HandleSomeCondition(void)
{
  wait ()
  {
    on "Some", "End-Condition":
      break;
  }
}

In this example, the thread is waiting on both "Some", "Condition" and also "Thread", "Exit". Either one will be handled correctly assuming that the thread is already running at the time they are posted. Even if multiple are posted, the thread will simply queue them and run each in turn.

The problem occurs when HandleSomeCondition() is triggered and the thread begins waiting on "Some", "End-Condition". In this scenario, the thread is no longer waiting on "Thread", "Exit", and any attempt to post that message will simply be ignored. To reiterate: the message dispatch is NOT postponed until the function returns; the message is dropped entirely.

Personal tools