Merge pull request #22 from Pathoschild/save-anywhere-fixes

Overhaul Save Anywhere file format, various fixes
This commit is contained in:
janavarro95 2017-08-05 21:59:33 -07:00 committed by GitHub
commit 7089475d46
10 changed files with 273 additions and 340 deletions

View File

@ -1,61 +0,0 @@
using System;
using System.IO;
namespace Omegasis.SaveAnywhere.Framework
{
/// <summary>Provides methods for reading and writing the config file.</summary>
internal class ConfigUtilities
{
/*********
** Properties
*********/
/// <summary>The full path to the mod folder.</summary>
private readonly string ModPath;
/*********
** Accessors
*********/
/// <summary>The key which saves the game.</summary>
public string KeyBinding { get; private set; } = "K";
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modPath">The full path to the mod folder.</param>
public ConfigUtilities(string modPath)
{
this.ModPath = modPath;
}
/// <summary>Load the configuration settings.</summary>
public void LoadConfig()
{
string path = Path.Combine(this.ModPath, "Save_Anywhere_Config.txt");
if (!File.Exists(path))
this.KeyBinding = "K";
else
{
string[] text = File.ReadAllLines(path);
this.KeyBinding = Convert.ToString(text[3]);
}
}
/// <summary>Save the configuration settings.</summary>
public void WriteConfig()
{
string path = Path.Combine(this.ModPath, "Save_Anywhere_Config.txt");
string[] text = new string[20];
text[0] = "Config: Save_Anywhere Info. Feel free to mess with these settings.";
text[1] = "====================================================================================";
text[2] = "Key binding for saving anywhere. Press this key to save anywhere!";
text[3] = this.KeyBinding;
File.WriteAllLines(path, text);
}
}
}

View File

@ -0,0 +1,9 @@
namespace Omegasis.SaveAnywhere.Framework
{
/// <summary>The mod configuration.</summary>
internal class ModConfig
{
/// <summary>The key which initiates a save.</summary>
public string SaveKey { get; set; } = "K";
}
}

View File

@ -0,0 +1,18 @@
namespace Omegasis.SaveAnywhere.Framework.Models
{
/// <summary>The character type for an NPC.</summary>
internal enum CharacterType
{
/// <summary>The player.</summary>
Player = 1,
/// <summary>The player's horse.</summary>
Horse = 2,
/// <summary>The player's pet.</summary>
Pet = 3,
/// <summary>A villager.</summary>
Villager = 4
}
}

View File

@ -0,0 +1,12 @@
namespace Omegasis.SaveAnywhere.Framework.Models
{
/// <summary>The data for the current player.</summary>
internal class PlayerData
{
/// <summary>The current time.</summary>
public int Time { get; set; }
/// <summary>The saved character data.</summary>
public CharacterData[] Characters { get; set; }
}
}

View File

