Пример #1
0
        /// <summary>
        /// Handles the entity selection event represented by the specified <see
        /// cref="Instruction"/>.</summary>
        /// <param name="instruction">
        /// The <see cref="SelectEntityInstruction"/> that represents the entity selection event to
        /// handle.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="instruction"/> is a null reference.</exception>
        /// <remarks><para>
        /// <b>SelectEntityEvent</b> selects the <see cref="Entity"/> indicated by the specified
        /// <paramref name="instruction"/> in the default <see cref="Session.MapView"/> and in the
        /// data view.
        /// </para><para>
        /// <b>SelectEntityEvent</b> does nothing if the specified <paramref name="instruction"/>
        /// indicates no <see cref="Entity"/>, or one that is unplaced.</para></remarks>

        public static void SelectEntityEvent(SelectEntityInstruction instruction)
        {
            if (instruction == null)
            {
                ThrowHelper.ThrowArgumentNullException("instruction");
            }

            // get entity identifier, if any
            string id = instruction.Id;

            if (String.IsNullOrEmpty(id))
            {
                return;
            }

            // get placed entity, if any
            Entity entity = Session.Instance.WorldState.Entities[id];

            if (entity == null || entity.Site == null)
            {
                return;
            }

            // select site and entity
            AsyncAction.Invoke(delegate {
                Session.MapView.SelectedSite       = entity.Site.Location;
                MainWindow.Instance.SelectedEntity = entity.Id;
            });
        }
Пример #2
0
        /// <summary>
        /// Stops a replay in progress.</summary>
        /// <remarks><para>
        /// <b>Stop</b> halts the interactive command replay, restores the original <see
        /// cref="Session.State"/> and <see cref="Session.WorldState"/> properties of the current
        /// <see cref="Session"/>, and clears all replay data.
        /// </para><para>
        /// <b>Stop</b> does nothing if the current session <b>State</b> does not equal <see
        /// cref="SessionState.Replay"/>.</para></remarks>

        private void Stop()
        {
            AsyncAction.Invoke(delegate {
                // do nothing if already stopped
                if (Session.State != SessionState.Replay)
                {
                    return;
                }

                // enter Stop state
                CurrentState = ReplayState.Stop;

                // restore original world state
                Session.Instance.WorldState = this._originalWorldState;
                Session.MapView.WorldState  = Session.Instance.WorldState;

                // restore original site selection, if any
                if (this._originalSelected != Site.InvalidLocation)
                {
                    Session.MapView.CenterAndSelect(this._originalSelected);
                }

                // restore original session state
                Session.State = this._originalState;
                MainWindow.Instance.StatusMessage.Pop();

                // clear last replay event message
                MainWindow.Instance.EventMessage.Clear();

                Clear(false); // clear all replay data
            });
        }
Пример #3
0
        /// <summary>
        /// Shows a <see cref="Site"/> associated with the specified <see cref="Faction"/>.
        /// </summary>
        /// <param name="faction">
        /// The <see cref="Faction"/> for which to show a <see cref="Site"/>.</param>
        /// <remarks>
        /// <b>ShowFaction</b> updates the "Turn/Faction" display, and may also update the default
        /// <see cref="Session.MapView"/> to show the <see cref="Site"/> returned by <see
        /// cref="Finder.FindFactionSite"/> for the specified <paramref name="faction"/>, depending
        /// on the settings of the current <see cref="ApplicationOptions"/> instance.</remarks>

        private void ShowFaction(Faction faction)
        {
            // find site associated with faction
            PointI factionSite = Finder.FindFactionSite(null, faction);

            // scroll site into view if desired
            if (ApplicationOptions.Instance.Game.Replay.Scroll)
            {
                AsyncAction.Invoke(() => Session.MapView.ScrollIntoView(factionSite));
            }

            // highlight site in any case
            ShowSite(factionSite);

            // update Turn/Faction display
            AsyncAction.Invoke(() => MainWindow.Instance.UpdateTurnFaction());
        }
