Press "Enter" to skip to content

Lesson 19.10: Create a GameState model for SaveGameService

The current SaveGameService class accepts and instantiates GameSession objects – which are ViewModels. We can’t have that in the new solution structure because services won’t have access to the ViewModel project.

To solve this, we’ll create a new GameState model class. The GameSession will send it to (or get it from) the SaveGameService.


Link to video on YouTube

Step 1: Create Engine\Models\GameState.cs

This is the class we’ll pass into, and get back from, the SaveGameService. It contains the Player and current X/Y coordinate – the only information we need in the save game file.

GameState.cs

namespace Engine.Models
{
    public class GameState
    {
        public Player Player { get; init; }
        public int XCoordinate { get; init; }
        public int YCoordinate { get; init; }

        public GameState(Player player, int xCoordinate, int yCoordinate)
        {
            Player = player;
            XCoordinate = xCoordinate;
            YCoordinate = yCoordinate;
        }
    }
}

Step 2: Modify Engine\Services\SaveGameService.cs

Remove the “using Engine.ViewModels;” directive.

Change the Save function’s first parameter from “GameSession gameSession” to “GameState gameState” and change the variable inside SerializeObject from “GameSession” to “gameState”.

  • Change the GameSession parameter datatype and name to GameState (line 13)
  • Change the variable passed in to SerializeObject to gameState (line 16)

Change the LoadLastSaveOrCreateNew function to return a GameState object, instead of a GameSession object.

  1. Change the return type on line 19
  2. Change the class used in nameof (lines 34 and 35) from GameSession to GameState, since we don’t have access to the ViewModel namespace
  3. Change the return object instantiation on line 38.
  4. Change the comment on line 37 from GameSession to GameState

Throughout this class, replace:

  1. “nameof(GameSession.CurrentPlayer)” with “nameof(GameState.Player)”
  2. “[nameof(GameSession.CurrentLocation)][nameof(Location.XCoordinate)]” with “[nameof(GameState.XCoordinate)]”
  3. “[nameof(GameSession.CurrentLocation)][nameof(Location.YCoordinate)]” with “[nameof(GameState.YCoordinate)]”

SaveGameService.cs

using System;
using System.Collections.Generic;
using System.IO;
using Engine.Factories;
using Engine.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Engine.Services
{
    public static class SaveGameService
    {
        public static void Save(GameState gameState, string fileName)
        {
            File.WriteAllText(fileName,
                              JsonConvert.SerializeObject(gameState, Formatting.Indented));
        }

        public static GameState 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(GameState.XCoordinate)];
                int y = (int)data[nameof(GameState.YCoordinate)];

                // Create GameState object with saved game data
                return new GameState(player, x, y);
            }
            catch
            {
                throw new FormatException($"Error reading: {fileName}");
            }
        }

        private static Player CreatePlayer(JObject data)
        {
            Player player =
                new Player((string)data[nameof(GameState.Player)][nameof(Player.Name)],
                           (int)data[nameof(GameState.Player)][nameof(Player.ExperiencePoints)],
                           (int)data[nameof(GameState.Player)][nameof(Player.MaximumHitPoints)],
                           (int)data[nameof(GameState.Player)][nameof(Player.CurrentHitPoints)],
                           GetPlayerAttributes(data),
                           (int)data[nameof(GameState.Player)][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(GameState.Player)]
                [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(GameState.Player)]
                [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(GameState.Player)]
                [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(GameState.Player)][nameof(Player.Recipes)])
            {
                int recipeId = (int)recipeToken[nameof(Recipe.ID)];

                Recipe recipe = RecipeFactory.RecipeByID(recipeId);

                player.Recipes.Add(recipe);
            }
        }
    }
}

Step 3: Modify WPFUI\MainWindow.xaml.cs

Change line 186 in the SaveGame function (the call to SaveGameService.Save) to pass in a GameState object that’s populated from the _gameContext variable.

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.Models;
using Engine.Services;
using Engine.ViewModels;
using Microsoft.Win32;
using SOSCSRPG.Models.EventArgs;
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(new GameState(_gameSession.CurrentPlayer, 
                    _gameSession.CurrentLocation.XCoordinate, 
                    _gameSession.CurrentLocation.YCoordinate), saveFileDialog.FileName);
            }
        }
    }
}

Step 4: Modify WPFUI\Startup.xaml.cs

Change line 39 of the LoadSavedGame_OnClick function to get back a GameState object from SaveGameService.LoadLastSaveOrCreateNew – instead of the current GameSession it’s getting back.

