public override void clearState()
 {
     this.formationStandingStartAnnounced            = false;
     this.formationStandingPreStartReminderAnnounced = false;
     this.numUpdatesActionSame       = 0;
     this.newFrozenOrderAction       = FrozenOrderAction.None;
     this.newDriverToFollow          = null;
     this.newFrozenOrderColumn       = FrozenOrderColumn.None;
     this.currFrozenOrderAction      = FrozenOrderAction.None;
     this.currDriverToFollow         = null;
     this.currFrozenOrderColumn      = FrozenOrderColumn.None;
     this.scrLastFCYLapLaneAnnounced = false;
 }
        override protected void triggerInternal(GameStateData previousGameState, GameStateData currentGameState)
        {
            var cgs = currentGameState;
            var pgs = previousGameState;

            if (cgs.PitData.InPitlane || /*|| cgs.SessionData.SessionRunningTime < 10 */
                GameStateData.onManualFormationLap ||  // We may want manual formation to phase of FrozenOrder.
                pgs == null)
            {
                return; // don't process if we're in the pits or just started a session
            }
            var cfod  = cgs.FrozenOrderData;
            var pfod  = pgs.FrozenOrderData;
            var cfodp = cgs.FrozenOrderData.Phase;

            if (cfodp == FrozenOrderPhase.None)
            {
                return;  // Nothing to do.
            }
            var useAmericanTerms = GlobalBehaviourSettings.useAmericanTerms || GlobalBehaviourSettings.useOvalLogic;
            var useOvalLogic     = GlobalBehaviourSettings.useOvalLogic;

            if (pfod.Phase == FrozenOrderPhase.None)
            {
                Console.WriteLine("Frozen Order: New Phase detected: " + cfod.Phase);
                int delay = Utilities.random.Next(0, 3);
                if (cfod.Phase == FrozenOrderPhase.Rolling)
                {
                    audioPlayer.playMessage(new QueuedMessage(folderRollingStartReminder, delay + 4, secondsDelay: delay, abstractEvent: this, priority: 10));
                }
                else if (cfod.Phase == FrozenOrderPhase.FormationStanding)
                {
                    audioPlayer.playMessage(new QueuedMessage(folderStandingStartReminder, delay + 4, secondsDelay: delay, abstractEvent: this, priority: 10));
                }

                // Clear previous state.
                this.clearState();
            }

            // Because FO Action is distance dependent, it tends to fluctuate.
            // We need to detect when it stabilizes (values stay identical for ACTION_STABLE_THRESHOLD times).
            if (cfod.Action == pfod.Action &&
                cfod.DriverToFollowRaw == pfod.DriverToFollowRaw &&
                cfod.AssignedColumn == pfod.AssignedColumn)
            {
                ++this.numUpdatesActionSame;
            }
            else
            {
                this.newFrozenOrderAction = cfod.Action;
                this.newDriverToFollow    = cfod.DriverToFollowRaw;
                this.newFrozenOrderColumn = cfod.AssignedColumn;

                this.numUpdatesActionSame = 0;
            }

            var isActionUpdateStable = this.numUpdatesActionSame >= FrozenOrderMonitor.ACTION_STABLE_THRESHOLD;

            // Detect if we should be following SC, as SC has no driver name.
            var shouldFollowSafetyCar = false;
            var driverToFollow        = "";

            if (cfodp == FrozenOrderPhase.Rolling || cfodp == FrozenOrderPhase.FullCourseYellow)
            {
                shouldFollowSafetyCar = (cfod.AssignedColumn == FrozenOrderColumn.None && cfod.AssignedPosition == 1) || // Single file order.
                                        (cfod.AssignedColumn != FrozenOrderColumn.None && cfod.AssignedPosition <= 2); // Double file (grid) order.

                driverToFollow = shouldFollowSafetyCar ? (useAmericanTerms ? folderThePaceCar : folderTheSafetyCar) : cfod.DriverToFollowRaw;
            }

            if (cfodp == FrozenOrderPhase.Rolling)
            {
                var prevDriverToFollow    = this.currDriverToFollow;
                var prevFrozenOrderAction = this.currFrozenOrderAction;

                if (isActionUpdateStable &&
                    (this.currFrozenOrderAction != this.newFrozenOrderAction ||
                     this.currDriverToFollow != this.newDriverToFollow ||
                     this.currFrozenOrderColumn != this.newFrozenOrderColumn))
                {
                    this.currFrozenOrderAction = this.newFrozenOrderAction;
                    this.currDriverToFollow    = this.newDriverToFollow;
                    this.currFrozenOrderColumn = this.newFrozenOrderColumn;

                    // canReadDriverToFollow will be true if we're behind the safety car or we can read the driver's name:
                    var canReadDriverToFollow = shouldFollowSafetyCar || AudioPlayer.canReadName(driverToFollow);

                    var usableDriverNameToFollow = shouldFollowSafetyCar ? driverToFollow : DriverNameHelper.getUsableDriverName(driverToFollow);

                    var validationData = new Dictionary <string, object>();
                    validationData.Add(FrozenOrderMonitor.validateMessageTypeKey, FrozenOrderMonitor.validateMessageTypeAction);
                    validationData.Add(FrozenOrderMonitor.validationActionKey, cfod.Action);
                    validationData.Add(FrozenOrderMonitor.validationAssignedPositionKey, cfod.AssignedPosition);
                    validationData.Add(FrozenOrderMonitor.validationDriverToFollowKey, cfod.DriverToFollowRaw);

                    if (this.newFrozenOrderAction == FrozenOrderAction.Follow &&
                        prevDriverToFollow != this.currDriverToFollow)     // Don't announce Follow messages for the driver that we caught up to or allowed to pass.
                    {
                        if (canReadDriverToFollow)
                        {
                            // Follow messages are only meaningful if there's name to announce.
                            int delay = Utilities.random.Next(3, 6);
                            if (cfod.AssignedColumn == FrozenOrderColumn.None ||
                                Utilities.random.Next(1, 11) > 8)     // Randomly, announce message without coulmn info.
                            {
                                audioPlayer.playMessage(new QueuedMessage(!shouldFollowSafetyCar ? "frozen_order/follow_driver" : "frozen_order/follow_safety_car", delay + 6,
                                                                          secondsDelay: delay, messageFragments: MessageContents(folderFollow, usableDriverNameToFollow), abstractEvent: this,
                                                                          validationData: validationData, priority: 10));
                            }
                            else
                            {
                                string columnName;
                                if (useOvalLogic)
                                {
                                    columnName = cfod.AssignedColumn == FrozenOrderColumn.Left ? folderInTheInsideColumn : folderInTheOutsideColumn;
                                }
                                else
                                {
                                    columnName = cfod.AssignedColumn == FrozenOrderColumn.Left ? folderInTheLeftColumn : folderInTheRightColumn;
                                }
                                audioPlayer.playMessage(new QueuedMessage(!shouldFollowSafetyCar ? "frozen_order/follow_driver_in_col" : "frozen_order/follow_safecy_car_in_col", delay + 6,
                                                                          secondsDelay: delay, messageFragments: MessageContents(folderFollow, usableDriverNameToFollow, columnName), abstractEvent: this,
                                                                          validationData: validationData, priority: 10));
                            }
                        }
                    }
                    else if (this.newFrozenOrderAction == FrozenOrderAction.AllowToPass)
                    {
                        // Follow messages are only meaningful if there's name to announce.
                        int delay = Utilities.random.Next(1, 4);
                        if (canReadDriverToFollow && Utilities.random.Next(1, 11) > 2)  // Randomly, announce message without name.
                        {
                            audioPlayer.playMessage(new QueuedMessage(!shouldFollowSafetyCar ? "frozen_order/allow_driver_to_pass" : "frozen_order/allow_safety_car_to_pass", delay + 6,
                                                                      secondsDelay: delay, messageFragments: MessageContents(folderAllow, usableDriverNameToFollow, folderToPass), abstractEvent: this,
                                                                      validationData: validationData, priority: 10));
                        }
                        else
                        {
                            audioPlayer.playMessage(new QueuedMessage(folderYoureAheadOfAGuyYouShouldBeFollowing, delay + 6, secondsDelay: delay, abstractEvent: this,
                                                                      validationData: validationData, priority: 10));
                        }
                    }
                    else if (this.newFrozenOrderAction == FrozenOrderAction.CatchUp)
                    {
                        int delay = Utilities.random.Next(1, 4);
                        if (canReadDriverToFollow && Utilities.random.Next(1, 11) > 2)  // Randomly, announce message without name.
                        {
                            audioPlayer.playMessage(new QueuedMessage(!shouldFollowSafetyCar ? "frozen_order/catch_up_to_driver" : "frozen_order/catch_up_to_safety_car", delay + 6,
                                                                      secondsDelay: delay, messageFragments: MessageContents(folderCatchUpTo, usableDriverNameToFollow), abstractEvent: this,
                                                                      validationData: validationData, priority: 10));
                        }
                        else
                        {
                            audioPlayer.playMessage(new QueuedMessage(folderYouNeedToCatchUpToTheGuyAhead, delay + 6, secondsDelay: delay, abstractEvent: this,
                                                                      validationData: validationData, priority: 10));
                        }
                    }
                    else if (this.newFrozenOrderAction == FrozenOrderAction.StayInPole &&
                             prevFrozenOrderAction != FrozenOrderAction.MoveToPole) // No point in nagging user to stay in pole if we previously told them to move there.
                    {
                        int delay = Utilities.random.Next(0, 3);
                        if (cfod.AssignedColumn == FrozenOrderColumn.None ||
                            Utilities.random.Next(1, 11) > 8)     // Randomly, announce message without coulmn info.
                        {
                            audioPlayer.playMessage(new QueuedMessage("frozen_order/stay_in_pole", delay + 6, secondsDelay: delay,
                                                                      messageFragments: MessageContents(folderStayInPole), abstractEvent: this, validationData: validationData, priority: 10));
                        }
                        else
                        {
                            string folderToPlay = null;
                            if (useOvalLogic)
                            {
                                folderToPlay = cfod.AssignedColumn == FrozenOrderColumn.Left ? folderStayInPoleInInsideColumn : folderStayInPoleInOutsideColumn;
                            }
                            else
                            {
                                folderToPlay = cfod.AssignedColumn == FrozenOrderColumn.Left ? folderStayInPoleInLeftColumn : folderStayInPoleInRightColumn;
                            }

                            audioPlayer.playMessage(new QueuedMessage("frozen_order/stay_in_pole_in_column", delay + 6, secondsDelay: delay,
                                                                      messageFragments: MessageContents(folderToPlay), abstractEvent: this, validationData: validationData, priority: 10));
                        }
                    }
                    else if (this.newFrozenOrderAction == FrozenOrderAction.MoveToPole)
                    {
                        int delay = Utilities.random.Next(2, 5);
                        if (cfod.AssignedColumn == FrozenOrderColumn.None)
                        {
                            audioPlayer.playMessage(new QueuedMessage("frozen_order/move_to_pole", delay + 6, secondsDelay: delay,
                                                                      messageFragments: MessageContents(folderMoveToPole), abstractEvent: this,
                                                                      validationData: validationData, priority: 10));
                        }
                        else
                        {
                            audioPlayer.playMessage(new QueuedMessage("frozen_order/move_to_pole_row", delay + 6, secondsDelay: delay,
                                                                      messageFragments: MessageContents(folderMoveToPoleRow), abstractEvent: this,
                                                                      validationData: validationData, priority: 10));
                        }
                    }
                }
            }
            else if (cfodp == FrozenOrderPhase.FullCourseYellow)
            {
                var prevDriverToFollow    = this.currDriverToFollow;
                var prevFrozenOrderColumn = this.currFrozenOrderColumn;

                var announceSCRLastFCYLapLane = useAmericanTerms &&
                                                currentGameState.StockCarRulesData.stockCarRulesEnabled &&
                                                (currentGameState.FlagData.fcyPhase == FullCourseYellowPhase.LAST_LAP_NEXT || currentGameState.FlagData.fcyPhase == FullCourseYellowPhase.LAST_LAP_CURRENT);

                if (isActionUpdateStable &&
                    (this.currFrozenOrderAction != this.newFrozenOrderAction ||
                     this.currDriverToFollow != this.newDriverToFollow ||
                     this.currFrozenOrderColumn != this.newFrozenOrderColumn ||
                     announceSCRLastFCYLapLane && !this.scrLastFCYLapLaneAnnounced))
                {
                    this.currFrozenOrderAction = this.newFrozenOrderAction;
                    this.currDriverToFollow    = this.newDriverToFollow;
                    this.currFrozenOrderColumn = this.newFrozenOrderColumn;

                    this.scrLastFCYLapLaneAnnounced = announceSCRLastFCYLapLane;

                    // canReadDriverToFollow will be true if we're behind the safety car or we can read the driver's name:
                    var canReadDriverToFollow = shouldFollowSafetyCar || AudioPlayer.canReadName(driverToFollow);

                    var usableDriverNameToFollow = shouldFollowSafetyCar ? driverToFollow : DriverNameHelper.getUsableDriverName(driverToFollow);

                    var validationData = new Dictionary <string, object>();
                    validationData.Add(FrozenOrderMonitor.validateMessageTypeKey, FrozenOrderMonitor.validateMessageTypeAction);
                    validationData.Add(FrozenOrderMonitor.validationActionKey, cfod.Action);
                    validationData.Add(FrozenOrderMonitor.validationAssignedPositionKey, cfod.AssignedPosition);
                    validationData.Add(FrozenOrderMonitor.validationDriverToFollowKey, cfod.DriverToFollowRaw);

                    if (this.newFrozenOrderAction == FrozenOrderAction.Follow &&
                        ((prevDriverToFollow != this.currDriverToFollow) ||  // Don't announce Follow messages for the driver that we caught up to or allowed to pass.
                         (prevFrozenOrderColumn != this.currFrozenOrderColumn && announceSCRLastFCYLapLane)))        // But announce for SCR last FCY lap.
                    {
                        int delay = Utilities.random.Next(0, 3);
                        if (canReadDriverToFollow)
                        {
                            if (announceSCRLastFCYLapLane && cfod.AssignedColumn == FrozenOrderColumn.Left)
                            {
                                audioPlayer.playMessage(new QueuedMessage(!shouldFollowSafetyCar ? "frozen_order/follow_driver_in_left" : "frozen_order/follow_safety_car_in_left",
                                                                          delay + 6, secondsDelay: delay, messageFragments: MessageContents(folderFollow, usableDriverNameToFollow,
                                                                                                                                            useOvalLogic ? folderInTheInsideColumn : folderInTheLeftColumn), abstractEvent: this, validationData: validationData, priority: 10));
                            }
                            else if (announceSCRLastFCYLapLane && cfod.AssignedColumn == FrozenOrderColumn.Right)
                            {
                                audioPlayer.playMessage(new QueuedMessage(!shouldFollowSafetyCar ? "frozen_order/follow_driver_in_right" : "frozen_order/follow_safety_car_in_right",
                                                                          delay + 6, secondsDelay: delay,
                                                                          messageFragments: MessageContents(folderFollow, usableDriverNameToFollow, useOvalLogic ? folderInTheOutsideColumn : folderInTheRightColumn),
                                                                          abstractEvent: this, validationData: validationData, priority: 10));
                            }
                            else
                            {
                                audioPlayer.playMessage(new QueuedMessage(!shouldFollowSafetyCar ? "frozen_order/follow_driver" : "frozen_order/follow_safety_car", delay + 6,
                                                                          secondsDelay: delay, messageFragments: MessageContents(folderFollow, usableDriverNameToFollow), abstractEvent: this, validationData: validationData, priority: 10));
                            }
                        }
                        else if (announceSCRLastFCYLapLane && cfod.AssignedColumn == FrozenOrderColumn.Left)
                        {
                            audioPlayer.playMessage(new QueuedMessage(useOvalLogic ? folderLineUpInInsideColumn : folderLineUpInLeftColumn, delay + 6, secondsDelay: delay,
                                                                      abstractEvent: this, validationData: validationData, priority: 10));
                        }
                        else if (announceSCRLastFCYLapLane && cfod.AssignedColumn == FrozenOrderColumn.Right)
                        {
                            audioPlayer.playMessage(new QueuedMessage(useOvalLogic ? folderLineUpInOutsideColumn : folderLineUpInRightColumn, delay + 6, secondsDelay: delay,
                                                                      abstractEvent: this, validationData: validationData, priority: 10));
                        }
                    }
                    else if (this.newFrozenOrderAction == FrozenOrderAction.AllowToPass)
                    {
                        int delay = Utilities.random.Next(1, 4);
                        if (canReadDriverToFollow && Utilities.random.Next(1, 11) > 2)  // Randomly, announce message without name.
                        {
                            audioPlayer.playMessage(new QueuedMessage(!shouldFollowSafetyCar ? "frozen_order/allow_driver_to_pass" : "frozen_order/allow_safety_car_to_pass",
                                                                      delay + 6, secondsDelay: delay, messageFragments: MessageContents(folderAllow, usableDriverNameToFollow, folderToPass),
                                                                      abstractEvent: this, validationData: validationData, priority: 10));
                        }
                        else
                        {
                            audioPlayer.playMessage(new QueuedMessage(folderYoureAheadOfAGuyYouShouldBeFollowing, delay + 6, secondsDelay: delay, abstractEvent: this,
                                                                      validationData: validationData, priority: 10));
                        }
                    }
                    else if (this.newFrozenOrderAction == FrozenOrderAction.CatchUp)
                    {
                        int delay = Utilities.random.Next(1, 4);
                        if (canReadDriverToFollow && Utilities.random.Next(1, 11) > 2)  // Randomly, announce message without name.
                        {
                            audioPlayer.playMessage(new QueuedMessage(!shouldFollowSafetyCar ? "frozen_order/catch_up_to_driver" : "frozen_order/catch_up_to_safety_car",
                                                                      delay + 6, secondsDelay: delay, messageFragments: MessageContents(folderCatchUpTo, usableDriverNameToFollow),
                                                                      abstractEvent: this, validationData: validationData, priority: 10));
                        }
                        else
                        {
                            audioPlayer.playMessage(new QueuedMessage(folderYouNeedToCatchUpToTheGuyAhead, delay + 6, secondsDelay: delay, abstractEvent: this,
                                                                      validationData: validationData, priority: 10));
                        }
                    }
                }
            }
            else if (cfodp == FrozenOrderPhase.FormationStanding)
            {
                string columnName = null;
                if (cfod.AssignedColumn != FrozenOrderColumn.None)
                {
                    if (useOvalLogic)
                    {
                        columnName = cfod.AssignedColumn == FrozenOrderColumn.Left ? folderInTheInsideColumn : folderInTheOutsideColumn;
                    }
                    else
                    {
                        columnName = cfod.AssignedColumn == FrozenOrderColumn.Left ? folderInTheLeftColumn : folderInTheRightColumn;
                    }
                }
                if (!this.formationStandingStartAnnounced && cgs.SessionData.SessionRunningTime > 10)
                {
                    this.formationStandingStartAnnounced = true;
                    var isStartingFromPole = cfod.AssignedPosition == 1;
                    int delay = Utilities.random.Next(0, 3);
                    if (isStartingFromPole)
                    {
                        if (columnName == null)
                        {
                            audioPlayer.playMessage(new QueuedMessage(folderWereStartingFromPole, 6, abstractEvent: this));
                        }
                        else
                        {
                            audioPlayer.playMessage(new QueuedMessage("frozen_order/youre_starting_from_pole_in_column", delay + 6, secondsDelay: delay,
                                                                      messageFragments: MessageContents(folderWereStartingFromPole, columnName), abstractEvent: this, priority: 10));
                        }
                    }
                    else
                    {
                        if (columnName == null)
                        {
                            audioPlayer.playMessage(new QueuedMessage("frozen_order/youre_starting_from_pos", delay + 6, secondsDelay: delay,
                                                                      messageFragments: MessageContents(folderWeStartingFromPosition, cfod.AssignedPosition), abstractEvent: this, priority: 10));
                        }
                        else
                        {
                            audioPlayer.playMessage(new QueuedMessage("frozen_order/youre_starting_from_pos_row_in_column", delay + 6, secondsDelay: delay,
                                                                      messageFragments: MessageContents(folderWeStartingFromPosition, cfod.AssignedPosition, folderRow, cfod.AssignedGridPosition, columnName),
                                                                      abstractEvent: this, priority: 10));
                        }
                    }
                }

                if (!this.formationStandingPreStartReminderAnnounced &&
                    cgs.SessionData.SectorNumber == 3 &&
                    cgs.PositionAndMotionData.DistanceRoundTrack > (cgs.SessionData.TrackDefinition.trackLength - FrozenOrderMonitor.DIST_TO_START_TO_ANNOUNCE_POS_REMINDER))
                {
                    this.formationStandingPreStartReminderAnnounced = true;
                    var isStartingFromPole = cfod.AssignedPosition == 1;
                    int delay = Utilities.random.Next(0, 3);
                    if (isStartingFromPole)
                    {
                        if (columnName == null)
                        {
                            audioPlayer.playMessage(new QueuedMessage("frozen_order/get_ready_starting_from_pole", delay + 6, secondsDelay: delay,
                                                                      messageFragments: MessageContents(LapCounter.folderGetReady, folderWereStartingFromPole), abstractEvent: this, priority: 10));
                        }
                        else
                        {
                            audioPlayer.playMessage(new QueuedMessage("frozen_order/get_ready_starting_from_pole_in_column", delay + 6, secondsDelay: delay,
                                                                      messageFragments: MessageContents(LapCounter.folderGetReady, folderWereStartingFromPole, columnName), abstractEvent: this, priority: 10));
                        }
                    }
                    else
                    {
                        if (columnName == null)
                        {
                            audioPlayer.playMessage(new QueuedMessage("frozen_order/get_ready_youre_starting_from_pos", delay + 6, secondsDelay: delay,
                                                                      messageFragments: MessageContents(LapCounter.folderGetReady, folderWeStartingFromPosition, cfod.AssignedPosition), abstractEvent: this, priority: 10));
                        }
                        else
                        {
                            audioPlayer.playMessage(new QueuedMessage("frozen_order/get_ready_youre_starting_from_pos_row_in_column", delay + 6, secondsDelay: delay,
                                                                      messageFragments: MessageContents(LapCounter.folderGetReady, folderWeStartingFromPosition, cfod.AssignedPosition, folderRow, cfod.AssignedGridPosition, columnName),
                                                                      abstractEvent: this, priority: 10));
                        }
                    }
                }
            }

            // Announce SC speed.
            if (pfod.SafetyCarSpeed == -1.0f && cfod.SafetyCarSpeed != -1.0f)
            {
                // TODO: may also need to announce basic "SC in" message.
                var kmPerHour        = cfod.SafetyCarSpeed * 3.6f;
                var messageFragments = new List <MessageFragment>();
                if (useAmericanTerms)
                {
                    messageFragments.Add(MessageFragment.Text(FrozenOrderMonitor.folderPaceCarSpeedIs));
                    var milesPerHour = kmPerHour * 0.621371f;
                    messageFragments.Add(MessageFragment.Integer((int)Math.Round(milesPerHour), false));
                    messageFragments.Add(MessageFragment.Text(FrozenOrderMonitor.folderMilesPerHour));
                }
                else
                {
                    messageFragments.Add(MessageFragment.Text(FrozenOrderMonitor.folderSafetyCarSpeedIs));
                    messageFragments.Add(MessageFragment.Integer((int)Math.Round(kmPerHour), false));
                    messageFragments.Add(MessageFragment.Text(FrozenOrderMonitor.folderKilometresPerHour));
                }
                int delay = Utilities.random.Next(10, 16);
                audioPlayer.playMessage(new QueuedMessage("frozen_order/pace_car_speed", delay + 6, secondsDelay: delay, messageFragments: messageFragments, abstractEvent: this, priority: 10));
            }

            // Announce SC left.
            if (pfod.SafetyCarSpeed != -1.0f && cfod.SafetyCarSpeed == -1.0f)
            {
                if (useAmericanTerms)
                {
                    audioPlayer.playMessage(new QueuedMessage(folderPaceCarJustLeft, 10, abstractEvent: this));
                }
                else
                {
                    audioPlayer.playMessage(new QueuedMessage(folderSafetyCarJustLeft, 10, abstractEvent: this));
                }
            }

            // For fast rolling, do nothing for now.
        }