using System; using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.Xna.Framework; using Omegasis.SaveAnywhere.Framework.Models; using StardewModdingAPI; using StardewValley; using StardewValley.Characters; using StardewValley.Monsters; namespace Omegasis.SaveAnywhere.Framework { /// Provides methods for saving and loading game data. internal class SaveManager { /********* ** Properties *********/ /// Simplifies access to game code. private readonly IReflectionHelper Reflection; /// A callback invoked when data is loaded. private readonly Action OnLoaded; /// SMAPI's APIs for this mod. private readonly IModHelper Helper; /// The full path to the player data file. private string SavePath => Path.Combine(this.Helper.DirectoryPath, "data", $"{Constants.SaveFolderName}.json"); /// Whether we should save at the next opportunity. private bool WaitingToSave; /********* ** Public methods *********/ /// Construct an instance. /// SMAPI's APIs for this mod. /// Simplifies access to game code. /// A callback invoked when data is loaded. public SaveManager(IModHelper helper, IReflectionHelper reflection, Action onLoaded) { this.Helper = helper; this.Reflection = reflection; this.OnLoaded = onLoaded; } /// Perform any required update logic. public void Update() { // perform passive save if (this.WaitingToSave && Game1.activeClickableMenu == null) { Game1.activeClickableMenu = new NewSaveGameMenu(); this.WaitingToSave = false; } } /// Clear saved data. public void ClearData() { File.Delete(this.SavePath); this.RemoveLegacyDataForThisPlayer(); } /// Initiate a game save. public void BeginSaveData() { // save game data Farm farm = Game1.getFarm(); if (farm.shippingBin.Any()) { Game1.activeClickableMenu = new NewShippingMenu(farm.shippingBin, this.Reflection); farm.shippingBin.Clear(); farm.lastItemShipped = null; this.WaitingToSave = true; } else Game1.activeClickableMenu = new NewSaveGameMenu(); // get data PlayerData data = new PlayerData { Time = Game1.timeOfDay, Characters = this.GetPositions().ToArray(), IsCharacterSwimming = Game1.player.swimming.Value }; // save to disk // ReSharper disable once PossibleNullReferenceException -- not applicable Directory.CreateDirectory(new FileInfo(this.SavePath).Directory.FullName); this.Helper.WriteJsonFile(this.SavePath, data); // clear any legacy data (no longer needed as backup)1 this.RemoveLegacyDataForThisPlayer(); } /// Load all game data. public void LoadData() { // get data PlayerData data = this.Helper.ReadJsonFile(this.SavePath); if (data == null) return; // apply Game1.timeOfDay = data.Time; this.ResumeSwimming(data); this.SetPositions(data.Characters); this.OnLoaded?.Invoke(); } /// /// Checks to see if the player was swimming when the game was saved and if so, resumes the swimming animation. /// /// public void ResumeSwimming(PlayerData data) { try { if (data.IsCharacterSwimming == true) { Game1.player.changeIntoSwimsuit(); Game1.player.swimming.Value = true; } } catch (Exception err) { err.ToString(); //Here to allow compatability with old save files. } } /********* ** Private methods *********/ /// Get the current character positions. private IEnumerable GetPositions() { // player { var player = Game1.player; string name = player.Name; string map = player.currentLocation.uniqueName.Value; //Try to get a unique name for the location and if we can't we are going to default to the actual name of the map. if (map == ""|| map==null) map = player.currentLocation.Name; //This is used to account for maps that share the same name but have a unique ID such as Coops, Barns and Sheds. Point tile = player.getTileLocationPoint(); int facingDirection = player.facingDirection; yield return new CharacterData(CharacterType.Player, name, map, tile, facingDirection); } // NPCs (including horse and pets) foreach (NPC npc in Utility.getAllCharacters()) { CharacterType? type = this.GetCharacterType(npc); if (type == null) continue; if (npc == null || npc.currentLocation == null) continue; string name = npc.Name; string map = npc.currentLocation.Name; Point tile = npc.getTileLocationPoint(); int facingDirection = npc.facingDirection; yield return new CharacterData(type.Value, name, map, tile, facingDirection); } } /// Reset characters to their saved state. /// The positions to set. /// Returns whether any NPCs changed position. private void SetPositions(CharacterData[] positions) { // player { CharacterData data = positions.FirstOrDefault(p => p.Type == CharacterType.Player && p.Name == Game1.player.Name); if (data != null) { Game1.player.previousLocationName = Game1.player.currentLocation.Name; //Game1.player. locationAfterWarp = Game1.getLocationFromName(data.Map); Game1.xLocationAfterWarp = data.X; Game1.yLocationAfterWarp = data.Y; Game1.facingDirectionAfterWarp = data.FacingDirection; Game1.fadeScreenToBlack(); Game1.warpFarmer(data.Map, data.X, data.Y, false); Game1.player.faceDirection(data.FacingDirection); } } // NPCs (including horse and pets) foreach (NPC npc in Utility.getAllCharacters()) { // get NPC type CharacterType? type = this.GetCharacterType(npc); if (type == null) continue; // get saved data CharacterData data = positions.FirstOrDefault(p => p.Type == type && p.Name == npc.Name); if (data == null) continue; // update NPC Game1.warpCharacter(npc, data.Map, new Point(data.X, data.Y)); npc.faceDirection(data.FacingDirection); } } /// Get the character type for an NPC. /// The NPC to check. private CharacterType? GetCharacterType(NPC npc) { if (npc is Monster) return null; if (npc is Horse) return CharacterType.Horse; if (npc is Pet) return CharacterType.Pet; return CharacterType.Villager; } /// Remove legacy save data for this player. private void RemoveLegacyDataForThisPlayer() { DirectoryInfo dataDir = new DirectoryInfo(Path.Combine(this.Helper.DirectoryPath, "Save_Data")); DirectoryInfo playerDir = new DirectoryInfo(Path.Combine(dataDir.FullName, Game1.player.Name)); if (playerDir.Exists) playerDir.Delete(recursive: true); if (dataDir.Exists && !dataDir.EnumerateDirectories().Any()) dataDir.Delete(recursive: true); } } }