Press "Enter" to skip to content

Lesson 18.5: Update game loading and saving

Now we can update the save and load game functions.

I originally added versioning, so we can automatically upgrade save game files as we release new versions of the game, with different saved game file formats. But, if we keep doing that while making many significant changes, we’re going to add “update save and load functions” to many lessons.

So, for now, I’m going to remove the versioning. Once the game is more stable, we’ll add it back in.

Step 1: Modify Engine\Services\SaveGameService.cs Start by adding “using System.Collections.Generic” as a new using directive. Uncomment the LoadLastSaveOrCreatenew function and replace the “return new GameSession();”on lines 24 and 43 with throwing exceptions. We don’t want to allow starting the game with a default Player object.

Uncomment the code for the CreatePlayer function and delete the lines that use the file version. On line 54 replace the code that used to set the Dexterity with a call to GetPlayerAttributes – a new function we’ll create. On the final line of this function, replace the “return null;” with “return player;”.

On lines 66-82, add the new GetPlayerAttributes function to read the PlayerAttribute values from the saved game file and create PlayerAttribute objects.

In the PopulatePlayerInventory, PopulatePlayerQuests, and PopulatePlayerRecipes functions, remove the code that works with file versions.

Finally, delete the FileVersion function at the end of the class.

SaveGameService.cs

