using System; using System.Collections.Generic; using System.IO; using System.Linq; using Omegasis.HappyBirthday.Framework; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewValley; using StardewValley.Characters; using StardewValley.Monsters; using SObject = StardewValley.Object; namespace Omegasis.HappyBirthday { /// The mod entry point. public class HappyBirthday : Mod { /********* ** Properties *********/ /// The relative path for the current player's data file. private string DataFilePath => Path.Combine("data", $"{Constants.SaveFolderName}.json"); /// The absolute path for the current player's legacy data file. private string LegacyDataFilePath => Path.Combine(this.Helper.DirectoryPath, "Player_Birthdays", $"HappyBirthday_{Game1.player.name}.txt"); /// The mod configuration. private ModConfig Config; /// The data for the current player. private PlayerData PlayerData; /// Whether the player has chosen a birthday. private bool HasChosenBirthday => !string.IsNullOrEmpty(this.PlayerData.BirthdaySeason) && this.PlayerData.BirthdayDay != 0; /// The queue of villagers who haven't given a gift yet. private List VillagerQueue; /// The gifts that villagers can give. private List PossibleBirthdayGifts; /// The next birthday gift the player will receive. private Item BirthdayGiftToReceive; /// Whether we've already checked for and (if applicable) set up the player's birthday today. private bool CheckedForBirthday; //private Dictionary Dialogue; //private bool SeenEvent; /********* ** 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(); TimeEvents.AfterDayStarted += this.TimeEvents_AfterDayStarted; GameEvents.UpdateTick += this.GameEvents_UpdateTick; SaveEvents.AfterLoad += this.SaveEvents_AfterLoad; SaveEvents.BeforeSave += this.SaveEvents_BeforeSave; ControlEvents.KeyPressed += this.ControlEvents_KeyPressed; } /********* ** Private methods *********/ /// The method invoked after a new day starts. /// The event sender. /// The event data. private void TimeEvents_AfterDayStarted(object sender, EventArgs e) { this.CheckedForBirthday = false; } /// The method invoked when the presses a keyboard button. /// The event sender. /// The event data. private void ControlEvents_KeyPressed(object sender, EventArgsKeyPressed e) { // show birthday selection menu if (Context.IsPlayerFree && !this.HasChosenBirthday && e.KeyPressed.ToString() == this.Config.KeyBinding) Game1.activeClickableMenu = new BirthdayMenu(this.PlayerData.BirthdaySeason, this.PlayerData.BirthdayDay, this.SetBirthday); } /// 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.VillagerQueue = new List(); this.PossibleBirthdayGifts = new List(); this.BirthdayGiftToReceive = null; this.CheckedForBirthday = false; // load settings this.MigrateLegacyData(); this.PlayerData = this.Helper.ReadJsonFile(this.DataFilePath) ?? new PlayerData(); //this.SeenEvent = false; //this.Dialogue = new Dictionary(); } /// The method invoked just before the game updates the saves. /// The event sender. /// The event data. private void SaveEvents_BeforeSave(object sender, EventArgs e) { if (this.HasChosenBirthday) this.Helper.WriteJsonFile(this.DataFilePath, this.PlayerData); } /// 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) { if (!Context.IsWorldReady || Game1.eventUp || Game1.isFestival()) return; if (!this.CheckedForBirthday) { this.CheckedForBirthday = true; // set up birthday if (this.IsBirthday()) { Messages.ShowStarMessage("It's your birthday today! Happy birthday!"); Game1.mailbox.Enqueue("birthdayMom"); Game1.mailbox.Enqueue("birthdayDad"); try { this.ResetVillagerQueue(); } catch (Exception ex) { this.Monitor.Log(ex.ToString(), LogLevel.Error); } foreach (GameLocation location in Game1.locations) { foreach (NPC npc in location.characters) { if (npc is Child || npc is Horse || npc is Junimo || npc is Monster || npc is Pet) continue; try { Dialogue d = new Dialogue(Game1.content.Load>("Data\\FarmerBirthdayDialogue")[npc.name], npc); npc.CurrentDialogue.Push(d); if (npc.CurrentDialogue.ElementAt(0) != d) npc.setNewDialogue(Game1.content.Load>("Data\\FarmerBirthdayDialogue")[npc.name]); } catch { Dialogue d = new Dialogue("Happy Birthday @!", npc); npc.CurrentDialogue.Push(d); if (npc.CurrentDialogue.ElementAt(0) != d) npc.setNewDialogue("Happy Birthday @!"); } } } } // ask for birthday date if (!this.HasChosenBirthday) { Game1.activeClickableMenu = new BirthdayMenu(this.PlayerData.BirthdaySeason, this.PlayerData.BirthdayDay, this.SetBirthday); this.CheckedForBirthday = false; } } // unreachable since we exit early if Game1.eventUp //if (Game1.eventUp) //{ // foreach (string npcName in this.VillagerQueue) // { // NPC npc = Game1.getCharacterFromName(npcName); // try // { // this.Dialogue.Add(npcName, npc.CurrentDialogue.Pop()); // } // catch (Exception ex) // { // this.Monitor.Log(ex.ToString(), LogLevel.Error); // this.Dialogue.Add(npcName, npc.CurrentDialogue.ElementAt(0)); // npc.loadSeasonalDialogue(); // } // this.SeenEvent = true; // } //} //if (!Game1.eventUp && this.SeenEvent) //{ // foreach (KeyValuePair v in this.Dialogue) // { // NPC npc = Game1.getCharacterFromName(v.Key); // npc.CurrentDialogue.Push(v.Value); // } // this.Dialogue.Clear(); // this.SeenEvent = false; //} // set birthday gift if (Game1.currentSpeaker != null) { string name = Game1.currentSpeaker.name; if (this.IsBirthday() && this.VillagerQueue.Contains(name)) { try { this.SetNextBirthdayGift(Game1.currentSpeaker.name); this.VillagerQueue.Remove(Game1.currentSpeaker.name); } catch (Exception ex) { this.Monitor.Log(ex.ToString(), LogLevel.Error); } } } if (this.BirthdayGiftToReceive != null && Game1.currentSpeaker != null) { while (this.BirthdayGiftToReceive.Name == "Error Item" || this.BirthdayGiftToReceive.Name == "Rock" || this.BirthdayGiftToReceive.Name == "???") this.SetNextBirthdayGift(Game1.currentSpeaker.name); Game1.player.addItemByMenuIfNecessaryElseHoldUp(this.BirthdayGiftToReceive); this.BirthdayGiftToReceive = null; } } /// Set the player's birtday/ /// The birthday season. /// The birthday day. private void SetBirthday(string season, int day) { this.PlayerData.BirthdaySeason = season; this.PlayerData.BirthdayDay = day; } /// Reset the queue of villager names. private void ResetVillagerQueue() { this.VillagerQueue.Clear(); foreach (GameLocation location in Game1.locations) { foreach (NPC npc in location.characters) { if (npc is Child || npc is Horse || npc is Junimo || npc is Monster || npc is Pet) continue; if (this.VillagerQueue.Contains(npc.name)) continue; this.VillagerQueue.Add(npc.name); } } } /// Set the next birthday gift the player will receive. /// The villager's name who's giving the gift. /// This returns gifts based on the speaker's heart level towards the player: neutral for 0-3, good for 4-6, and best for 7-10. private void SetNextBirthdayGift(string name) { Item gift; if (this.PossibleBirthdayGifts.Count > 0) { Random random = new Random(); int index = random.Next(this.PossibleBirthdayGifts.Count); gift = this.PossibleBirthdayGifts[index]; if (Game1.player.isInventoryFull()) Game1.createItemDebris(gift, Game1.player.getStandingPosition(), Game1.player.getDirection()); else this.BirthdayGiftToReceive = gift; return; } this.PossibleBirthdayGifts.AddRange(this.GetDefaultBirthdayGifts(name)); Random rnd2 = new Random(); int r2 = rnd2.Next(this.PossibleBirthdayGifts.Count); gift = this.PossibleBirthdayGifts.ElementAt(r2); if (Game1.player.isInventoryFull()) Game1.createItemDebris(gift, Game1.player.getStandingPosition(), Game1.player.getDirection()); else this.BirthdayGiftToReceive = gift; this.PossibleBirthdayGifts.Clear(); } /// Get the default gift items. /// The villager's name. private IEnumerable GetDefaultBirthdayGifts(string name) { List gifts = new List(); try { // read from birthday gifts file IDictionary data = Game1.content.Load>("Data\\PossibleBirthdayGifts"); string text; data.TryGetValue(name, out text); if (text != null) { string[] fields = text.Split('/'); // love if (Game1.player.getFriendshipHeartLevelForNPC(name) >= 7) { string[] loveFields = fields[1].Split(' '); for (int i = 0; i < loveFields.Length; i += 2) { try { gifts.AddRange(this.GetItems(Convert.ToInt32(loveFields[i]), Convert.ToInt32(loveFields[i + 1]))); } catch { } } } // like if (Game1.player.getFriendshipHeartLevelForNPC(name) >= 4 && Game1.player.getFriendshipHeartLevelForNPC(name) <= 6) { string[] likeFields = fields[3].Split(' '); for (int i = 0; i < likeFields.Length; i += 2) { try { gifts.AddRange(this.GetItems(Convert.ToInt32(likeFields[i]), Convert.ToInt32(likeFields[i + 1]))); } catch { } } } // neutral if (Game1.player.getFriendshipHeartLevelForNPC(name) >= 0 && Game1.player.getFriendshipHeartLevelForNPC(name) <= 3) { string[] neutralFields = fields[5].Split(' '); for (int i = 0; i < neutralFields.Length; i += 2) { try { gifts.AddRange(this.GetItems(Convert.ToInt32(neutralFields[i]), Convert.ToInt32(neutralFields[i + 1]))); } catch { } } } } // get NPC's preferred gifts if (Game1.player.getFriendshipHeartLevelForNPC(name) >= 7) gifts.AddRange(this.GetUniversalItems("Love", true)); if (Game1.player.getFriendshipHeartLevelForNPC(name) >= 4 && Game1.player.getFriendshipHeartLevelForNPC(name) <= 6) this.PossibleBirthdayGifts.AddRange(this.GetUniversalItems("Like", true)); if (Game1.player.getFriendshipHeartLevelForNPC(name) >= 0 && Game1.player.getFriendshipHeartLevelForNPC(name) <= 3) this.PossibleBirthdayGifts.AddRange(this.GetUniversalItems("Neutral", true)); } catch { // get NPC's preferred gifts if (Game1.player.getFriendshipHeartLevelForNPC(name) >= 7) { this.PossibleBirthdayGifts.AddRange(this.GetUniversalItems("Love", false)); this.PossibleBirthdayGifts.AddRange(this.GetLovedItems(name)); } if (Game1.player.getFriendshipHeartLevelForNPC(name) >= 4 && Game1.player.getFriendshipHeartLevelForNPC(name) <= 6) { this.PossibleBirthdayGifts.AddRange(this.GetLikedItems(name)); this.PossibleBirthdayGifts.AddRange(this.GetUniversalItems("Like", false)); } if (Game1.player.getFriendshipHeartLevelForNPC(name) >= 0 && Game1.player.getFriendshipHeartLevelForNPC(name) <= 3) this.PossibleBirthdayGifts.AddRange(this.GetUniversalItems("Neutral", false)); } //TODO: Make different tiers of gifts depending on the friendship, and if it is the spouse. /* this.possible_birthday_gifts.Add((Item)new SytardewValley.Object(198, 1)); this.possible_birthday_gifts.Add((Item)new SytardewValley.Object(204, 1)); this.possible_birthday_gifts.Add((Item)new SytardewValley.Object(220, 1)); this.possible_birthday_gifts.Add((Item)new SytardewValley.Object(221, 1)); this.possible_birthday_gifts.Add((Item)new SytardewValley.Object(223, 1)); this.possible_birthday_gifts.Add((Item)new SytardewValley.Object(233, 1)); this.possible_birthday_gifts.Add((Item)new SytardewValley.Object(234, 1)); this.possible_birthday_gifts.Add((Item)new SytardewValley.Object(286, 5)); this.possible_birthday_gifts.Add((Item)new SytardewValley.Object(368, 5)); this.possible_birthday_gifts.Add((Item)new SytardewValley.Object(608, 1)); this.possible_birthday_gifts.Add((Item)new SytardewValley.Object(612, 1)); this.possible_birthday_gifts.Add((Item)new SytardewValley.Object(773, 1)); */ return gifts; } /// Get the items loved by all villagers. /// The group to get (one of Like, Love, Neutral). /// Whether to get data from Data\PossibleBirthdayGifts.xnb instead of the game data. private IEnumerable GetUniversalItems(string group, bool isBirthdayGiftList) { if (!isBirthdayGiftList) { // get raw data string text; Game1.NPCGiftTastes.TryGetValue($"Universal_{group}", out text); if (text == null) yield break; // parse string[] neutralIDs = text.Split(' '); foreach (string neutralID in neutralIDs) { foreach (SObject obj in this.GetItems(Convert.ToInt32(neutralID))) yield return obj; } } else { // get raw data Dictionary data = Game1.content.Load>("Data\\PossibleBirthdayGifts"); string text; data.TryGetValue($"Universal_{group}_Gift", out text); if (text == null) yield break; // parse string[] array = text.Split(' '); for (int i = 0; i < array.Length; i += 2) { foreach (SObject obj in this.GetItems(Convert.ToInt32(array[i]), Convert.ToInt32(array[i + 1]))) yield return obj; } } } /// Get a villager's loved items. /// The villager's name. private IEnumerable GetLikedItems(string name) { // get raw data string text; Game1.NPCGiftTastes.TryGetValue(name, out text); if (text == null) yield break; // parse string[] data = text.Split('/'); string[] likedIDs = data[3].Split(' '); foreach (string likedID in likedIDs) { foreach (SObject obj in this.GetItems(Convert.ToInt32(likedID))) yield return obj; } } /// Get a villager's loved items. /// The villager's name. private IEnumerable GetLovedItems(string name) { // get raw data string text; Game1.NPCGiftTastes.TryGetValue(name, out text); if (text == null) yield break; // parse string[] data = text.Split('/'); string[] lovedIDs = data[1].Split(' '); foreach (string lovedID in lovedIDs) { foreach (SObject obj in this.GetItems(Convert.ToInt32(lovedID))) yield return obj; } } /// Get the items matching the given ID. /// The category or item ID. private IEnumerable GetItems(int id) { return id < 0 ? ObjectUtility.GetObjectsInCategory(id) : new[] { new SObject(id, 1) }; } /// Get the items matching the given ID. /// The category or item ID. /// The stack size. private IEnumerable GetItems(int id, int stack) { foreach (SObject obj in this.GetItems(id)) yield return new SObject(obj.parentSheetIndex, stack); } /// Get whether today is the player's birthday. private bool IsBirthday() { return this.PlayerData.BirthdayDay == Game1.dayOfMonth && this.PlayerData.BirthdaySeason == Game1.currentSeason; } /// Migrate the legacy settings for the current player. private void MigrateLegacyData() { // skip if no legacy data or new data already exists try { if (!File.Exists(this.LegacyDataFilePath) || File.Exists(this.DataFilePath)) if (this.PlayerData == null) this.PlayerData = new PlayerData(); return; } catch(Exception err) { // migrate to new file try { string[] text = File.ReadAllLines(this.LegacyDataFilePath); this.Helper.WriteJsonFile(this.DataFilePath, new PlayerData { BirthdaySeason = text[3], BirthdayDay = Convert.ToInt32(text[5]) }); FileInfo file = new FileInfo(this.LegacyDataFilePath); file.Delete(); if (!file.Directory.EnumerateFiles().Any()) file.Directory.Delete(); } catch (Exception ex) { this.Monitor.Log($"Error migrating data from the legacy 'Player_Birthdays' folder for the current player. Technical details:\n {ex}", LogLevel.Error); } } } } }