/// <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; }); }
/// <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 }); }
/// <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()); }
/// <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; } }); } }
/// <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()); }
/// <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); }); }
/// <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; } }
/// <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(); }); }
/// <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 }
/// <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)); }
/// <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); }
/// <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(); }