Пример #4
0
        /// <summary>
        /// Enqueues the specified command and executes all <see
        /// cref="CommandExecutor.QueuedCommands"/>.</summary>
        /// <param name="worldState">
        /// The <see cref="WorldState"/> on which all <see cref="CommandExecutor.QueuedCommands"/>
        /// are executed.</param>
        /// <param name="command">
        /// The <see cref="Command"/> to enqueue.</param>
        /// <returns>
        /// <c>true</c> if all <see cref="CommandExecutor.QueuedCommands"/> were successfully
        /// executed; otherwise, <c>false</c>.</returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="worldState"/> or <paramref name="command"/> is a null reference.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// <b>ProcessCommand</b> was called on the <see cref="DispatcherObject.Dispatcher"/> thread
        /// of the current <see cref="Application"/> instance, rather than on a background thread.
        /// </exception>
        /// <remarks><para>
        /// <b>ProcessCommand</b> updates the <see cref="StatusBar"/> message and returns the result
        /// of the base class implementation of <see cref="CommandExecutor.ProcessCommand"/>, which
        /// is always <c>true</c>. If that call throws an <see cref="InvalidCommandException"/>,
        /// <b>ProcessCommand</b> displays its text and immediately returns <c>false</c>.
        /// </para><para>
        /// If the current session <see cref="Session.State"/> equals <see
        /// cref="SessionState.Human"/> or <see cref="SessionState.Selection"/>, it is changed to
        /// <see cref="SessionState.Command"/> while <b>ProcessCommand</b> executes, and then reset
        /// to <see cref="SessionState.Human"/>.</para></remarks>

        protected override bool ProcessCommand(WorldState worldState, Command command)
        {
            if (Application.Current.Dispatcher.CheckAccess())
            {
                ThrowHelper.ThrowInvalidOperationException(Tektosyne.Strings.ThreadForeground);
            }

            AsyncAction.Invoke(delegate {
                // switch human player to Command state
                if (Session.State == SessionState.Human || Session.State == SessionState.Selection)
                {
                    Session.State = SessionState.Command;
                }

                // show command execution message
                MainWindow.Instance.StatusMessage.Push(Global.Strings.StatusCommandExecuting);
            });

            try {
                return(base.ProcessCommand(worldState, command));
            }
            catch (InvalidCommandException e) {
                AsyncAction.Invoke(delegate {
                    Mouse.OverrideCursor = null;

                    // notify user of command error
                    MessageDialog.Show(MainWindow.Instance,
                                       Global.Strings.DialogCommandInvalid, Global.Strings.TitleCommandInvalid,
                                       e, MessageBoxButton.OK, Images.Error);
                });

                return(false);
            }
            finally {
                AsyncAction.Invoke(delegate {
                    MainWindow.Instance.StatusMessage.Pop();

                    // revert human player to default state
                    if (Session.State == SessionState.Command)
                    {
                        Session.State = SessionState.Human;
                    }
                });
            }
        }
Пример #5
0
        /// <summary>
        /// Selects the specified <see cref="Site"/>.</summary>
        /// <param name="site">
        /// The coordinates of the <see cref="Site"/> to select.</param>
        /// <remarks>
        /// <b>ShowSite</b> selects the specified <paramref name="site"/> in the default <see
        /// cref="Session.MapView"/> if the coordinates are valid, and otherwise updates the
        /// "Selected Site" display to reflect possible changes in the contents of the current <see
        /// cref="MapView.SelectedSite"/>.</remarks>

        private void ShowSite(PointI site)
        {
            // highlight valid site for a while
            if (Finder.MapGrid.Contains(site))
            {
                AsyncAction.Invoke(() => Session.MapView.SelectedSite = site);
                this._commandSkip = true;
                return;
            }

            /*
             * Since we did not set SelectedSite we must refresh the
             * Selection panel in case its SelectedSite was affected
             * by whatever event caused the call to ShowSite.
             */

            AsyncAction.Invoke(() => MainWindow.Instance.UpdateSelection());
        }