using System;
using System.Collections.Generic;
using System.IO;
using Engine.Factories;
using Engine.Models;
using Engine.ViewModels;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Engine.Services
{
    public static class SaveGameService
    {
        public static void Save(GameSession gameSession, string fileName)
        {
            File.WriteAllText(fileName,
                              JsonConvert.SerializeObject(gameSession, Formatting.Indented));
        }
        public static GameSession LoadLastSaveOrCreateNew(string fileName)
        {
            if (!File.Exists(fileName))
            {
                throw new FileNotFoundException($"Filename: {fileName}");
            }
            // Save game file exists, so create the GameSession object from it.
            try
            {
                JObject data = JObject.Parse(File.ReadAllText(fileName));
                // Populate Player object
                Player player = CreatePlayer(data);
                int x = (int)data[nameof(GameSession.CurrentLocation)][nameof(Location.XCoordinate)];
                int y = (int)data[nameof(GameSession.CurrentLocation)][nameof(Location.YCoordinate)];
                // Create GameSession object with saved game data
                return new GameSession(player, x, y);
            }
            catch
            {
                throw new FormatException($"Error reading: {fileName}");
            }
        }
        private static Player CreatePlayer(JObject data)
        {
            Player player =
                new Player((string)data[nameof(GameSession.CurrentPlayer)][nameof(Player.Name)],
                           (int)data[nameof(GameSession.CurrentPlayer)][nameof(Player.ExperiencePoints)],
                           (int)data[nameof(GameSession.CurrentPlayer)][nameof(Player.MaximumHitPoints)],
                           (int)data[nameof(GameSession.CurrentPlayer)][nameof(Player.CurrentHitPoints)],
                           GetPlayerAttributes(data),
                           (int)data[nameof(GameSession.CurrentPlayer)][nameof(Player.Gold)]);
            PopulatePlayerInventory(data, player);
            PopulatePlayerQuests(data, player);
            PopulatePlayerRecipes(data, player);
            return player;
        }
        private static IEnumerable<PlayerAttribute> GetPlayerAttributes(JObject data)
        {
            List<PlayerAttribute> attributes =
                new List<PlayerAttribute>();
            foreach(JToken itemToken in (JArray)data[nameof(GameSession.CurrentPlayer)]
                [nameof(Player.Attributes)])
            {
                attributes.Add(new PlayerAttribute(
                                   (string)itemToken[nameof(PlayerAttribute.Key)],
                                   (string)itemToken[nameof(PlayerAttribute.DisplayName)],
                                   (string)itemToken[nameof(PlayerAttribute.DiceNotation)],
                                   (int)itemToken[nameof(PlayerAttribute.BaseValue)],
                                   (int)itemToken[nameof(PlayerAttribute.ModifiedValue)]));
            }
            return attributes;
        }
        
        private static void PopulatePlayerInventory(JObject data, Player player)
        {
            foreach(JToken itemToken in (JArray)data[nameof(GameSession.CurrentPlayer)]
                [nameof(Player.Inventory)]
                [nameof(Inventory.Items)])
            {
                int itemId = (int)itemToken[nameof(GameItem.ItemTypeID)];
                player.AddItemToInventory(ItemFactory.CreateGameItem(itemId));
            }
        }
        private static void PopulatePlayerQuests(JObject data, Player player)
        {
            foreach(JToken questToken in (JArray)data[nameof(GameSession.CurrentPlayer)]
                [nameof(Player.Quests)])
            {
                int questId =
                    (int)questToken[nameof(QuestStatus.PlayerQuest)][nameof(QuestStatus.PlayerQuest.ID)];
                Quest quest = QuestFactory.GetQuestByID(questId);
                QuestStatus questStatus = new QuestStatus(quest);
                questStatus.IsCompleted = (bool)questToken[nameof(QuestStatus.IsCompleted)];
                player.Quests.Add(questStatus);
            }
        }
        private static void PopulatePlayerRecipes(JObject data, Player player)
        {
            foreach(JToken recipeToken in
                (JArray)data[nameof(GameSession.CurrentPlayer)][nameof(Player.Recipes)])
            {
                int recipeId = (int)recipeToken[nameof(Recipe.ID)];
                Recipe recipe = RecipeFactory.RecipeByID(recipeId);
                player.Recipes.Add(recipe);
            }
        }
    }

Step 2: Modify WPFUI\MainWindow.xaml and MainWindow.xaml.cs In MainWindow.xaml.cs, I added two optional parameters to the constructor for the starting location coordinates.

For new games, we won’t have coordinates and will start the player at location 0, 0. For saved games, we will have values to pass in.

In StartNewGame_OnClick (line 140), we’ll display a Startup window to let the user either create a new player or load a game from a saved game file.

Since we’re going to delete the “Load Game” menu option, and handle that through the “Start new game” menu option displaying the Startup window, we can delete the LoadGame_OnClick function that started on line 145.

In MainWindow_OnClosing, I moved its code to a separate AskToSaveGame function. In the future, I’d like to expand that a bit to check if the player did anything (moved location, changed inventory, learned a new recipe, etc.). If the player hasn’t done anything, we won’t ask to save the game – since nothing has changed.

Finally, I uncommented the SaveGameService.Save on line 186, so the play can save the game.

In MainWindow.xaml, I removed the IsEnabled=False lines from the start, load, and save game MenuItems.

I changed the “Start New Game” text to “New Game” on line 36, deleted the Separator on line 39, and deleted the “Load Game” MenuItem – since it’s handled in the Startup window called by the “New Game” MenutItem.

MainWindow.xaml

<Window x:Class="WPFUI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewModels="clr-namespace:Engine.ViewModels;assembly=Engine"
        d:DataContext="{d:DesignInstance viewModels:GameSession}"
        mc:Ignorable="d"
        FontSize="11pt"
        Title="{Binding GameDetails.Title}" Height="768" Width="1024"
        KeyDown="MainWindow_OnKeyDown"
        Closing="MainWindow_OnClosing">
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="225"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="250"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <!-- Menu -->
        <Menu Grid.Row="0" Grid.Column="0"
              Grid.ColumnSpan="2"
              FontSize="11pt"
              Background="AliceBlue">
            <MenuItem Header="File">
                <MenuItem Header="New Game"
                          Click="StartNewGame_OnClick"/>
                <MenuItem Header="Save Game"
                          Click="SaveGame_OnClick"/>
                <Separator/>
                <MenuItem Header="Exit"
                          Click="Exit_OnClick"/>
            </MenuItem>
            <MenuItem Header="Help">
                <MenuItem Header="Help"
                          IsEnabled="False"/>
                <Separator/>
                <MenuItem Header="About"
                          IsEnabled="False"/>
            </MenuItem>
        </Menu>
        <!-- Player stats -->
        <Grid Grid.Row="1" Grid.Column="0" Background="Aquamarine">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Label Grid.Row="0" Grid.Column="0" Content="Name:"/>
            <Label Grid.Row="0" Grid.Column="1" Content="{Binding CurrentPlayer.Name}"/>
            <Label Grid.Row="1" Grid.Column="0" Content="Hit points:"/>
            <Label Grid.Row="1" Grid.Column="1" Content="{Binding CurrentPlayer.CurrentHitPoints}"/>
            <Label Grid.Row="2" Grid.Column="0" Content="Gold:"/>
            <Label Grid.Row="2" Grid.Column="1" Content="{Binding CurrentPlayer.Gold}"/>
            <Label Grid.Row="3" Grid.Column="0" Content="XP:"/>
            <Label Grid.Row="3" Grid.Column="1" Content="{Binding CurrentPlayer.ExperiencePoints}"/>
            <Label Grid.Row="4" Grid.Column="0" Content="Level:"/>
            <Label Grid.Row="4" Grid.Column="1" Content="{Binding CurrentPlayer.Level}"/>
            <!-- Player Attributes -->
            <ListBox Grid.Row="5" Grid.Column="0"
                      Grid.ColumnSpan="2"
                      Background="Aquamarine"
                      BorderThickness="0"
                      Grid.IsSharedSizeScope="True"
                      ItemsSource="{Binding CurrentPlayer.Attributes}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition SharedSizeGroup="Description"/>
                                </Grid.ColumnDefinitions>
                                <TextBlock Text="{Binding DisplayName}" 
                                           HorizontalAlignment="Left"
                                           MinWidth="100"/>
                            </Grid>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition SharedSizeGroup="ModifiedValue"/>
                                </Grid.ColumnDefinitions>
                                <TextBlock Text="{Binding ModifiedValue}"
                                           HorizontalAlignment="Right"/>
                            </Grid>
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Grid>
        <!-- Gameplay -->
        <Grid Grid.Row="1" Grid.Column="1"
              Background="Beige">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="3*"/>
                <ColumnDefinition Width="2*"/>
            </Grid.ColumnDefinitions>
            <!-- Game Messages -->
            <Border Grid.Row="0" Grid.Column="0"
                    Grid.RowSpan="2"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">
                <RichTextBox x:Name="GameMessages"
                             Background="Beige"
                             VerticalScrollBarVisibility="Auto">
                    <RichTextBox.Resources>
                        <Style TargetType="{x:Type Paragraph}">
                            <Setter Property="Margin" Value="0"/>
                        </Style>
                    </RichTextBox.Resources>
                </RichTextBox>
            </Border>
            <!-- Location information -->
            <Border Grid.Row="0" Grid.Column="1"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">
                <Grid Margin="3">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <TextBlock Grid.Row="0"
                               HorizontalAlignment="Center"
                               Text="{Binding CurrentLocation.Name}"/>
                    <Image Grid.Row="1"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Height="125"
                           Width="125"
                           Source="{Binding CurrentLocation.ImageName, 
                                            Converter={StaticResource FileToBitmapConverter}}"/>
                    <TextBlock Grid.Row="2"
                               HorizontalAlignment="Center"
                               Text="{Binding CurrentLocation.Description}"
                               TextWrapping="Wrap"/>
                </Grid>
            </Border>
            <!-- Monster information -->
            <Border Grid.Row="1" Grid.Column="1"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>
                    <TextBlock Grid.Row="0"
                               HorizontalAlignment="Center"
                               Height="Auto"
                               Text="{Binding CurrentMonster.Name}" />
                    <Image Grid.Row="1"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Height="125"
                           Width="125"
                           Source="{Binding CurrentMonster.ImageName, 
                                            Converter={StaticResource FileToBitmapConverter}}"/>
                    <StackPanel Grid.Row="2"
                                Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
                                HorizontalAlignment="Center"
                                Orientation="Horizontal">
                        <TextBlock>Current Hit Points:</TextBlock>
                        <TextBlock Text="{Binding CurrentMonster.CurrentHitPoints}" />
                    </StackPanel>
                </Grid>
            </Border>
        </Grid>
        <!-- Inventory, Quests, and Recipes -->
        <Grid Grid.Row="2" Grid.Column="0"
              Background="BurlyWood">
            <TabControl x:Name="PlayerDataTabControl">
                <TabItem Header="Inventory"
                         x:Name="InventoryTabItem">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Inventory.GroupedInventory}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Description"
                                                Binding="{Binding Item.Name, Mode=OneWay}"
                                                Width="*"/>
                            <DataGridTextColumn Header="Qty"
                                                IsReadOnly="True"
                                                Width="Auto"
                                                Binding="{Binding Quantity, Mode=OneWay}"/>
                            <DataGridTextColumn Header="Price"
                                                Binding="{Binding Item.Price, Mode=OneWay}"
                                                Width="Auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>
                <TabItem Header="Quests"
                         x:Name="QuestsTabItem">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Quests}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Name"
                                                Binding="{Binding PlayerQuest.Name, Mode=OneWay}"
                                                Width="*">
                                <DataGridTextColumn.CellStyle>
                                    <Style TargetType="DataGridCell">
                                        <Setter Property="ToolTip" 
                                                Value="{Binding PlayerQuest.ToolTipContents}"/>
                                    </Style>
                                </DataGridTextColumn.CellStyle>
                            </DataGridTextColumn>
                            <DataGridTextColumn Header="Done?"
                                                Binding="{Binding IsCompleted, Mode=OneWay}"
                                                Width="Auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>
                <TabItem Header="Recipes"
                         x:Name="RecipesTabItem">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Recipes}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Name"
                                                Binding="{Binding Name, Mode=OneWay}"
                                                Width="*">
                                <DataGridTextColumn.CellStyle>
                                    <Style TargetType="DataGridCell">
                                        <Setter Property="ToolTip" 
                                                Value="{Binding ToolTipContents}"/>
                                    </Style>
                                </DataGridTextColumn.CellStyle>
                            </DataGridTextColumn>
                            <DataGridTemplateColumn MinWidth="75">
                                <DataGridTemplateColumn.CellTemplate>
                                    <DataTemplate>
                                        <Button Click="OnClick_Craft"
                                                Width="55"
                                                Content="Craft"/>
                                    </DataTemplate>
                                </DataGridTemplateColumn.CellTemplate>
                            </DataGridTemplateColumn>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>
            </TabControl>
        </Grid>
        <!-- Action controls -->
        <Grid Grid.Row="2" Grid.Column="1"
              Background="Lavender">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="255" />
            </Grid.ColumnDefinitions>
            <!-- Combat Controls -->
            <Grid Grid.Row="0" Grid.Column="0"
                  HorizontalAlignment="Center"
                  VerticalAlignment="Center">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="150"/>
                    <ColumnDefinition Width="10"/>
                    <ColumnDefinition Width="50"/>
                </Grid.ColumnDefinitions>
                <ComboBox Grid.Row="0" Grid.Column="0"
                          Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
                          ItemsSource="{Binding CurrentPlayer.Inventory.Weapons}"
                          SelectedItem="{Binding CurrentPlayer.CurrentWeapon}"
                          DisplayMemberPath="Name"/>
                <Button Grid.Row="0" Grid.Column="2"
                        Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
                        Content="Use"
                        Click="OnClick_AttackMonster"/>
                <ComboBox Grid.Row="1" Grid.Column="0"
                          Visibility="{Binding CurrentPlayer.Inventory.HasConsumable, Converter={StaticResource BooleanToVisibility}}"
                          ItemsSource="{Binding CurrentPlayer.Inventory.Consumables}"
                          SelectedItem="{Binding CurrentPlayer.CurrentConsumable}"
                          DisplayMemberPath="Name"/>
                <Button Grid.Row="1" Grid.Column="2"
                        Visibility="{Binding CurrentPlayer.Inventory.HasConsumable, Converter={StaticResource BooleanToVisibility}}"
                        Content="Use"
                        Click="OnClick_UseCurrentConsumable"/>
            </Grid>
            <!-- Movement Controls -->
            <Grid Grid.Row="0" Grid.Column="1">
                <Grid.RowDefinitions>
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Button Grid.Row="0" Grid.Column="1" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveNorth"
                        Visibility="{Binding HasLocationToNorth, Converter={StaticResource BooleanToVisibility}}"
                        Content="North"/>
                <Button Grid.Row="1" Grid.Column="0" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveWest"
                        Visibility="{Binding HasLocationToWest, Converter={StaticResource BooleanToVisibility}}"
                        Content="West"/>
                <Button Grid.Row="1" Grid.Column="1" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_DisplayTradeScreen"
                        Visibility="{Binding HasTrader, Converter={StaticResource BooleanToVisibility}}"
                        Content="Trade"/>
                <Button Grid.Row="1" Grid.Column="2" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveEast"
                        Visibility="{Binding HasLocationToEast, Converter={StaticResource BooleanToVisibility}}"
                        Content="East"/>
                <Button Grid.Row="2" Grid.Column="1" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveSouth"
                        Visibility="{Binding HasLocationToSouth, Converter={StaticResource BooleanToVisibility}}"
                        Content="South"/>
            </Grid>
        </Grid>
    </Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using Engine.EventArgs;
