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; } /*Notes. Mods that want to support save anywhere will get the api for Save anywhere and then add their clean up code to the events that happen for Before/After Save and Loading. Example with pseudo code. SaveAnywhere.api.BeforeSave+=StardustCore.Objects.CleanUpBeforeSave; We then can use function wrapping (is that what it's called?) to just handle calling the actual function that deals with clean-up code. */ /********* ** 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; } } } }