Пример #6
0
        /// <summary>
        /// Shows the data of the specified <see cref="Command"/>.</summary>
        /// <param name="command">
        /// The <see cref="Command"/> whose data to show.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="command"/> is a null reference.</exception>
        /// <exception cref="PropertyValueException">
        /// The current session <see cref="Session.State"/> is <see cref="SessionState.Invalid"/>.
        /// </exception>
        /// <remarks>
        /// <b>ShowCommand</b> invokes <see cref="Object.ToString"/> on the specified <paramref
        /// name="command"/> and replaces the contents of the event view with the resulting string.
        /// </remarks>

        public static void ShowCommand(Command command)
        {
            if (command == null)
            {
                ThrowHelper.ThrowArgumentNullException("command");
            }

            if (Session.State == SessionState.Invalid)
            {
                ThrowHelper.ThrowPropertyValueExceptionWithFormat("Session.State",
                                                                  Session.State, Tektosyne.Strings.PropertyIsValue, SessionState.Invalid);
            }

            AsyncAction.Invoke(delegate {
                // replace message text with command text plus line break
                MainWindow.Instance.EventMessage.Text = command.ToString();
                MainWindow.Instance.EventMessage.AppendText(Environment.NewLine);
            });
        }
Пример #7
0
        /// <summary>
        /// Handles the command event represented by the specified <see cref="Instruction"/>.
        /// </summary>
        /// <param name="instruction">
        /// The <see cref="Instruction"/> that represents the command event to handle.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="instruction"/> is a null reference.</exception>
        /// <exception cref="PropertyValueException">
        /// The current session <see cref="Session.State"/> is <see cref="SessionState.Invalid"/>.
        /// </exception>
        /// <remarks><para>
        /// <b>ShowCommandEvent</b> takes the following actions, depending on the exact type of the
        /// specified <paramref name="instruction"/>:
        /// </para><list type="table"><listheader>
        /// <term>Type</term><description>Action</description>
        /// </listheader><item>
        /// <term><see cref="SelectEntityInstruction"/></term>
        /// <description>Call <see cref="SelectEntityEvent"/>.</description>
        /// </item><item>
        /// <term><see cref="ImageInstruction"/></term>
        /// <description>Call <see cref="ShowImageEvent"/>.</description>
        /// </item><item>
        /// <term><see cref="MessageInstruction"/></term>
        /// <description>Call <see cref="ShowMessageEvent"/>.</description>
        /// </item><item>
        /// <term>Other</term><description>Do nothing.</description>
        /// </item></list></remarks>

        public static void ShowCommandEvent(Instruction instruction)
        {
            if (instruction == null)
            {
                ThrowHelper.ThrowArgumentNullException("instruction");
            }

            if (Session.State == SessionState.Invalid)
            {
                ThrowHelper.ThrowPropertyValueExceptionWithFormat("Session.State",
                                                                  Session.State, Tektosyne.Strings.PropertyIsValue, SessionState.Invalid);
            }

            MessageInstruction message = instruction as MessageInstruction;

            if (message != null)
            {
                AsyncAction.Invoke(() =>
                                   ShowMessageEvent(message, MainWindow.Instance.EventMessage, true));
                return;
            }

            SelectEntityInstruction selectEntity = instruction as SelectEntityInstruction;

            if (selectEntity != null)
            {
                SelectEntityEvent(selectEntity);
                return;
            }

            ImageInstruction image = instruction as ImageInstruction;

            if (image != null)
            {
                ShowImageEvent(image);
                return;
            }
        }
