using System;
using System.Collections.Generic;
using System.Linq;
using Omegasis.SaveAnywhere.API;
using Omegasis.SaveAnywhere.Framework;
using StardewModdingAPI;
using StardewModdingAPI.Events;
using StardewValley;
using StardewValley.Characters;
using StardewValley.Monsters;
namespace Omegasis.SaveAnywhere
{
/// The mod entry point.
public class SaveAnywhere : Mod
{
/*********
** Properties
*********/
/// The mod configuration.
private ModConfig Config;
/// Provides methods for saving and loading game data.
private SaveManager SaveManager;
/// The parsed schedules by NPC name.
private readonly IDictionary NpcSchedules = new Dictionary();
/// Whether villager schedules should be reset now.
private bool ShouldResetSchedules;
/// Whether we're performing a non-vanilla save (i.e. not by sleeping in bed).
private bool IsCustomSaving;
///
/// Used to access the Mod's helper from other files associated with the mod.
///
public static IModHelper ModHelper;
///
/// Used to access the Mod's monitor to allow for debug logging in other files associated with the mod.
///
public static IMonitor ModMonitor;
private List monsters;
private bool customMenuOpen;
/*********
** Public methods
*********/
/// The mod entry point, called after the mod is first loaded.
/// Provides simplified APIs for writing mods.
public override void Entry(IModHelper helper)
{
this.Config = helper.ReadConfig();
this.SaveManager = new SaveManager(this.Helper, this.Helper.Reflection, onLoaded: () => this.ShouldResetSchedules = true);
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;
ModHelper = helper;
ModMonitor = Monitor;
customMenuOpen = false;
}
///
/// Exposes the SaveAnywhere API to other SMAPI mods
///
///
public override object GetApi()
{
SaveAnywhereAPI api = new SaveAnywhereAPI(SaveManager);
return api;
}
/*********
** Private methods
*********/
/// The method invoked after the player loads a save.
/// The event sender.
/// The event data.
private void SaveEvents_AfterLoad(object sender, EventArgs e)
{
// reset state
this.IsCustomSaving = false;
this.ShouldResetSchedules = false;
// load positions
this.SaveManager.LoadData();
}
/// The method invoked after the player finishes saving.
/// The event sender.
/// The event data.
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();
}
/// The method invoked after a menu is opened or changed.
/// The event sender.
/// The event data.
private void MenuEvents_MenuChanged(object sender, EventArgsClickableMenuChanged e)
{
this.IsCustomSaving = e.NewMenu != null && (e.NewMenu is NewSaveGameMenu || e.NewMenu is NewShippingMenu);
}
/// The method invoked when the game updates (roughly 60 times per second).
/// The event sender.
/// The event data.
private void GameEvents_UpdateTick(object sender, EventArgs e)
{
// let save manager run background logic
if (Context.IsWorldReady)
{
if (Game1.player.IsMainPlayer == false) return;
this.SaveManager.Update();
}
// reset NPC schedules
if (Context.IsWorldReady && this.ShouldResetSchedules)
{
this.ShouldResetSchedules = false;
this.ApplySchedules();
}
if (Game1.activeClickableMenu == null && this.customMenuOpen == false) return;
if(Game1.activeClickableMenu==null && this.customMenuOpen == true)
{
restoreMonsters();
this.customMenuOpen = false;
return;
}
if (Game1.activeClickableMenu != null)
{
if (Game1.activeClickableMenu.GetType() == typeof(NewSaveGameMenu))
{
this.customMenuOpen = true;
}
}
}
///
/// Saves all monsters from the game world.
///
private void cleanMonsters()
{
monsters = new List();
foreach (var monster in Game1.player.currentLocation.characters)
{
try
{
if (monster is Monster)
{
monsters.Add(monster as Monster);
}
}
catch (Exception err)
{
}
}
foreach (var monster in this.monsters)
{
Game1.player.currentLocation.characters.Remove(monster);
}
}
///
/// Adds all saved monster back into the game world.
///
private void restoreMonsters()
{
foreach (var monster in this.monsters)
{
Game1.player.currentLocation.characters.Add(monster);
}
}
/// The method invoked after a new day starts.
/// The event sender.
/// The event data.
private void TimeEvents_AfterDayStarted(object sender, EventArgs e)
{
// reload NPC schedules
this.ShouldResetSchedules = true;
// update NPC schedules
this.NpcSchedules.Clear();
foreach (NPC npc in Utility.getAllCharacters())
{
if (!this.NpcSchedules.ContainsKey(npc.Name))
this.NpcSchedules.Add(npc.Name, this.ParseSchedule(npc));
}
}
/// The method invoked when the presses a keyboard button.
/// The event sender.
/// The event data.
private void ControlEvents_KeyPressed(object sender, EventArgsKeyPressed e)
{
if (!Context.IsPlayerFree)
return;
// initiate save (if valid context)
if (e.KeyPressed.ToString() == this.Config.SaveKey)
{
if (Game1.client==null)
{
cleanMonsters();
// validate: community center Junimos can't be saved
if (Game1.player.currentLocation.getCharacters().OfType().Any())
{
Game1.addHUDMessage(new HUDMessage("The spirits don't want you to save here.", HUDMessage.error_type));
return;
}
// save
this.SaveManager.BeginSaveData();
}
else
{
Game1.addHUDMessage(new HUDMessage("Only server hosts can save anywhere.",HUDMessage.error_type));
}
}
}
/// Apply the NPC schedules to each NPC.
private void ApplySchedules()
{
if (Game1.weatherIcon == Game1.weather_festival || Game1.isFestival() || Game1.eventUp)
return;
// apply for each NPC
foreach (NPC npc in Utility.getAllCharacters())
{
if (npc.DirectionsToNewLocation != null || npc.isMoving() || npc.Schedule == null || npc.controller != null || npc is Horse)
continue;
// get raw schedule from XNBs
IDictionary rawSchedule = this.GetRawSchedule(npc.Name);
if (rawSchedule == null)
continue;
// get schedule data
string scheduleData;
if (!this.NpcSchedules.TryGetValue(npc.Name, out scheduleData) || string.IsNullOrEmpty(scheduleData))
{
//this.Monitor.Log("THIS IS AWKWARD");
continue;
}
// get schedule script
string script;
if (!rawSchedule.TryGetValue(scheduleData, out script))
continue;
// parse entries
string[] entries = script.Split('/');
int index = 0;
foreach (string _ in entries)
{
string[] fields = entries[index].Split(' ');
// handle GOTO command
if (fields.Contains("GOTO"))
{
for (int i = 0; i < fields.Length; i++)
{
string s = fields[i];
if (s == "GOTO")
{
rawSchedule.TryGetValue(fields[i + 1], out script);
string[] newEntries = script.Split('/');
fields = newEntries[0].Split(' ');
}
}
}
// parse schedule script
SchedulePathDescription schedulePathDescription;
try
{
if (Convert.ToInt32(fields[0]) > Game1.timeOfDay) break;
string endMap = Convert.ToString(fields[1]);
int x = Convert.ToInt32(fields[2]);
int y = Convert.ToInt32(fields[3]);
int endFacingDir = Convert.ToInt32(fields[4]);
schedulePathDescription = this.Helper.Reflection
.GetMethod(npc, "pathfindToNextScheduleLocation")
.Invoke(npc.currentLocation.Name, npc.getTileX(), npc.getTileY(), endMap, x, y, endFacingDir, null, null);
index++;
}
catch (Exception ex)
{
ex.ToString();
//this.Monitor.Log($"Error pathfinding NPC {npc.name}: {ex}", LogLevel.Error);
continue;
}
npc.DirectionsToNewLocation = schedulePathDescription;
npc.controller = new PathFindController(npc.DirectionsToNewLocation.route, npc, Utility.getGameLocationOfCharacter(npc))
{
finalFacingDirection = npc.DirectionsToNewLocation.facingDirection,
endBehaviorFunction = null
};
}
}
}
/// Get an NPC's raw schedule data from the XNB files.
/// The NPC name whose schedules to read.
/// Returns the NPC schedule if found, else null.
private IDictionary GetRawSchedule(string npcName)
{
try
{
return Game1.content.Load>($"Characters\\schedules\\{npcName}");
}
catch (Exception)
{
return null;
}
}
/// Load the raw schedule data for an NPC.
/// The NPC whose schedule to read.
private string ParseSchedule(NPC npc)
{
// set flags
if (npc.Name.Equals("Robin") || Game1.player.currentUpgrade != null)
npc.IsInvisible = false;
if (npc.Name.Equals("Willy") && Game1.stats.DaysPlayed < 2u)
npc.IsInvisible = true;
else if (npc.Schedule != null)
npc.followSchedule = true;
// read schedule data
IDictionary schedule = this.GetRawSchedule(npc.Name);
if (schedule == null)
return "";
// do stuff
if (npc.isMarried())
{
string dayName = Game1.shortDayNameFromDayOfSeason(Game1.dayOfMonth);
if ((npc.Name.Equals("Penny") && (dayName.Equals("Tue") || dayName.Equals("Wed") || dayName.Equals("Fri"))) || (npc.Name.Equals("Maru") && (dayName.Equals("Tue") || dayName.Equals("Thu"))) || (npc.Name.Equals("Harvey") && (dayName.Equals("Tue") || dayName.Equals("Thu"))))
{
this.Helper.Reflection
.GetField(npc, "nameofTodaysSchedule")
.SetValue("marriageJob");
return "marriageJob";
}
if (!Game1.isRaining && schedule.ContainsKey("marriage_" + Game1.shortDayNameFromDayOfSeason(Game1.dayOfMonth)))
{
this.Helper.Reflection
.GetField(npc, "nameofTodaysSchedule")
.SetValue("marriage_" + Game1.shortDayNameFromDayOfSeason(Game1.dayOfMonth));
return "marriage_" + Game1.shortDayNameFromDayOfSeason(Game1.dayOfMonth);
}
npc.followSchedule = false;
return null;
}
else
{
if (schedule.ContainsKey(Game1.currentSeason + "_" + Game1.dayOfMonth))
return Game1.currentSeason + "_" + Game1.dayOfMonth;
int i;
Friendship f;
Game1.player.friendshipData.TryGetValue(npc.Name, out f);
for (i = (Game1.player.friendshipData.ContainsKey(npc.Name) ? (f.Points/ 250) : -1); i > 0; i--)
{
if (schedule.ContainsKey(Game1.dayOfMonth + "_" + i))
return Game1.dayOfMonth + "_" + i;
}
if (schedule.ContainsKey(string.Empty + Game1.dayOfMonth))
return string.Empty + Game1.dayOfMonth;
if (npc.Name.Equals("Pam") && Game1.player.mailReceived.Contains("ccVault"))
return "bus";
if (Game1.isRaining)
{
if (Game1.random.NextDouble() < 0.5 && schedule.ContainsKey("rain2"))
return "rain2";
if (schedule.ContainsKey("rain"))
return "rain";
}
List list = new List { Game1.currentSeason, Game1.shortDayNameFromDayOfSeason(Game1.dayOfMonth) };
Friendship friendship;
Game1.player.friendshipData.TryGetValue(npc.Name, out friendship);
i = (Game1.player.friendshipData.ContainsKey(npc.Name) ? (friendship.Points / 250) : -1);
while (i > 0)
{
list.Add(string.Empty + i);
if (schedule.ContainsKey(string.Join("_", list)))
{
return string.Join("_", list);
}
i--;
list.RemoveAt(list.Count - 1);
}
if (schedule.ContainsKey(string.Join("_", list)))
{
return string.Join("_", list);
}
if (schedule.ContainsKey(Game1.shortDayNameFromDayOfSeason(Game1.dayOfMonth)))
{
return Game1.shortDayNameFromDayOfSeason(Game1.dayOfMonth);
}
if (schedule.ContainsKey(Game1.currentSeason))
{
return Game1.currentSeason;
}
if (schedule.ContainsKey("spring_" + Game1.shortDayNameFromDayOfSeason(Game1.dayOfMonth)))
{
return "spring_" + Game1.shortDayNameFromDayOfSeason(Game1.dayOfMonth);
}
list.RemoveAt(list.Count - 1);
list.Add("spring");
Friendship friendship2;
Game1.player.friendshipData.TryGetValue(npc.Name, out friendship2);
i = (Game1.player.friendshipData.ContainsKey(npc.Name) ? (friendship2.Points / 250) : -1);
while (i > 0)
{
list.Add(string.Empty + i);
if (schedule.ContainsKey(string.Join("_", list)))
return string.Join("_", list);
i--;
list.RemoveAt(list.Count - 1);
}
if (schedule.ContainsKey("spring"))
return "spring";
return null;
}
}
}
}