Update the instantiation of the MainWindow object (lines 43-45) to use the updated “gameState” variable, instead of the current “GameSession” variable.

Startup.xaml.cs

using System;
using System.Windows;
using Engine.Models;
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)
            {
                GameState gameState = 
                    SaveGameService.LoadLastSaveOrCreateNew(openFileDialog.FileName);
                
                MainWindow mainWindow = 
                    new MainWindow(gameState.Player,
                                   gameState.XCoordinate,
                                   gameState.YCoordinate);
                
                mainWindow.Show();
                Close();
            }
        }
 
        private void Exit_OnClick(object sender, RoutedEventArgs e)
        {
            Close();
        }
    }
}

Step 5: Test the game

10 Comments

  1. Steven
    Steven 2021-12-09

    I’m getting warring’s like these
    3>C:\Games\SOSCSRPG-master\Engine\Models\PlayerAttribute.cs(14,50,14,65): warning CS0067: The event ‘PlayerAttribute.PropertyChanged’ is never used
    3>C:\Games\SOSCSRPG-master\Engine\Models\QuestStatus.cs(7,50,7,65): warning CS0067: The event ‘QuestStatus.PropertyChanged’ is never used
    3>C:\Games\SOSCSRPG-master\Engine\Models\GroupedInventoryItem.cs(7,50,7,65): warning CS0067: The event ‘GroupedInventoryItem.PropertyChanged’ is never used
    3>C:\Games\SOSCSRPG-master\Engine\ViewModels\GameSession.cs(21,50,21,65): warning CS0067: The event ‘GameSession.PropertyChanged’ is never used
    3>C:\Games\SOSCSRPG-master\Engine\Models\LivingEntity.cs(17,50,17,65): warning CS0067: The event ‘LivingEntity.PropertyChanged’ is never used
    3>MSBUILD : warning FodyPackageReference: Fody: The package reference for PropertyChanged.Fody does not contain PrivateAssets=’All
    5>C:\Games\SOSCSRPG-master\SOSCSRPG.ViewModels\CharacterCreationViewModel.cs(12,50,12,65): warning CS0067: The event ‘CharacterCreationViewModel.PropertyChanged’ is never used

    Every place fodypackagaereference that has this:
    all
    I read online to add to this part to make it this:
    that fixes the fody warring’s
    is the warring’s ok or should I fix them?

    • SOSCSRPG
      SOSCSRPG 2021-12-09

      Hi Steven,

      Can you play the game, and does movement and combat work?

      If so, then those warnings are acceptable. They’re saying the “PropertyChanged” even handler is never used – but it is used, once Fody injects its code during the build process.

  2. Steven
    Steven 2021-12-09

    nvm the fody part wont show for some reason when i click post

    • SOSCSRPG
      SOSCSRPG 2021-12-09

      FYI: Comments that have a “less-than” sign usually automatically have that part removed. This is to prevent hacking attempts that would inject JavaScript code into the comments.

      • Steven
        Steven 2021-12-09

        oh i see thats why. Yes the game works fine just didnt know if i should fix those warring’s or if they were harmless

  3. Steven
    Steven 2021-12-09

    what i was saying bout the fody part was

    Every fodypackagaereference that has this: <PackageReference Include="PropertyChanged.Fody" Version="3.4.0"
    add this to it <PackageReference Include="PropertyChanged.Fody" Version="3.4.0" PrivateAssets="All" and that fixes the fody warring's

    • SOSCSRPG
      SOSCSRPG 2021-12-09

      Thanks! I’ll make a note to add that in the next lesson.

  4. Steven
    Steven 2021-12-10

    I created a new location and monster with a quest when i run game and go to the location abd go to attack the monster the game crashes giving me this error:

    System.NullReferenceException
    HResult=0x80004003
    Message=Object reference not set to an instance of an object.
    Source=Engine
    StackTrace:
    at Engine.Models.LivingEntity.UseCurrentWeaponOn(LivingEntity target) in C:\Games\SOSCSRPG-master\Engine\Models\LivingEntity.cs:line 96

    when i go to the spider forest i can attack the monster there with no crash
    here is a screenshot of the error in VS if it helps:https://ibb.co/mCPRy2G

    • SOSCSRPG
      SOSCSRPG 2021-12-11

      Is your only change to the game data files? Can you upload your game data files someplace where I can test the game with them?

      • Steven
        Steven 2021-12-11

        i fixed the problem it was a typo on my part

Leave a Reply

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