Пример #8
0
        /// <summary>
        /// Replays commands starting at the specified full turn and active faction indices.
        /// </summary>
        /// <param name="turn">
        /// The index of the full turn at which to start interactive replay.</param>
        /// <param name="faction">
        /// The index of the faction whose activation during the specified <paramref name="turn"/>
        /// should start interactive replay.</param>
        /// <exception cref="ArgumentOutOfRangeException"><para>
        /// <paramref name="turn"/> is greater than the <see cref="WorldState.CurrentTurn"/> of the
        /// current <see cref="Session.WorldState"/>.
        /// </para><para>-or-</para><para>
        /// <paramref name="faction"/> is less than zero.</para></exception>
        /// <exception cref="PropertyValueException">
        /// The current session <see cref="Session.State"/> is neither <see
        /// cref="SessionState.Closed"/>, <see cref="SessionState.Computer"/>, nor <see
        /// cref="SessionState.Human"/>.</exception>
        /// <remarks><para>
        /// <b>Start</b> sets the session <see cref="Session.State"/> to <see
        /// cref="SessionState.Replay"/> and <see cref="CurrentState"/> to <see
        /// cref="ReplayState.Play"/>, skips ahead to the specified <paramref name="turn"/> and
        /// <paramref name="faction"/> indices, and then begins an interactive replay of all
        /// remaining commands stored in the session's <see cref="WorldState.History"/>.
        /// </para><para>
        /// <b>Start</b> shows an informational message and returns immediately if there are no
        /// commands to replay.
        /// </para><para>
        /// If the specified <paramref name="turn"/> is negative, <b>Start</b> shows a <see
        /// cref="Dialog.ChangeTurn"/> dialog, allowing the user to enter the turn at which to start
        /// interactive replay.
        /// </para><para>
        /// If the specified <paramref name="faction"/> is greater than the number of surviving
        /// factions at any point while skipping ahead, interactive replay will begin with the first
        /// active faction during the specified <paramref name="turn"/>.</para></remarks>

        public void Start(int turn, int faction)
        {
            CheckStartState();
            Session    session = Session.Instance;
            WorldState world   = session.WorldState;

            if (turn > world.CurrentTurn)
            {
                ThrowHelper.ThrowArgumentOutOfRangeExceptionWithFormat(
                    "turn", turn, Tektosyne.Strings.ArgumentGreaterValue, "CurrentTurn");
            }

            if (faction < 0)
            {
                ThrowHelper.ThrowArgumentOutOfRangeException(
                    "faction", faction, Tektosyne.Strings.ArgumentNegative);
            }

            // show message and quit if no commands to replay
            if (world.History.Commands.Count == 0)
            {
                MessageBox.Show(MainWindow.Instance,
                                Global.Strings.DialogReplayNone, Global.Strings.TitleReplay,
                                MessageBoxButton.OK, MessageBoxImage.Information);
                return;
            }

            if (turn < 0)
            {
                // ask user to specify starting turn
                var dialog = new Dialog.ChangeTurn(
                    world.CurrentTurn, Global.Strings.TitleReplayFromTurn);
                dialog.Owner = MainWindow.Instance;
                if (dialog.ShowDialog() != true)
                {
                    return;
                }

                // get turn entered by user
                turn = dialog.Turn;
                Debug.Assert(turn >= 0);
                Debug.Assert(turn <= world.CurrentTurn);
            }

            // save current session data
            this._originalWorldState = session.WorldState;
            this._originalSelected   = Session.MapView.SelectedSite;
            this._originalState      = Session.State;

            // switch session to Replay state
            Session.State = SessionState.Replay;

            // show replay control message...
            MainWindow.Instance.StatusMessage.Push(Global.Strings.StatusReplay);

            // ...but wait for new world state
            MainWindow.Instance.BeginWait(Global.Strings.StatusReplayCommands);
            MainWindow.Instance.StatusMessage.Push();

            TaskEvents events = new TaskEvents(Application.Current.Dispatcher);

            events.TaskMessage += ((sender, args) =>
                                   MainWindow.Instance.StatusMessage.Text = args.Value);

            try {
                // create world state from scenario
                WorldState worldState = new WorldState();
                worldState.Initialize(events);

                // copy original turn count
                worldState.History.CopyFullTurns(this._originalWorldState.History);
                session.WorldState = worldState;
            }
            finally {
                MainWindow.Instance.StatusMessage.Pop();
                MainWindow.Instance.EndWait();
            }

            AsyncAction.Run(delegate {
                // skip to specified turn & faction
                this._commandIndex = 0;
                if (turn > 0 || faction > 0)
                {
                    Skip(turn, faction);
                }

                // set default map view to new world state
                AsyncAction.Invoke(() => Session.MapView.WorldState = session.WorldState);

                // show active faction's home if not skipped
                if (this._commandIndex == 0)
                {
                    ShowFaction(session.WorldState.ActiveFaction);
                }

                // enter Play state
                CurrentState = ReplayState.Play;
                PlayCommands();
            });
        }
Пример #9
0
        /// <summary>
        /// Skips ahead to the specified full turn and active faction indices.</summary>
        /// <param name="turn">
        /// The index of the full turn at which to resume interactive replay.</param>
        /// <param name="faction">
        /// The index of the faction whose activation during the specified <paramref name="turn"/>
        /// should resume interactive replay.</param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="faction"/> is less than zero.</exception>
        /// <exception cref="PropertyValueException">
        /// The current session <see cref="Session.State"/> is not <see
        /// cref="SessionState.Replay"/>.</exception>
        /// <remarks><para>
        /// <b>Skip</b> performs one of the following actions, depending on the specified <paramref
        /// name="turn"/> index:
        /// </para><list type="table"><listheader>
        /// <term><paramref name="turn"/></term><description>Action</description>
        /// </listheader><item>
        /// <term>Less than zero</term>
        /// <description>Silently execute history commands until the next <see
        /// cref="EndTurnCommand"/> was executed.</description>
        /// </item><item>
        /// <term>Less than or equal to the index of the currently replayed turn</term>
        /// <description>Do nothing.</description>
        /// </item><item>
        /// <term>Less than the maximum turn index in the current game</term>
        /// <description>Silently execute history commands until the index of the currently replayed
        /// turn equals <paramref name="turn"/>.</description>
        /// </item><item>
        /// <term>Greater than the maximum turn index in the current game</term>
        /// <description>Call <see cref="Stop"/>.</description>
        /// </item></list><para>
        /// If the specified <paramref name="faction"/> index is less than the number of surviving
        /// <see cref="WorldState.Factions"/> when interactive replay would normally resume,
        /// <b>Skip</b> continues to silently execute history commands until the faction with the
        /// specified index has been activated. Otherwise, the <paramref name="faction"/> parameter
        /// is ignored.
        /// </para><para>
        /// <b>Skip</b> also sets the <see cref="CurrentState"/> to <see cref="ReplayState.Skip"/>
        /// during execution, then back to <see cref="ReplayState.Play"/> when finished. <b>Skip</b>
        /// calls <see cref="Stop"/> instead if an error occurred, or if the command history is
        /// already exhausted.</para></remarks>

        private void Skip(int turn, int faction)
        {
            if (Session.State != SessionState.Replay)
            {
                ThrowHelper.ThrowPropertyValueExceptionWithFormat("Session.State",
                                                                  Session.State, Tektosyne.Strings.PropertyNotValue, SessionState.Replay);
            }

            if (faction < 0)
            {
                ThrowHelper.ThrowArgumentOutOfRangeException(
                    "faction", faction, Tektosyne.Strings.ArgumentNegative);
            }

            WorldState world = Session.Instance.WorldState;

            // reset faction to zero if already dead
            if (faction >= world.Factions.Count)
            {
                faction = 0;
            }

            // do nothing if specified turn & faction reached
            if ((turn >= 0 && turn < world.CurrentTurn) ||
                (turn == world.CurrentTurn && world.ActiveFactionIndex >= faction))
            {
                return;
            }

            // stop immediately if unreachable turn specified
            if (turn > this._originalWorldState.CurrentTurn)
            {
                Stop();
                return;
            }

            // enter Skip state
            CurrentState = ReplayState.Skip;

            // clear pending command, if any
            this._command = null;

            // suspend interactive replay
            AsyncAction.Invoke(delegate {
                MainWindow.Instance.BeginWait(Global.Strings.StatusReplayCommands);
                MainWindow.Instance.StatusMessage.Push();
            });

            TaskEvents events = new TaskEvents(Application.Current.Dispatcher);

            events.TaskMessage += ((sender, args) =>
                                   MainWindow.Instance.StatusMessage.Text = args.Value);

            try {
                // skip forward to specified turn & faction
                bool resume = SilentReplay(turn, faction, events);

                if (resume)
                {
                    // show active faction's home site
                    ShowFaction(world.ActiveFaction);

                    // update map view if replay visible
                    if (world == Session.MapView.WorldState)
                    {
                        AsyncAction.Invoke(Session.MapView.Redraw);
                    }

                    // enter Play state
                    CurrentState = ReplayState.Play;
                    return;
                }
            }
            finally {
                AsyncAction.Invoke(delegate {
                    MainWindow.Instance.StatusMessage.Pop();
                    MainWindow.Instance.EndWait();
                });
            }

            Stop(); // end of history or error
        }
