diff --git a/SpecialOrdersExtended/DataModels/DialogueLog.cs b/SpecialOrdersExtended/DataModels/DialogueLog.cs index 4387687..da2a49d 100644 --- a/SpecialOrdersExtended/DataModels/DialogueLog.cs +++ b/SpecialOrdersExtended/DataModels/DialogueLog.cs @@ -20,9 +20,10 @@ public DialogueLog(string savefile, long multiplayerID) : base(savefile) => this.multiplayerID = multiplayerID; /// - /// Gets or sets backing field that contains all the SeenDialogues. + /// Gets backing field that contains all the SeenDialogues. /// - public Dictionary> SeenDialogues { get; set; } = new(); + /// Avoid using this directly; use the TryAdd/TryRemove/Contains methods instead if possible. + public Dictionary> SeenDialogues { get; private set; } = new(); /// /// Loads a dialogueLog. @@ -38,7 +39,7 @@ public static DialogueLog Load(long multiplayerID) } DialogueLog log = ModEntry.DataHelper.ReadGlobalData($"{Constants.SaveFolderName}{IDENTIFIER}{multiplayerID:X8}") ?? new DialogueLog(Constants.SaveFolderName, multiplayerID); - log.multiplayerID = multiplayerID; + log.multiplayerID = multiplayerID; // fix the multiplayer ID since ReadGlobalData will use the default zero-parameter constructor. return log; } @@ -87,7 +88,6 @@ public static DialogueLog LoadTempIfAvailable(long multiplayerID) /// Exact dialogue key. /// Which character to check. /// True if found, false otheerwise. - [Pure] public bool Contains(string dialoguekey, string characterName) { if (this.SeenDialogues.TryGetValue(dialoguekey, out List? characterList)) @@ -139,7 +139,6 @@ public bool TryRemove(string dialoguekey, string characterName) } /// - [Pure] public override string ToString() { StringBuilder stringBuilder = new(); diff --git a/SpecialOrdersExtended/DataModels/RecentCompletedSO.cs b/SpecialOrdersExtended/DataModels/RecentCompletedSO.cs index 18fa2f6..aa93266 100644 --- a/SpecialOrdersExtended/DataModels/RecentCompletedSO.cs +++ b/SpecialOrdersExtended/DataModels/RecentCompletedSO.cs @@ -118,7 +118,6 @@ public List dayUpdate(uint daysPlayed) /// Days to check. /// True if order found and completed in the last X days, false otherwise. /// Orders are removed from the list after seven days. - [Pure] public bool IsWithinXDays(string orderKey, uint days) { if (this.RecentOrdersCompleted.TryGetValue(orderKey, out uint dayCompleted)) @@ -133,7 +132,6 @@ public bool IsWithinXDays(string orderKey, uint days) /// /// Number of days to look at. /// IEnumerable of keys within the given timeframe. - [Pure] public IEnumerable GetKeys(uint days) { return this.RecentOrdersCompleted.Keys @@ -141,7 +139,6 @@ public IEnumerable GetKeys(uint days) } /// - [Pure] public override string ToString() { StringBuilder stringBuilder = new(); diff --git a/SpecialOrdersExtended/DialogueManager.cs b/SpecialOrdersExtended/DialogueManager.cs index f77cad2..3aca291 100644 --- a/SpecialOrdersExtended/DialogueManager.cs +++ b/SpecialOrdersExtended/DialogueManager.cs @@ -193,7 +193,6 @@ public static void ConsoleSpecialOrderDialogue(string command, string[] args) /// Name of character. /// True if key has been said, false otherwise. /// Save is not loaded. - [Pure] public static bool HasSeenDialogue(string key, string characterName) { if (!Context.IsWorldReady) @@ -359,10 +358,12 @@ public static void PushPossibleDelayedDialogues() { if (result.PushIfPastTime(Game1.timeOfDay)) { + // Successfully pushed, remove from queue. _ = DelayedDialogues.Value.Dequeue(); } else { + // Everyone else should be behind me in time, so skip to next timeslot. return; } } @@ -381,13 +382,17 @@ private static bool PushAndSaveDialogue(string dialogueKey, NPC npc) {// I have already said this dialogue return false; } + + // Empty NPC's current dialogue stack and keep it in a queue for now. while (npc.CurrentDialogue.TryPop(out Dialogue? result)) { DelayedDialogues.Value.Enqueue(new DelayedDialogue( - time: Game1.timeOfDay + 100, + time: Game1.timeOfDay + 100, // delay by one hour. npc: npc, dialogue: result)); } + + // Push my dialogue onto their stack. npc.CurrentDialogue.Push(new Dialogue(npc.Dialogue[dialogueKey], npc) { removeOnNextMove = true }); if (ModEntry.Config.Verbose) { @@ -434,7 +439,10 @@ private static bool FindBestDialogue(string baseKey, NPC npc, int hearts) } } - ModEntry.ModMonitor.DebugLog(I18n.Dialogue_NoKey(baseKey, npc.Name), LogLevel.Trace); + if (ModEntry.Config.Verbose) + { + ModEntry.ModMonitor.Log(I18n.Dialogue_NoKey(baseKey, npc.Name), LogLevel.Trace); + } return false; } } diff --git a/SpecialOrdersExtended/ModEntry.cs b/SpecialOrdersExtended/ModEntry.cs index 65e5109..4843f5f 100644 --- a/SpecialOrdersExtended/ModEntry.cs +++ b/SpecialOrdersExtended/ModEntry.cs @@ -92,6 +92,7 @@ public override void Entry(IModHelper helper) ModMonitor.Log($"Failed to patch NPC::checkForNewCurrentDialogue for Special Orders Dialogue. Dialogue will be disabled\n\n{ex}", LogLevel.Error); } + // Register console commands. helper.ConsoleCommands.Add( name: "special_order_pool", documentation: I18n.SpecialOrderPool_Description(), @@ -108,6 +109,8 @@ public override void Entry(IModHelper helper) name: "special_orders_dialogue", documentation: $"{I18n.SpecialOrdersDialogue_Description()}\n\n{I18n.SpecialOrdersDialogue_Example()}\n {I18n.SpecialOrdersDialogue_Usage()}\n {I18n.SpecialOrdersDialogue_Save()}", callback: DialogueManager.ConsoleSpecialOrderDialogue); + + // Register event handlers. helper.Events.GameLoop.GameLaunched += this.OnGameLaunched; helper.Events.GameLoop.SaveLoaded += this.SaveLoaded; helper.Events.GameLoop.Saving += this.Saving; @@ -115,16 +118,34 @@ public override void Entry(IModHelper helper) helper.Events.GameLoop.OneSecondUpdateTicking += this.OneSecondUpdateTicking; } + /// + /// Raised every 10 in game minutes. + /// + /// Unknown, used by SMAPI. + /// TimeChanged params. + /// Currently handles: pushing delayed dialogue back onto the stack. private void OnTimeChanged(object? sender, TimeChangedEventArgs e) { DialogueManager.PushPossibleDelayedDialogues(); } + /// + /// Raised every second. + /// + /// Unknown, used by SMAPI. + /// OneSecondUpdate params. + /// Currently handles: grabbing new recently completed special orders. private void OneSecondUpdateTicking(object? sender, OneSecondUpdateTickingEventArgs e) { RecentSOManager.GrabNewRecentlyCompletedOrders(); } + /// + /// Raised on game launch. + /// + /// Unknown, used by SMAPI. + /// Game Launched arguments. + /// Used to bind APIs and register CP tokens. private void OnGameLaunched(object? sender, GameLaunchedEventArgs e) { // Bind Spacecore API @@ -148,6 +169,12 @@ private void OnGameLaunched(object? sender, GameLaunchedEventArgs e) api.RegisterToken(this.ModManifest, "RecentCompleted", new Tokens.RecentCompletedSO()); } + /// + /// Raised right before the game is saved. + /// + /// Unknown, used by SMAPI. + /// Event arguments. + /// Used to handle day-end events. private void Saving(object? sender, SavingEventArgs e) { this.Monitor.DebugLog("Event Saving raised"); @@ -167,6 +194,12 @@ private void Saving(object? sender, SavingEventArgs e) RecentSOManager.Save(); } + /// + /// Raised when save is loaded. + /// + /// Unknown, used by SMAPI. + /// Parameters. + /// Used to load in this mod's data models. private void SaveLoaded(object? sender, SaveLoadedEventArgs e) { this.Monitor.DebugLog("Event SaveLoaded raised"); @@ -174,6 +207,11 @@ private void SaveLoaded(object? sender, SaveLoadedEventArgs e) RecentSOManager.Load(); } + /// + /// Console commands to check the value of a tag. + /// + /// Name of the command. + /// List of tags to check. private void ConsoleCheckTag(string command, string[] args) { if (!Context.IsWorldReady) @@ -199,6 +237,11 @@ private void ConsoleCheckTag(string command, string[] args) } } + /// + /// Console command to get all available orders. + /// + /// Name of command. + /// Arguments for command. private void GetAvailableOrders(string command, string[] args) { if (!Context.IsWorldReady) diff --git a/SpecialOrdersExtended/RecentSOManager.cs b/SpecialOrdersExtended/RecentSOManager.cs index 3f7ef54..a2cbfa9 100644 --- a/SpecialOrdersExtended/RecentSOManager.cs +++ b/SpecialOrdersExtended/RecentSOManager.cs @@ -51,7 +51,6 @@ public static void SaveTemp() /// /// current number of days played. /// IEnumerable of keys within the given timeframe. May return null. - [Pure] public static IEnumerable? GetKeys(uint days) => recentCompletedSO?.GetKeys(days); /// @@ -177,7 +176,6 @@ public static bool TryRemove(string questkey) /// True if questkey found and was completed in X days, false otherwise. /// Save not loaded. /// The data model will delete any entries older than 7 days, so beyond that it just won't know. - [Pure] public static bool IsWithinXDays(string questkey, uint days) { if (!Context.IsWorldReady) diff --git a/SpecialOrdersExtended/Tokens/AbstractToken.cs b/SpecialOrdersExtended/Tokens/AbstractToken.cs index d68b22e..d316495 100644 --- a/SpecialOrdersExtended/Tokens/AbstractToken.cs +++ b/SpecialOrdersExtended/Tokens/AbstractToken.cs @@ -16,7 +16,6 @@ internal abstract class AbstractToken /// Whether or not the token allows input. Default, true. /// /// true - all derived tokens should allow input. - [Pure] public virtual bool AllowsInput() => false; /// @@ -24,12 +23,10 @@ internal abstract class AbstractToken /// /// Input to token. /// False (no need for my own inputs). - [Pure] public virtual bool CanHaveMultipleValues(string? input = null) => true; /// Get whether the token is available for use. /// True if token ready, false otherwise. - [Pure] public virtual bool IsReady() => this.tokenCache is not null; /// Validate that the provided input arguments are valid. diff --git a/SpecialOrdersExtended/docs/CHANGELOG.MD b/SpecialOrdersExtended/docs/CHANGELOG.MD index e355dde..1195163 100644 --- a/SpecialOrdersExtended/docs/CHANGELOG.MD +++ b/SpecialOrdersExtended/docs/CHANGELOG.MD @@ -3,6 +3,7 @@ Changelog 1. Add condition to just randomly reduce the chance of a quest appearing. (May need integrate with that one mod that changes the day special orders appear?) 2. Add condition for total number of quests completed, per board. + #### Stardew 1.6 todo @@ -17,7 +18,8 @@ Changelog #### Version 1.0.6 * Add verbosity config option. Intended mostly for mod authors. -* `week_X` now handles extended months (in theory.) +* Special Order Dialogue will now cause the NPC's original dialogue to be delayed by one in-game hour. (Previously, dialogue from this mod would just be added on top of the dialogue stack.) +* `week_X` now handles extended months. * Add `profession` tag. Makes `skilllevel` tag take into account Spacecore skills, if that's installed (in theory. Memo to self: test that). * For the `_IsAvailable` dialogue series, remove from possible pool if quest is already accepted. * Fixes splitscreen, but for real this time. diff --git a/SpecialOrdersExtended/docs/DialogueKeys.MD b/SpecialOrdersExtended/docs/DialogueKeys.MD index cb9c8cd..6317745 100644 --- a/SpecialOrdersExtended/docs/DialogueKeys.MD +++ b/SpecialOrdersExtended/docs/DialogueKeys.MD @@ -5,7 +5,7 @@ New in 1.0.2, there's dialogue keys. Special Order Dialogue Keys will only ever Quest key is the key for the quest (as it is in the `SpecialOrders` file). Quest status is either `Completed` or `InProgress`. (The status `Failed` is technically also available, but I don't think you can actually talk to a NPC in that time.) `Completed` dialogue will persist to the end of the day after quest completion. Additionally, `IsAvailable` will reference quests currently available on the Quests board, and `RepeatOrder` can be used when the quest has been completed at least once before. `RepeatOrder` keys will also be cleared seven days after the successful completion of a quest. -Preface/predicate limits when the key can be used, and follows a similar structure as to vanilla's location keys. can be any season, and season-specific keys will always take precedence. Predicate can be any of the following values, in priority order: +Preface/predicate limits when the key can be used, and follows a similar structure as to vanilla's location keys. `` can be any season, and season-specific keys will always take precedence. Predicate can be any of the following values, in priority order: 1. `_` 2. `` (where valid heart levels are even numbers, checked counting down - ie 14,12,10,8,6,4,2 in vanilla) @@ -15,6 +15,8 @@ For example: if I have a quest key that's `atravita.ValleyFlowers` and I'm writi If I wanted a bit of dialogue to be said while the quest is in progress, I would use `atravita.ValleyFlowers_InProgress`, which will only ever be said once while the quest is in progress. On the other hand, `atravita.ValleyFlowers_RepeatOrder` can only be said while the quest is in progress AND has been completed at least once before. +The NPC's original dialogue is held in a queue and re-added after an in-game hour. + ### Storage To prevent dialogue keys from this mod from becoming too spammy, they're only ever said once (except for `RepeatOrder` keys, but those have a cooldown period of seven days after the completion of the Special Order). As of 1.0.4+, previously seen keys are stored in the global mod data, locally (`.smapi/mod-data/atravita.specialordersextended`). The console command `special_orders_dialogue` is included to look at and and/remove from this data. diff --git a/SpecialOrdersExtended/manifest.json b/SpecialOrdersExtended/manifest.json index 833ad0f..baa3be9 100644 --- a/SpecialOrdersExtended/manifest.json +++ b/SpecialOrdersExtended/manifest.json @@ -2,7 +2,7 @@ "$schema": "https://smapi.io/schemas/manifest.json", "Name": "Special Orders Tags Extended", "Author": "atravita", - "Version": "1.0.6-beta", + "Version": "1.0.6", "Description": "Extends the number of tags available for special orders", "UniqueID": "atravita.SpecialOrdersExtended", "EntryDll": "SpecialOrdersExtended.dll",