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. public class SaveManager { /********* ** Fields *********/ /// 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 relative path to the player data file. private string RelativeDataPath => Path.Combine("data", $"{Constants.SaveFolderName}.json"); /// Whether we should save at the next opportunity. private bool WaitingToSave; /// Currently displayed save menu (null if no menu is displayed) private NewSaveGameMenu currentSaveMenu; /********* ** 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; } private void empty(object o, EventArgs args) { } /// Perform any required update logic. public void Update() { // perform passive save if (this.WaitingToSave && Game1.activeClickableMenu == null) { this.currentSaveMenu = new NewSaveGameMenu(); this.currentSaveMenu.SaveComplete += this.CurrentSaveMenu_SaveComplete; Game1.activeClickableMenu = this.currentSaveMenu; this.WaitingToSave = false; } } /// Event function for NewSaveGameMenu event SaveComplete /// The event sender. /// The event arguments. private void CurrentSaveMenu_SaveComplete(object sender, EventArgs e) { this.currentSaveMenu.SaveComplete -= this.CurrentSaveMenu_SaveComplete; this.currentSaveMenu = null; //AfterSave.Invoke(this, EventArgs.Empty); } /// Clear saved data. public void ClearData() { if (File.Exists(Path.Combine(this.Helper.DirectoryPath, this.RelativeDataPath))) { File.Delete(Path.Combine(this.Helper.DirectoryPath, this.RelativeDataPath)); } 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 { this.currentSaveMenu = new NewSaveGameMenu(); this.currentSaveMenu.SaveComplete += this.CurrentSaveMenu_SaveComplete; Game1.activeClickableMenu = this.currentSaveMenu; } // save data to disk PlayerData data = new PlayerData { Time = Game1.timeOfDay, Characters = this.GetPositions().ToArray(), IsCharacterSwimming = Game1.player.swimming.Value }; this.Helper.Data.WriteJsonFile(this.RelativeDataPath, data); // clear any legacy data (no longer needed as backup) this.RemoveLegacyDataForThisPlayer(); } /// Load all game data. public void LoadData() { // get data PlayerData data = this.Helper.Data.ReadJsonFile(this.RelativeDataPath); if (data == null) return; // apply Game1.timeOfDay = data.Time; this.ResumeSwimming(data); this.SetPositions(data.Characters); this.OnLoaded?.Invoke(); // Notify other mods that load is complete //AfterLoad.Invoke(this, EventArgs.Empty); } /// 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) { Game1.player.changeIntoSwimsuit(); Game1.player.swimming.Value = true; } } catch { //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 (string.IsNullOrEmpty(map)) 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 || 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); } } }