using Engine.Models;
using Engine.Services;
using Engine.ViewModels;
using Microsoft.Win32;
using WPFUI.Windows;
namespace WPFUI
{
    public partial class MainWindow : Window
    {
        private const string SAVE_GAME_FILE_EXTENSION = "soscsrpg";
        private readonly MessageBroker _messageBroker = MessageBroker.GetInstance();
        private readonly Dictionary<Key, Action> _userInputActions = 
            new Dictionary<Key, Action>();
        private GameSession _gameSession;
        public MainWindow(Player player, int xLocation = 0, int yLocation = 0)
        {
            InitializeComponent();
            InitializeUserInputActions();
            SetActiveGameSessionTo(new GameSession(player, xLocation, yLocation));
        }
        private void OnClick_MoveNorth(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveNorth();
        }
        private void OnClick_MoveWest(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveWest();
        }
        private void OnClick_MoveEast(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveEast();
        }
        private void OnClick_MoveSouth(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveSouth();
        }
        private void OnClick_AttackMonster(object sender, RoutedEventArgs e)
        {
            _gameSession.AttackCurrentMonster();
        }
        private void OnClick_UseCurrentConsumable(object sender, RoutedEventArgs e)
        {
            _gameSession.UseCurrentConsumable();
        }
        private void OnGameMessageRaised(object sender, GameMessageEventArgs e)
        {
            GameMessages.Document.Blocks.Add(new Paragraph(new Run(e.Message)));
            GameMessages.ScrollToEnd();
        }
        private void OnClick_DisplayTradeScreen(object sender, RoutedEventArgs e)
        {
            if(_gameSession.CurrentTrader != null)
            {
                TradeScreen tradeScreen = new TradeScreen();
                tradeScreen.Owner = this;
                tradeScreen.DataContext = _gameSession;
                tradeScreen.ShowDialog();
            }
        }
        private void OnClick_Craft(object sender, RoutedEventArgs e)
        {
            Recipe recipe = ((FrameworkElement)sender).DataContext as Recipe;
            _gameSession.CraftItemUsing(recipe);
        }
        private void InitializeUserInputActions()
        {
            _userInputActions.Add(Key.W, () => _gameSession.MoveNorth());
            _userInputActions.Add(Key.A, () => _gameSession.MoveWest());
            _userInputActions.Add(Key.S, () => _gameSession.MoveSouth());
            _userInputActions.Add(Key.D, () => _gameSession.MoveEast());
            _userInputActions.Add(Key.Z, () => _gameSession.AttackCurrentMonster());
            _userInputActions.Add(Key.C, () => _gameSession.UseCurrentConsumable());
            _userInputActions.Add(Key.I, () => SetTabFocusTo("InventoryTabItem"));
            _userInputActions.Add(Key.Q, () => SetTabFocusTo("QuestsTabItem"));
            _userInputActions.Add(Key.R, () => SetTabFocusTo("RecipesTabItem"));
            _userInputActions.Add(Key.T, () => OnClick_DisplayTradeScreen(this, new RoutedEventArgs()));
        }
        private void MainWindow_OnKeyDown(object sender, KeyEventArgs e)
        {
            if(_userInputActions.ContainsKey(e.Key))
            {
                _userInputActions[e.Key].Invoke();
            }
        }
        private void SetTabFocusTo(string tabName)
        {
            foreach(object item in PlayerDataTabControl.Items)
            {
                if (item is TabItem tabItem)
                {
                    if (tabItem.Name == tabName)
                    {
                        tabItem.IsSelected = true;
                        return;
                    }
                }
            }
        }
        private void SetActiveGameSessionTo(GameSession gameSession)
        {
            // Unsubscribe from OnMessageRaised, or we will get double messages
            _messageBroker.OnMessageRaised -= OnGameMessageRaised;
            _gameSession = gameSession;
            DataContext = _gameSession;
            // Clear out previous game's messages
            GameMessages.Document.Blocks.Clear();
            _messageBroker.OnMessageRaised += OnGameMessageRaised;
        }
        private void StartNewGame_OnClick(object sender, RoutedEventArgs e)
        {
            Startup startup = new Startup();
            startup.Show();
            Close();
        }
        private void SaveGame_OnClick(object sender, RoutedEventArgs e)
        {
            SaveGame();
        }
        private void Exit_OnClick(object sender, RoutedEventArgs e)
        {
            Close();
        }
        private void MainWindow_OnClosing(object sender, CancelEventArgs e)
        {
            AskToSaveGame();
        }
        private void AskToSaveGame()
        {
            YesNoWindow message =
                new YesNoWindow("Save Game", "Do you want to save your game?");
            message.Owner = GetWindow(this);
            message.ShowDialog();
            if(message.ClickedYes)
            {
                SaveGame();
            }
        }
        private void SaveGame()
        {
            SaveFileDialog saveFileDialog =
                new SaveFileDialog
                {
                    InitialDirectory = AppDomain.CurrentDomain.BaseDirectory,
                    Filter = $"Saved games (*.{SAVE_GAME_FILE_EXTENSION})|*.{SAVE_GAME_FILE_EXTENSION}"
                };
            if (saveFileDialog.ShowDialog() == true)
            {
                SaveGameService.Save(_gameSession, saveFileDialog.FileName);
            }
        }
    }
}