@ -0,0 +1,63 @@
using Microsoft.Xna.Framework;
namespace Omegasis.SaveAnywhere.Framework.Models
{
/// <summary>Represents saved data for an NPC.</summary>
internal class CharacterData
{
/*********
** Accessors
*********/
/// <summary>The character type.</summary>
public CharacterType Type { get; set; }
/// <summary>The character name.</summary>
public string Name { get; set; }
/// <summary>The map name.</summary>
public string Map { get; set; }
/// <summary>The X position.</summary>
public int X { get; set; }
/// <summary>The Y position.</summary>
public int Y { get; set; }
/// <summary>The direction the character is facing.</summary>
public int FacingDirection { get; set; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <remarks>This default constructor is needed by Json.NET.</remarks>
public CharacterData() { }
/// <summary>Construct an instance.</summary>
/// <param name="type">The character type.</param>
/// <param name="name">The character name.</param>
/// <param name="map">The map name.</param>
/// <param name="x">The X position.</param>
/// <param name="y">The Y position.</param>
/// <param name="facingDirection">The direction the character is facing.</param>
public CharacterData(CharacterType type, string name, string map, int x, int y, int facingDirection)
{
this.Type = type;
this.Name = name;
this.Map = map;
this.X = x;
this.Y = y;
this.FacingDirection = facingDirection;
}
/// <summary>Construct an instance.</summary>
/// <param name="type">The character type.</param>
/// <param name="name">The character name.</param>
/// <param name="map">The map name.</param>
/// <param name="tile">The tile position.</param>
/// <param name="facingDirection">The direction the character is facing.</param>
public CharacterData(CharacterType type, string name, string map, Point tile, int facingDirection)
: this(type, name, map, tile.X, tile.Y, facingDirection) { }
}
}

View File

@ -0,0 +1,7 @@
using StardewValley.Menus;
namespace Omegasis.SaveAnywhere.Framework
{
/// <summary>A marker subclass to detect when a custom save is in progress.</summary>
internal class NewSaveGameMenu : SaveGameMenu { }
}

View File

@ -1,13 +1,13 @@
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.Menus;
using StardewValley.Monsters;
using SFarmer = StardewValley.Farmer;
namespace Omegasis.SaveAnywhere.Framework
{
@ -17,26 +17,17 @@ namespace Omegasis.SaveAnywhere.Framework
/*********
** Properties
*********/
/// <summary>The player for which to save data.</summary>
private readonly SFarmer Player;
/// <summary>Simplifies access to game code.</summary>
private readonly IReflectionHelper Reflection;
/// <summary>Writes messages to the console and log file.</summary>
private readonly IMonitor Monitor;
/// <summary>A callback invoked when data is loaded.</summary>
private readonly Action OnLoaded;
/// <summary>A callback invoked when villagers are reset during a load.</summary>
private readonly Action OnVillagersReset;
/// <summary>SMAPI's APIs for this mod.</summary>
private readonly IModHelper Helper;
/// <summary>The full path to the folder in which to store data for this player.</summary>
private readonly string SavePath;
/// <summary>The full path to the folder in which to store animal data for this player.</summary>
private readonly string SaveAnimalsPath;
/// <summary>The full path to the folder in which to store villager data for this player.</summary>
private readonly string SaveVillagersPath;
/// <summary>The full path to the player data file.</summary>
private string SavePath => Path.Combine(this.Helper.DirectoryPath, "data", $"{Constants.SaveFolderName}.json");
/// <summary>Whether we should save at the next opportunity.</summary>
private bool WaitingToSave;
@ -46,23 +37,14 @@ namespace Omegasis.SaveAnywhere.Framework
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="player">The player for which to save data.</param>
/// <param name="modPath">The full path to the mod folder.</param>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="helper">SMAPI's APIs for this mod.</param>
/// <param name="reflection">Simplifies access to game code.</param>
/// <param name="onVillagersReset">A callback invoked when villagers are reset during a load.</param>
public SaveManager(SFarmer player, string modPath, IMonitor monitor, IReflectionHelper reflection, Action onVillagersReset)
/// <param name="onLoaded">A callback invoked when data is loaded.</param>
public SaveManager(IModHelper helper, IReflectionHelper reflection, Action onLoaded)
{
// save info
this.Player = player;
this.Monitor = monitor;
this.Helper = helper;
this.Reflection = reflection;
this.OnVillagersReset = onVillagersReset;
// generate paths
this.SavePath = Path.Combine(modPath, "Save_Data", player.name);
this.SaveAnimalsPath = Path.Combine(this.SavePath, "Animals");
this.SaveVillagersPath = Path.Combine(this.SavePath, "NPC_Save_Info");
this.OnLoaded = onLoaded;
}
/// <summary>Perform any required update logic.</summary>
@ -71,13 +53,20 @@ namespace Omegasis.SaveAnywhere.Framework
// perform passive save
if (this.WaitingToSave && Game1.activeClickableMenu == null)
{
Game1.activeClickableMenu = new SaveGameMenu();
Game1.activeClickableMenu = new NewSaveGameMenu();
this.WaitingToSave = false;
}
}
/// <summary>Save all game data.</summary>
public void SaveGameAndPositions()
/// <summary>Clear saved data.</summary>
public void ClearData()
{
Directory.Delete(this.SavePath, recursive: true);
this.RemoveLegacyDataForThisPlayer();
}
/// <summary>Initiate a game save.</summary>
public void BeginSaveData()
{
// save game data
Farm farm = Game1.getFarm();
@ -89,280 +78,134 @@ namespace Omegasis.SaveAnywhere.Framework
this.WaitingToSave = true;
}
else
Game1.activeClickableMenu = new SaveGameMenu();
Game1.activeClickableMenu = new NewSaveGameMenu();
// save custom data
Directory.CreateDirectory(this.SaveAnimalsPath);
Directory.CreateDirectory(this.SaveVillagersPath);
this.SavePlayerPosition();
this.SaveHorsePosition();
this.SavePetPosition();
this.SaveVillagerPositions();
// 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();
}
/// <summary>Load all game data.</summary>
public void LoadPositions()
public void LoadData()
{
if (!this.HasSaveData())
// get data
PlayerData data = this.Helper.ReadJsonFile<PlayerData>(this.SavePath);
if (data == null)
return;
this.LoadPlayerPosition();
this.LoadHorsePosition();
this.LoadPetPosition();
bool anyVillagersMoved = this.LoadVillagerPositions();
if (anyVillagersMoved)
this.OnVillagersReset?.Invoke();
// apply
Game1.timeOfDay = data.Time;
this.SetPositions(data.Characters);
this.OnLoaded?.Invoke();
}
/*********
** Private methods
*********/
/// <summary>Save the horse state to the save file.</summary>
private void SaveHorsePosition()
/// <summary>Get the current character positions.</summary>
private IEnumerable<CharacterData> GetPositions()
{
// find horse
Horse horse = Utility.findHorse();
if (horse == null)
return;
// player
{
var player = Game1.player;
string name = player.name;
string map = player.currentLocation.name;
Point tile = player.getTileLocationPoint();
int facingDirection = player.facingDirection;
// get horse info
string map = horse.currentLocation.name;
Point tile = horse.getTileLocationPoint();
// save data
string path = Path.Combine(this.SaveAnimalsPath, $"Horse_Save_Info_{this.Player.name}.txt");
string[] text = new string[20];
text[0] = "Horse: Save_Anywhere Info. Editing this might break some things.";
text[1] = "====================================================================================";
text[2] = "Horse Current Map Name";
text[3] = map;
text[4] = "Horse X Position";
text[5] = tile.X.ToString();
text[6] = "Horse Y Position";
text[7] = tile.Y.ToString();
File.WriteAllLines(path, text);
yield return new CharacterData(CharacterType.Player, name, map, tile, facingDirection);
}
/// <summary>Reset the horse to the saved state.</summary>
private void LoadHorsePosition()
{
// find horse
Horse horse = Utility.findHorse();
if (horse == null)
return;
// get file path
string path = Path.Combine(this.SaveAnimalsPath, $"Horse_Save_Info_{this.Player.name}.txt");
if (!File.Exists(path))
return;
// read saved data
string[] text = File.ReadAllLines(path);
string map = Convert.ToString(text[3]);
int x = Convert.ToInt32(text[5]);
int y = Convert.ToInt32(text[7]);
// update horse
Game1.warpCharacter(horse, map, new Point(x, y), false, true);
}
/// <summary>Save the villager states to the save file.</summary>
private void SaveVillagerPositions()
{
// NPCs (including horse and pets)
foreach (NPC npc in Utility.getAllCharacters())
{
// ignore non-villagers
if (npc is Pet || npc is Monster)
CharacterType? type = this.GetCharacterType(npc);
if (type == null)
continue;
// get NPC data
string name = npc.name;
string map = npc.currentLocation.name;
Point tile = npc.getTileLocationPoint();
int facingDirection = npc.facingDirection;
// save data
string path = Path.Combine(this.SaveVillagersPath, npc.name + ".txt");
string[] text = new string[20];
text[0] = "NPC: Save_Anywhere Info. Editing this might break some things.";
text[1] = "====================================================================================";
text[2] = "NPC Name";
text[3] = name;
text[4] = "NPC Current Map Name";
text[5] = map;
text[6] = "NPC X Position";
text[7] = tile.X.ToString();
text[8] = "NPC Y Position";
text[9] = tile.Y.ToString();
File.WriteAllLines(path, text);
yield return new CharacterData(type.Value, name, map, tile, facingDirection);
}
}
/// <summary>Reset the villagers to their saved state.</summary>
/// <returns>Returns whether any villagers changed position.</returns>
private bool LoadVillagerPositions()
/// <summary>Reset characters to their saved state.</summary>
/// <param name="positions">The positions to set.</param>
/// <returns>Returns whether any NPCs changed position.</returns>
private void SetPositions(CharacterData[] positions)
{
bool anyLoaded = false;
// 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())
{
// ignore non-villagers
if (npc is Pet || npc is Monster)
// get NPC type
CharacterType? type = this.GetCharacterType(npc);
if (type == null)
continue;
// get file path
string path = Path.Combine(this.SaveVillagersPath, npc.name + ".txt");
if (!File.Exists(path))
{
this.Monitor.Log($"No save data for {npc.name} villager, skipping.", LogLevel.Error);
continue;
}
// read data
string[] text = File.ReadAllLines(path);
string map = Convert.ToString(text[5]);
int x = Convert.ToInt32(text[7]);
int y = Convert.ToInt32(text[9]);
if (string.IsNullOrEmpty(map))
// get saved data
CharacterData data = positions.FirstOrDefault(p => p.Type == type && p.Name == npc.name);
if (data == null)
continue;
// update NPC
anyLoaded = true;
Game1.warpCharacter(npc, map, new Point(x, y), false, true);
Game1.warpCharacter(npc, data.Map, new Point(data.X, data.Y), false, true);
npc.faceDirection(data.FacingDirection);
}
}
return anyLoaded;
}
/// <summary>Save the pet state to the save file.</summary>
private void SavePetPosition()
/// <summary>Get the character type for an NPC.</summary>
/// <param name="npc">The NPC to check.</param>
private CharacterType? GetCharacterType(NPC npc)
{
if (!this.Player.hasPet())
return;
// find pet
Pet pet = Utility.getAllCharacters().OfType<Pet>().FirstOrDefault();
if (pet == null)
return;
// get pet info
string map = pet.currentLocation.name;
Point tile = pet.getTileLocationPoint();
// save data
string path = Path.Combine(this.SaveAnimalsPath, $"Pet_Save_Info_{this.Player.name}.txt");
string[] text = new string[20];
text[0] = "Pet: Save_Anywhere Info. Editing this might break some things.";
text[1] = "====================================================================================";
text[2] = "Pet Current Map Name";
text[3] = map;
text[4] = "Pet X Position";
text[5] = tile.X.ToString();
text[6] = "Pet Y Position";
text[7] = tile.Y.ToString();
File.WriteAllLines(path, text);
if (npc is Monster)
return null;
if (npc is Horse)
return CharacterType.Horse;
if (npc is Pet)
return CharacterType.Pet;
return CharacterType.Villager;
}
/// <summary>Reset the pet to the saved state.</summary>
private void LoadPetPosition()
/// <summary>Remove legacy save data for this player.</summary>
private void RemoveLegacyDataForThisPlayer()
{
if (!this.Player.hasPet())
return;
// find pet
Pet pet = Utility.getAllCharacters().OfType<Pet>().FirstOrDefault();
if (pet == null)
return;
// get file path
string path = Path.Combine(this.SaveAnimalsPath, $"Pet_Save_Info_{this.Player.name}.txt");
if (!File.Exists(path))
return;
// read saved data
string[] text = File.ReadAllLines(path);
string map = Convert.ToString(text[3]);
int x = Convert.ToInt32(text[5]);
int y = Convert.ToInt32(text[7]);
// update pet
Game1.warpCharacter(pet, map, new Point(x, y), false, true);
}
/// <summary>Save the player state to the save file.</summary>
private void SavePlayerPosition()
{
// get player info
string map = this.Player.currentLocation.name;
Point tile = this.Player.getTileLocationPoint();
// save data
string path = Path.Combine(this.SavePath, $"Player_Save_Info_{this.Player.name}.txt");
string[] text = new string[20];
text[0] = "Player: Save_Anywhere Info. Editing this might break some things.";
text[1] = "====================================================================================";
text[2] = "Player Current Game Time";
text[3] = Game1.timeOfDay.ToString();
text[4] = "Player Current Map Name";
text[5] = map;
text[6] = "Player X Position";
text[7] = tile.X.ToString();
text[8] = "Player Y Position";
text[9] = tile.Y.ToString();
File.WriteAllLines(path, text);
}
/// <summary>Reset the player to the saved state.</summary>
private void LoadPlayerPosition()
{
// get file path
string path = Path.Combine(this.SavePath, $"Player_Save_Info_{this.Player.name}.txt");
if (!File.Exists(path))
return;
// read saved data
string[] text = File.ReadAllLines(path);
int time = Convert.ToInt32(text[3]);
string map = Convert.ToString(text[5]);
int x = Convert.ToInt32(text[7]);
int y = Convert.ToInt32(text[9]);
// update player
Game1.timeOfDay = time;
this.Player.previousLocationName = this.Player.currentLocation.name;
Game1.locationAfterWarp = Game1.getLocationFromName(map);
Game1.xLocationAfterWarp = x;
Game1.yLocationAfterWarp = y;
//Game1.facingDirectionAfterWarp = this.player_facing_direction;
Game1.fadeScreenToBlack();
Game1.warpFarmer(map, x, y, false);
//this.Player.faceDirection(this.player_facing_direction);
}
/// <summary>Get whether any data has been saved for this player yet.</summary>
private bool HasSaveData()
{
return Directory.Exists(this.SavePath);
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);
}
}
}