Пример #10
0
        /// <summary>
        /// Shows an error message for the specified <see cref="InvalidCommandException"/>.
        /// </summary>
        /// <param name="exception">
        /// An <see cref="InvalidCommandException"/> that occurred during command replay.</param>
        /// <remarks>
        /// <b>ShowError</b> shows a <see cref="MessageDialog"/> with the specified <paramref
        /// name="exception"/> and a note that replay has been stopped.</remarks>

        private static void ShowCommandError(InvalidCommandException exception)
        {
            AsyncAction.Invoke(() => MessageDialog.Show(MainWindow.Instance,
                                                        Global.Strings.DialogReplayError, Global.Strings.TitleReplayError,
                                                        exception, MessageBoxButton.OK, Images.Error));
        }
Пример #11
0
        /// <summary>
        /// Replay the next history <see cref="Command"/>.</summary>
        /// <returns>
        /// <c>true</c> if interactive replay should continue; <c>false</c> if <see cref="Stop"/>
        /// should be called.</returns>
        /// <exception cref="InvalidCommandException">
        /// The current <see cref="Command"/> contains invalid data.</exception>
        /// <remarks><para>
        /// <b>NextCommand</b> fetches the next history command, validates and executes it, and
        /// increments the command counter as necessary. <b>NextCommand</b> returns <c>false</c> if
        /// the command history has been exhausted.
        /// </para><para>
        /// The <see cref="Command.Source"/> and <see cref="Command.Target"/> sites of each history
        /// command are scrolled into view on the default <see cref="Session.MapView"/> if the <see
        /// cref="Options.ReplayOptions.Scroll"/> property of the current <see
        /// cref="ApplicationOptions"/> instance is <c>true</c>.</para></remarks>

        private bool NextCommand()
        {
            Debug.Assert(this._originalWorldState != null);
            Debug.Assert(this._commandIndex >= 0);
            Debug.Assert(!this._commandSkip);

            // retrieve original command history
            IList <Command> commands = this._originalWorldState.History.Commands;

            // stop replay if history exhausted
            if (this._commandIndex >= commands.Count)
            {
                return(false);
            }

            MapView mapView = Session.MapView;

            // retrieve next command in history
            if (this._command == null)
            {
                this._command = commands[this._commandIndex];

                // validate & show command
                this._command.Validate(Session.Instance.WorldState);
                SessionExecutor.ShowCommand(this._command);

                // scroll sites into view if desired
                PointI source = this._command.Source.Location;
                if (ApplicationOptions.Instance.Game.Replay.Scroll)
                {
                    PointI target = this._command.Target.Location;
                    AsyncAction.Invoke(() => mapView.ScrollIntoView(source, target));
                }

                // highlight valid source site
                if (Finder.MapGrid.Contains(source))
                {
                    AsyncAction.Invoke(() => mapView.SelectedSite = source);

                    // select first entity if specified
                    EntityReference[] entities = this._command.Entities;
                    if (entities != null && entities.Length > 0)
                    {
                        AsyncAction.Invoke(() => MainWindow.Instance.SelectedEntity = entities[0].Id);
                    }

                    return(true); // show source for a while
                }
            }

            // attempt to execute command
            this._command.Execute(new ExecutionContext(
                                      Session.Instance.WorldState, null, SessionExecutor.ShowCommandEvent));

            // check if command cleared by reentrant call
            if (this._command != null)
            {
                // add some delay after commands with message events
                this._commandSkip = this._command.Program.Exists(x => x is MessageInstruction);

                // highlight active faction for Begin/EndTurn, otherwise command target
                if (this._command is BeginTurnCommand || this._command is EndTurnCommand)
                {
                    ShowFaction(this._command.Faction.Value);
                }
                else
                {
                    ShowSite(this._command.Target.Location);
                }

                // ensure command effects are shown
                AsyncAction.Invoke(mapView.Redraw);
            }

            // prepare for next command
            this._command = null;
            ++this._commandIndex;

            return(true);
        }