Step 3: Modify WPFUI\Startup.xaml and Startup.xaml.cs We’ll add the ability to load a game from a saved game file to the Startup window.

We’ll start by updating the using directives for Startup.xaml.cs.

I added a constant for the saved game file extension to line 11 and deleted the _gameDetails backing variable (because we only used it in one place).

In the constructor, I removed storing the GameDetails to the private variable we also removed and just store it directly into the DataContext.

On lines 27-49, there’s a new function to load a saved game from disk.

It creates an OpenFileDialog box to let the user browse for the saved game file. If they select a file, we create a GameSession object from the file on lines 38-39. Then, we pass in the GameSession’s Player and location information to a new MainWindow game, to start playing the game.

In the Startup.xaml, we add a new row to the grid on line 17, then add the new “Load saved game” button on lines 30-35.

I also changed the text for “Start New Game” to “Start new game”, just because I think it looks a little nicer that way.

Startup.xaml

<Window x:Class="WPFUI.Startup"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFUI"
        xmlns:viewModels="clr-namespace:Engine.Models;assembly=Engine"
        d:DataContext="{d:DesignInstance viewModels:GameDetails}"
        mc:Ignorable="d"
        FontSize="11pt"
        Title="{Binding Title}" Height="400" Width="400">
 
    <Grid Margin="10,10,10,10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
 
        <Button Grid.Row="0" Grid.Column="0"
                Margin="0,5,0,5"
                HorizontalAlignment="Center"
                Width="125"
                Content="Start new game"
                Click="StartNewGame_OnClick"/>
 
        <Button Grid.Row="1" Grid.Column="0"
                Margin="0,5,0,5"
                HorizontalAlignment="Center"
                Width="125"
                Content="Load saved game"
                Click="LoadSavedGame_OnClick"/>
 
        <Button Grid.Row="2" Grid.Column="0"
                Margin="0,5,0,5"
                HorizontalAlignment="Center"
                Width="125"
                Content="Exit"
                Click="Exit_OnClick"/>
         
    </Grid>
 