View File

@ -37,4 +37,12 @@ Press `K` to save anywhere. Edit `Save_Anywhere_Config.txt` to configure the key
2.5:
* Updated for SMAPI 2.0.
* Overhauled save format.
* Switched to standard JSON config file.
* Fixed crash when saving in the community center.
* Fixed crash during cutscenes.
* Fixed load warp only happening after you move.
* Fixed load not working after you exit to title.
* Fixed some old data being restored if you reload after a normal save.
* Fixed player/NPC facing directions not being restored.
* Internal refactoring.

View File

@ -15,18 +15,20 @@ namespace Omegasis.SaveAnywhere
/*********
** Properties
*********/
/// <summary>The mod configuration.</summary>
private ModConfig Config;
/// <summary>Provides methods for saving and loading game data.</summary>
private SaveManager SaveManager;
/// <summary>Provides methods for reading and writing the config file.</summary>
private ConfigUtilities ConfigUtilities;
/// <summary>The parsed schedules by NPC name.</summary>
private readonly IDictionary<string, string> NpcSchedules = new Dictionary<string, string>();
/// <summary>Whether villager schedules should be reset now.</summary>
private bool ShouldResetSchedules;
/// <summary>Whether we're performing a non-vanilla save (i.e. not by sleeping in bed).</summary>
private bool IsCustomSaving;
/*********
@ -36,9 +38,11 @@ namespace Omegasis.SaveAnywhere
/// <param name="helper">Provides simplified APIs for writing mods.</param>
public override void Entry(IModHelper helper)
{
this.ConfigUtilities = new ConfigUtilities(this.Helper.DirectoryPath);
this.Config = helper.ReadConfig<ModConfig>();
SaveEvents.AfterLoad += this.SaveEvents_AfterLoad;
SaveEvents.AfterSave += this.SaveEvents_AfterSave;
MenuEvents.MenuChanged += this.MenuEvents_MenuChanged;
ControlEvents.KeyPressed += this.ControlEvents_KeyPressed;
GameEvents.UpdateTick += this.GameEvents_UpdateTick;
TimeEvents.AfterDayStarted += this.TimeEvents_AfterDayStarted;
@ -54,15 +58,30 @@ namespace Omegasis.SaveAnywhere
private void SaveEvents_AfterLoad(object sender, EventArgs e)
{
// reset state
this.IsCustomSaving = false;
this.ShouldResetSchedules = false;
// load config
this.ConfigUtilities.LoadConfig();
this.ConfigUtilities.WriteConfig();
// load positions
this.SaveManager = new SaveManager(Game1.player, this.Helper.DirectoryPath, this.Monitor, this.Helper.Reflection, onVillagersReset: () => this.ShouldResetSchedules = true);
this.SaveManager.LoadPositions();
this.SaveManager = new SaveManager(this.Helper, this.Helper.Reflection, onLoaded: () => this.ShouldResetSchedules = true);
this.SaveManager.LoadData();
}
/// <summary>The method invoked after the player finishes saving.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event data.</param>
private void SaveEvents_AfterSave(object sender, EventArgs e)
{
// clear custom data after a normal save (to avoid restoring old state)
if (!this.IsCustomSaving)
this.SaveManager.ClearData();
}
/// <summary>The method invoked after a menu is opened or changed.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event data.</param>
private void MenuEvents_MenuChanged(object sender, EventArgsClickableMenuChanged e)
{
this.IsCustomSaving = e.NewMenu != null && (e.NewMenu is NewSaveGameMenu || e.NewMenu is NewShippingMenu);
}
/// <summary>The method invoked when the game updates (roughly 60 times per second).</summary>
@ -107,8 +126,19 @@ namespace Omegasis.SaveAnywhere
if (!Context.IsPlayerFree)
return;
if (e.KeyPressed.ToString() == this.ConfigUtilities.KeyBinding)
this.SaveManager.SaveGameAndPositions();
// initiate save (if valid context)
if (e.KeyPressed.ToString() == this.Config.SaveKey)
{
// validate: community center Junimos can't be saved
if (Utility.getAllCharacters().OfType<Junimo>().Any())
{
Game1.addHUDMessage(new HUDMessage("The spirits don't want you to save here.", HUDMessage.error_type));
return;
}
// save
this.SaveManager.BeginSaveData();
}
}
/// <summary>Apply the NPC schedules to each NPC.</summary>

View File

@ -37,8 +37,12 @@
<Compile Include="..\GlobalAssemblyInfo.cs">
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Framework\ModConfig.cs" />
<Compile Include="Framework\Models\CharacterType.cs" />
<Compile Include="Framework\Models\PositionData.cs" />
<Compile Include="Framework\NewSaveMenu.cs" />
<Compile Include="Framework\Models\PlayerData.cs" />
<Compile Include="Framework\SaveManager.cs" />
<Compile Include="Framework\ConfigUtilities.cs" />
<Compile Include="SaveAnywhere.cs" />
<Compile Include="Framework\NewShippingMenu.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />