#region Copyright /* -------------------------------------------------------------------------------- This source file is part of Xenocide by Project Xenocide Team For the latest info on Xenocide, see http://www.projectxenocide.com/ This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/2.5/ or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA. -------------------------------------------------------------------------------- */ /* * @file LoadSaveGameScreen.cs * @date Created: 2007/01/21 * @author File creator: David Teviotdale * @author Credits: none */ #endregion #region Using Statements using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Xml.Serialization; using System.Runtime.Serialization.Formatters.Binary; using System.Diagnostics; using System.Threading; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Storage; using Microsoft.Xna.Framework.GamerServices; using CeGui; using ProjectXenocide.UI.Dialogs; using ProjectXenocide.Model; using ProjectXenocide.Model.Geoscape; using ProjectXenocide.Utils; using Xenocide.Resources; #endregion namespace ProjectXenocide.UI.Screens { /// /// Screen for Saving game to file, and loading game from a file /// public class LoadSaveGameScreen : Screen { /// /// Constructor /// /// Run in Load or Save mode /// Screen to go to if cancel is pressed public LoadSaveGameScreen(Mode mode, CancelScreen cancelScreen) : base("LoadSaveGameScreen") { this.mode = mode; this.cancelScreen = cancelScreen; } #region Create the CeGui widgets /// /// create the widgets we're going to show on the screen /// protected override void CreateCeguiWidgets() { // initializeEditBox filenameEditBox = GuiBuilder.CreateEditBox("editBox"); AddWidget(filenameEditBox, 0.05f, 0.84f, 0.9f, 0.07f); // The list of saved games InitializeGrid(); // and the buttons deleteButton = AddButton("BUTTON_DELETE", 0.36f, 0.92f, 0.25f, 0.07f); cancelButton = AddButton("BUTTON_CANCEL", 0.66f, 0.92f, 0.25f, 0.07f); deleteButton.Clicked += new GuiEventHandler(OnDeleteGame); cancelButton.Clicked += new GuiEventHandler(OnCloseScreen); // save/load button depends on mode if (mode == Mode.Save) { saveButton = AddButton("BUTTON_SAVE", 0.07f, 0.92f, 0.25f, 0.07f); saveButton.Clicked += new GuiEventHandler(OnSaveGame); } else { saveButton = AddButton("BUTTON_LOAD", 0.07f, 0.92f, 0.25f, 0.07f); saveButton.Clicked += new GuiEventHandler(OnLoadGame); } } /// /// Creates a MultiColumnListBox (will hold the name of the saved games) /// private void InitializeGrid() { savesgrid = GuiBuilder.CreateGrid("savesgrid"); AddWidget(savesgrid, 0.01f, 0.08f, 0.98f, 0.75f); AddColumnHeader("Name", 0.4f); AddColumnHeader("Real Time", 0.295f); AddColumnHeader("Game Time", 0.295f); AddSaveGamesToGrid(); savesgrid.SelectionChanged += new WindowEventHandler(OnGridSelectionChanged); } /// /// Add a column to the supplied grid of saved games /// /// Name to give the column /// Width to make the column (relative to grid's width) private void AddColumnHeader(String title, float width) { savesgrid.AddColumn(title, savesgrid.ColumnCount, width); } private void AddRowToGrid(String NameCol, String RealTimeCol, String GameTimeCol, int rowNum) { savesgrid.InsertRow(Util.CreateListboxItem(NameCol), 0, rowNum); Util.AddStringElementToGrid(savesgrid, 1, rowNum, RealTimeCol); Util.AddStringElementToGrid(savesgrid, 2, rowNum, GameTimeCol); } private CeGui.Widgets.MultiColumnList savesgrid; private CeGui.Widgets.EditBox filenameEditBox; private CeGui.Widgets.PushButton saveButton; private CeGui.Widgets.PushButton deleteButton; private CeGui.Widgets.PushButton cancelButton; #endregion Create the CeGui widgets #region event handlers /// Write the game state to named file /// Not used /// Not used private void OnSaveGame(object sender, GuiEventArgs e) { String saveName = filenameEditBox.Text; if (SaveGameExists(saveName)) { Util.ShowMessageBox(Strings.SCREEN_LOADSAVEGAME_DUPLICATE_FILENAME); } else { // if able to save to file, update the grid if (WriteToFile(saveName)) { AddSaveGameToGrid(saveName); } } } /// Load the seleted game /// Not used /// Not used [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "FxCop False Positive")] private void OnLoadGame(object sender, GuiEventArgs e) { CeGui.Widgets.ListboxItem item = savesgrid.GetFirstSelectedItem(); if (item != null) { // load the file GameState game = ReadFromFile(item.Text); if (null != game) { Xenocide.GameState = game; // We always restart with time suspended Xenocide.GameState.GeoData.GeoTime.StopTime(); // resume the game (may be either geoscape or battlescape) ScreenManager.ScheduleScreen(new GeoscapeScreen()); } } } /// Restore screen that was present before Save/Load game screen /// Not used /// Not used private void OnCloseScreen(object sender, GuiEventArgs e) { Screen nextScreen = null; switch (cancelScreen) { case CancelScreen.Geoscape: nextScreen = new GeoscapeScreen(); break; case CancelScreen.Start: nextScreen = new StartScreen(); break; case CancelScreen.Battlescape: // ToDo implement break; default: // Should never get here Debug.Assert(false); break; } ScreenManager.ScheduleScreen(nextScreen); } /// delete the currently selected save file /// Button that has been clicked /// Not used private void OnDeleteGame(object sender, GuiEventArgs e) { // delete the currently selected save game CeGui.Widgets.ListboxItem item = savesgrid.GetFirstSelectedItem(); if (item != null) { //ToDo: we should pop up a messagebox to confirm user really does want to delete the savegame DeleteSaveGameFile(item.Text); // Now remove the savegame from the screen's grid (and edit box) savesgrid.RemoveRow(savesgrid.GetRowIndexOfItem(item)); filenameEditBox.Text = String.Empty; } } /// Handles user clicking on an item in the grid /// Not used /// Not used private void OnGridSelectionChanged(object sender, WindowEventArgs e) { CeGui.Widgets.ListboxItem item = savesgrid.GetFirstSelectedItem(); if (item != null) { Xenocide.AudioSystem.PlaySound("Menu\\buttonclick2_changesetting.ogg"); filenameEditBox.Text = item.Text; } } #endregion event handlers #region File manipulation routines /// /// Populate the grid with the existing saved games /// private void AddSaveGamesToGrid() { using (StorageContainer container = GetContainer()) { ICollection FileList = Directory.GetFiles(container.Path); foreach (string filename in FileList) { using (FileStream stream = File.Open(filename, FileMode.Open)) { AddSaveGameToGrid(stream, Path.GetFileName(filename)); } } } } /// /// Add this save game to the grid of saved games /// /// stream holding save game /// Name of the file private void AddSaveGameToGrid(FileStream stream, String filename) { SaveGameHeader header = new SaveGameHeader(stream); AddRowToGrid( filename, header.realTime, header.gameTime, 0); } /// /// Add this save game to the grid of saved games /// /// filename of saved game private void AddSaveGameToGrid(string filename) { using (StorageContainer container = GetContainer()) { using (FileStream stream = File.Open(Path.Combine(container.Path, filename), FileMode.Open)) { AddSaveGameToGrid(stream, filename); } } } /// /// Write the current game state to file /// /// Name of file to save game as /// true if successful [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Guideline is wrong in this case")] private bool WriteToFile(string saveName) { try { using (StorageContainer container = GetContainer()) { string filename = Path.Combine(container.Path, saveName); using (FileStream stream = File.Create(filename)) { SaveGameHeader.WriteHeader(stream); WriteGameState(stream); } } return true; } catch (Exception e) { Util.ShowMessageBox(Strings.MSGBOX_UNABLE_TO_SAVE_FILE, e.Message); return false; } } /// /// Retrieve a GameState from file /// /// Name of file holding save game /// GameState to set game to private GameState ReadFromFile(string filename) { using (StorageContainer container = GetContainer()) { using (FileStream stream = File.Open(Path.Combine(container.Path, filename), FileMode.Open)) { // check version from header SaveGameHeader saveGameHeader = new SaveGameHeader(stream); if (saveGameHeader.IsSameVersion(Xenocide.CurrentVersion)) { // get the game state BinaryFormatter formatter = new BinaryFormatter(); return (GameState)formatter.Deserialize(stream); } else { Util.ShowMessageBox(Strings.SCREEN_LOADSAVEGAME_VERSION_CONFLICT); return null; } } } } /// /// Write the game state to a stream /// /// Stream to write the game state to private static void WriteGameState(FileStream stream) { BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(stream, Xenocide.GameState); } /// /// Does a SaveGame with this (file)name already exist? /// /// filename to check /// true if it exists private bool SaveGameExists(string filename) { using (StorageContainer container = GetContainer()) { return File.Exists(Path.Combine(container.Path, filename)); } } /// /// Delete the specified file (presumed to be a save game) /// /// Name of file to delete private void DeleteSaveGameFile(string filename) { using (StorageContainer container = GetContainer()) { string path = Path.Combine(container.Path, filename); if (File.Exists(path)) { File.Delete(path); } } } /// /// Get the container (directory) holding the saved files /// /// the container private StorageContainer GetContainer() { // this bit is dummy on windows IAsyncResult result = Guide.BeginShowStorageDeviceSelector(PlayerIndex.One, null, null); StorageDevice device = Guide.EndShowStorageDeviceSelector(result); // Now open container(directory) return device.OpenContainer(savesDirectory); } #endregion File manipulation routines /// /// Screen can run in two modes, Load Game or Save Game /// public enum Mode { /// /// Run dialog in Save Game mode /// Save, /// /// Run dialog in Load Game mode /// Load } /// /// Is screen running as Save or Load? /// private Mode mode; /// /// Screen to go to if cancel is pressed /// public enum CancelScreen { /// /// Go to Geoscape screen /// Geoscape, /// /// Go to StartScreen /// Start, /// /// Go to Battlescape screen /// Battlescape } /// /// Screen to go to if cancel is pressed /// private CancelScreen cancelScreen; private string savesDirectory = ".\\XeNAcide\\saves"; /// /// The "header" information in a save game file /// [Serializable] private class SaveGameHeader { // data members public string realTime; public string gameTime; public string assemblyVersion; /// /// Read a save game header from a stream /// /// Stream to read the header from public SaveGameHeader(FileStream stream) { realTime = SaveGameHeader.ReadString(stream); gameTime = SaveGameHeader.ReadString(stream); assemblyVersion = SaveGameHeader.ReadString(stream); } /// /// Write a save game header to a stream /// /// Stream to write the header to public static void WriteHeader(FileStream stream) { WriteString(stream, FormatTime(DateTime.Now)); WriteString(stream, FormatTime(Xenocide.GameState.GeoData.GeoTime.Time)); WriteString(stream, Xenocide.CurrentVersion); } /// /// Compares a version with version recorded in header /// /// String representation of version /// True if versions are the same, false if not public bool IsSameVersion(string version) { return assemblyVersion == version; } // format a time for display in the load/save dialog /// time to format /// the formatted time public static string FormatTime(DateTime time) { return time.ToString("yyyy-MM-dd HH:mm:ss", Thread.CurrentThread.CurrentCulture); } /// /// Write the specified string to a file /// /// stream to write to /// string to write to the file /// Used to write header to file /// Had to write this myself because StreamWriter closes stream when it's done public static void WriteString(Stream fs, String value) { byte[] info = new UTF8Encoding(true).GetBytes(value); Debug.Assert(info.Length < 256); fs.WriteByte((byte)info.Length); fs.Write(info, 0, info.Length); } /// /// Read a string from a file at the current position /// /// file to read from /// string read /// Used to read header from file /// Had to write this myself because StreamReader closes stream when it's done public static String ReadString(Stream fs) { int count = fs.ReadByte(); byte[] b = new byte[count]; fs.Read(b, 0, count); UTF8Encoding temp = new UTF8Encoding(true); return temp.GetString(b); } } } }