</Window>

Startup.xaml.cs

using System;
using System.Windows;
using Engine.Services;
using Engine.ViewModels;
using Microsoft.Win32;
namespace WPFUI
{
    public partial class Startup : Window
    {
        private const string SAVE_GAME_FILE_EXTENSION = "soscsrpg";
        public Startup()
        {
            InitializeComponent();
            DataContext = GameDetailsService.ReadGameDetails();
        }
 
        private void StartNewGame_OnClick(object sender, RoutedEventArgs e)
        {
            CharacterCreation characterCreationWindow = new CharacterCreation();
            characterCreationWindow.Show();
            Close();
        }
        private void LoadSavedGame_OnClick(object sender, RoutedEventArgs e)
        {
            OpenFileDialog openFileDialog =
                new OpenFileDialog
                {
                    InitialDirectory = AppDomain.CurrentDomain.BaseDirectory,
                    Filter = $"Saved games (*.{SAVE_GAME_FILE_EXTENSION})|*.{SAVE_GAME_FILE_EXTENSION}"
                };
            if (openFileDialog.ShowDialog() == true)
            {
                GameSession gameSession = 
                    SaveGameService.LoadLastSaveOrCreateNew(openFileDialog.FileName);
                
                MainWindow mainWindow = 
                    new MainWindow(gameSession.CurrentPlayer,
                                   gameSession.CurrentLocation.XCoordinate,
                                   gameSession.CurrentLocation.YCoordinate);
                
                mainWindow.Show();
                Close();
            }
        }
 
        private void Exit_OnClick(object sender, RoutedEventArgs e)
        {
            Close();
        }
    }
}

Step 4: Delete TestEngine\Services\TestSaveGameService.cs and TestEngine\TestFiles\SavedGames\Game_0_1_000.soscsrpg We’ll add unit tests back in once we start working with different versions of saved game files.

NEXT LESSON: Lesson 19.1: Converting SOSCSRPG to .NET 5

PREVIOUS LESSON: Lesson 18.4: Adding Player Attributes to Living Entities

    Leave a Reply

    Your email address will not be published. Required fields are marked *