Пример #12
0
        /// <summary>
        /// Executes the specified command and adds it to the command history.</summary>
        /// <param name="worldState">
        /// The <see cref="WorldState"/> on which the specified <paramref name="command"/> is
        /// executed.</param>
        /// <param name="command">
        /// The <see cref="Command"/> to execute.</param>
        /// <param name="queued">
        /// <c>true</c> if <paramref name="command"/> was enqueued by the <see
        /// cref="CommandExecutor.QueueCommand"/> method; <c>false</c> if <paramref name="command"/>
        /// was directly supplied to the <see cref="ProcessCommand"/> method.</param>
        /// <exception cref="ArgumentException">
        /// <paramref name="worldState"/> does not equal the <see cref="Session.WorldState"/> of the
        /// current <see cref="Session"/>.</exception>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="worldState"/> or <paramref name="command"/> is a null reference.
        /// </exception>
        /// <exception cref="InvalidCommandException">
        /// The specified <paramref name="command"/> contains data that is invalid with respect to
        /// the specified <paramref name="worldState"/>.</exception>
        /// <remarks><para>
        /// <b>ExecuteCommand</b> attempts to execute the specified <paramref name="command"/> by
        /// calling the base class implementation of <see cref="CommandExecutor.ExecuteCommand"/>.
        /// </para><para>
        /// On success, <b>ExecuteCommand</b> shows any events that were generated and sets the <see
        /// cref="Session.WorldChanged"/> flag of the current <see cref="Session"/>.
        /// </para><para>
        /// If <paramref name="queued"/> is <c>true</c>, <b>ExecuteCommand</b> inserts additional
        /// delays before and during command execution, and scrolls the default <see
        /// cref="Session.MapView"/> to bring the affected sites into view. These additional actions
        /// are skipped if the <see cref="AbortSignal"/> is set, however.</para></remarks>

        protected override void ExecuteCommand(
            WorldState worldState, Command command, bool queued)
        {
            if (worldState == null)
            {
                ThrowHelper.ThrowArgumentNullException("worldState");
            }
            if (command == null)
            {
                ThrowHelper.ThrowArgumentNullException("command");
            }

            if (worldState != Session.Instance.WorldState)
            {
                ThrowHelper.ThrowArgumentExceptionWithFormat("worldState",
                                                             Tektosyne.Strings.ArgumentNotEquals, "Session.Instance.WorldState");
            }

            // get current delay for interactive replay
            int delay = ApplicationOptions.Instance.Game.Replay.Delay;

            // add delay between queued commands
            if (queued)
            {
                AbortSignal.WaitOne(2 * delay, false);
            }

            // validate & show command
            command.Validate(worldState);
            ShowCommand(command);

            PointI  source  = command.Source.Location;
            PointI  target  = command.Target.Location;
            MapView mapView = Session.MapView;

            if (queued && !AbortSignal.WaitOne(0, false))
            {
                bool sourceValid = Finder.MapGrid.Contains(source);

                AsyncAction.Invoke(delegate {
                    // scroll command sites into view
                    mapView.ScrollIntoView(source, target);

                    // highlight source site if valid
                    if (sourceValid)
                    {
                        mapView.SelectedSite = source;

                        // select first entity if specified
                        EntityReference[] entities = command.Entities;
                        if (entities != null && entities.Length > 0)
                        {
                            MainWindow.Instance.SelectedEntity = entities[0].Id;
                        }
                    }
                });

                // show valid source site for a while
                if (sourceValid)
                {
                    AbortSignal.WaitOne(delay, false);
                }
            }

            // execute command and add to history
            command.Execute(new ExecutionContext(worldState, QueueCommand, ShowCommandEvent));
            worldState.History.AddCommand(command, worldState.CurrentTurn);

            AsyncAction.Invoke(delegate {
                // move to target if valid, else update source
                if (Finder.MapGrid.Contains(target))
                {
                    mapView.SelectedSite = target;
                }
                else
                {
                    MainWindow.Instance.UpdateSelection();
                }

                // ensure command effects are shown
                mapView.Redraw();
            });

            // world state has changed
            Session.Instance.SetWorldChanged();
        }