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() { Directory.Delete(this.SavePath, recursive: true); 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() }; // 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) 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.SetPositions(data.Characters); this.OnLoaded?.Invoke(); } /********* ** Private methods *********/ /// Get the current character positions. private IEnumerable GetPositions() { // player { var player = Game1.player; string name = player.name; string map = player.currentLocation.name; 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; 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.locationAfterWarp = Game1.getLocationFromName(data.Name); 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), false, true); 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); } } }