From 162769c4b03867dbbd2a6be5874a217ca9348281 Mon Sep 17 00:00:00 2001 From: Laura Verdoes <1766841+Vivelin@users.noreply.github.com> Date: Sun, 20 Mar 2022 11:55:41 +0100 Subject: [PATCH 1/3] Reworked clear dungeon to always clear the dungeon and mentions what locations might need to be double-checked --- .../Configuration/DungeonInfo.cs | 2 +- .../Configuration/ResponseConfig.cs | 44 ++++++++-- src/Randomizer.SMZ3.Tracking/Tracker.cs | 88 ++++++++++++------- .../VoiceCommands/LocationTrackingModule.cs | 15 +++- .../VoiceCommands/TrackerModule.cs | 5 +- src/Randomizer.SMZ3.Tracking/tracker.json | 23 +++-- src/Randomizer.Shared/EnumerableExtensions.cs | 18 ++++ .../Randomizer.Shared.csproj | 1 + 8 files changed, 151 insertions(+), 45 deletions(-) diff --git a/src/Randomizer.SMZ3.Tracking/Configuration/DungeonInfo.cs b/src/Randomizer.SMZ3.Tracking/Configuration/DungeonInfo.cs index 28b2b3aec..35c4933bb 100644 --- a/src/Randomizer.SMZ3.Tracking/Configuration/DungeonInfo.cs +++ b/src/Randomizer.SMZ3.Tracking/Configuration/DungeonInfo.cs @@ -222,7 +222,7 @@ public bool IsInRegion(Region region) public IReadOnlyCollection GetLocations(World world) { var region = GetRegion(world); - return region.Locations.ToImmutableList(); + return region.Locations.Where(x => x.Type != LocationType.NotInDungeon).ToImmutableList(); } } } diff --git a/src/Randomizer.SMZ3.Tracking/Configuration/ResponseConfig.cs b/src/Randomizer.SMZ3.Tracking/Configuration/ResponseConfig.cs index 1ef7731b1..074ed0f07 100644 --- a/src/Randomizer.SMZ3.Tracking/Configuration/ResponseConfig.cs +++ b/src/Randomizer.SMZ3.Tracking/Configuration/ResponseConfig.cs @@ -14,7 +14,8 @@ public class ResponseConfig public SchrodingersString? StartedTracking { get; init; } /// - /// Gets the phrases to respond with when tracker starts in "alternate" mode. + /// Gets the phrases to respond with when tracker starts in "alternate" + /// mode. /// public SchrodingersString? StartingTrackingAlternate { get; init; } @@ -290,6 +291,39 @@ public class ResponseConfig public SchrodingersString NoRemainingDungeons { get; init; } = new SchrodingersString("You already marked every dungeon."); + /// + /// Gets the phrases to respond with when clearing all locations in a + /// dungeon. + /// + /// + /// {0} is a placeholder for the name of the dungeon. + /// + public SchrodingersString DungeonCleared { get; init; } + = new("Cleared everything in {0}."); + + /// + /// Gets the phrases to respond with when clearing all location in a + /// dungeon, but all locations are already cleared. + /// + /// + /// {0} is a placeholder for the name of the dungeon. + /// + public SchrodingersString DungeonAlreadyCleared { get; init; } + = new("But you already got everything in {0}."); + + /// + /// Gets the phrases to respond with when clearing all locations in a + /// dungeon, but some of the cleared locations were out of logic. + /// + /// + /// {0} is a placeholder for the name of the dungeon. {1} + /// is a placeholder for the name of a location that was missed. + /// {2} is a placeholder for the items that are required for a + /// missed location. + /// + public SchrodingersString DungeonClearedWithInaccessibleItems { get; init; } + = new("Including some out of logic checks that require {2}, such as {1}."); + /// /// Gets the phrases to respond with when clearing a dungeon. /// @@ -297,7 +331,7 @@ public class ResponseConfig /// {0} is a placeholder for the name of the dungeon that was /// cleared. {1} is a placeholder for the boss of the dungeon. /// - public SchrodingersString DungeonCleared { get; init; } + public SchrodingersString DungeonBossCleared { get; init; } = new SchrodingersString("Cleared {0}.", "Marked {1} as defeated."); /// @@ -308,7 +342,7 @@ public class ResponseConfig /// {0} is a placeholder for the name of the dungeon that was /// cleared. {1} is a placeholder for the boss of the dungeon. /// - public SchrodingersString DungeonAlreadyCleared { get; init; } + public SchrodingersString DungeonBossAlreadyCleared { get; init; } = new SchrodingersString("You already cleared {0}.", "You already defeated {1}."); /// @@ -319,7 +353,7 @@ public class ResponseConfig /// {0} is a placeholder for the name of the dungeon. {1} /// is a placeholder for the boss of the dungeon. /// - public SchrodingersString DungeonUncleared { get; init; } + public SchrodingersString DungeonBossUncleared { get; init; } = new SchrodingersString("Reset {0}.", "Marked {1} as still alive."); /// @@ -330,7 +364,7 @@ public class ResponseConfig /// {0} is a placeholder for the name of the dungeon. {1} /// is a placeholder for the boss of the dungeon. /// - public SchrodingersString DungeonNotYetCleared { get; init; } + public SchrodingersString DungeonBossNotYetCleared { get; init; } = new SchrodingersString("You haven't cleared {0} yet.", "You never defeated {1} in the first place."); /// diff --git a/src/Randomizer.SMZ3.Tracking/Tracker.cs b/src/Randomizer.SMZ3.Tracking/Tracker.cs index f78a69957..dff9d8ec5 100644 --- a/src/Randomizer.SMZ3.Tracking/Tracker.cs +++ b/src/Randomizer.SMZ3.Tracking/Tracker.cs @@ -1381,17 +1381,8 @@ public void TrackItemAmount(ItemData item, int count, float confidence) /// public void ClearArea(IHasLocations area, bool trackItems, bool includeUnavailable = false, float? confidence = null, bool assumeKeys = false) { - Action? undoTrackDungeon = null; - - var dungeon = WorldInfo.Dungeons.SingleOrDefault(x => area is Region region && x.Is(region)); - if (dungeon != null) - { - assumeKeys = true; // Always assume keys when clearing the dungeon itself - } - var locations = area.Locations .Where(x => !x.Cleared) - .WhereIf(dungeon != null, x => x.Type != LocationType.NotInDungeon) .WhereUnless(includeUnavailable, x => x.IsAvailable(GetProgression(assumeKeys))) .ToImmutableList(); @@ -1401,7 +1392,6 @@ public void ClearArea(IHasLocations area, bool trackItems, bool includeUnavailab { var outOfLogicLocations = area.Locations .Where(x => !x.Cleared) - .WhereIf(dungeon != null, x => x.Type != LocationType.NotInDungeon) .Count(); if (outOfLogicLocations > 1) @@ -1413,12 +1403,6 @@ public void ClearArea(IHasLocations area, bool trackItems, bool includeUnavailab } else { - if (dungeon != null) - { - dungeon.Cleared = true; - OnDungeonUpdated(new(confidence)); - } - // If there is only one (available) item here, just call the // regular TrackItem instead var onlyLocation = locations.TrySingle(); @@ -1502,12 +1486,6 @@ public void ClearArea(IHasLocations area, bool trackItems, bool includeUnavailab { Say(x => x.ClearedMultipleItems, itemsCleared, area.GetName()); } - - if (dungeon != null && treasureTracked > 0) - { - TrackDungeonTreasure(dungeon, amount: treasureTracked); - undoTrackDungeon = _undoHistory.Pop(); - } } OnItemTracked(new ItemTrackedEventArgs(null, confidence)); } @@ -1516,9 +1494,6 @@ public void ClearArea(IHasLocations area, bool trackItems, bool includeUnavailab AddUndo(() => { - if (dungeon != null) - dungeon.Cleared = false; - foreach (var location in locations) { if (trackItems) @@ -1529,11 +1504,64 @@ public void ClearArea(IHasLocations area, bool trackItems, bool includeUnavailab } location.Cleared = false; } - undoTrackDungeon?.Invoke(); UpdateTrackerProgression = true; }); } + /// + /// Marks all locations and treasure within a dungeon as cleared. + /// + /// The dungeon to clear. + /// The speech recognition confidence. + public void ClearDungeon(DungeonInfo dungeon, float? confidence = null) + { + var remaining = dungeon.TreasureRemaining; + if (remaining > 0) + { + dungeon.TreasureRemaining = 0; + dungeon.Cleared = true; + } + + var progress = GetProgression(); + var locations = dungeon.GetLocations(World).Where(x => !x.Cleared).ToList(); + var inaccessibleLocations = locations.Where(x => !x.IsAvailable(progress)).ToList(); + if (locations.Count > 0) + { + locations.ForEach(x => x.Cleared = true); + } + + if (remaining > 0 || locations.Count > 0) + { + Say(x => x.DungeonCleared, dungeon.Name); + if (inaccessibleLocations.Count > 0) + { + var anyMissedLocation = inaccessibleLocations.Random(s_random); + var locationInfo = WorldInfo.Location(anyMissedLocation); + var missingItems = Logic.GetMissingRequiredItems(anyMissedLocation, progress) + .Random(s_random) + .Select(FindItemByType) + .NonNull(); + var missingItemsText = NaturalLanguage.Join(missingItems, World.Config); + + Say(x => x.DungeonClearedWithInaccessibleItems, dungeon.Name, locationInfo.Name, missingItemsText); + } + } + else + { + Say(x => x.DungeonAlreadyCleared, dungeon.Name); + } + + OnDungeonUpdated(new(confidence)); + + AddUndo(() => + { + dungeon.TreasureRemaining = remaining; + if (remaining > 0) + dungeon.Cleared = false; + locations.ForEach(x => x.Cleared = false); + }); + } + /// /// Clears an item from the specified location. /// @@ -1595,12 +1623,12 @@ public void MarkDungeonAsCleared(DungeonInfo dungeon, float? confidence = null) if (dungeon.Cleared) { - Say(Responses.DungeonAlreadyCleared.Format(dungeon.Name, dungeon.Boss)); + Say(Responses.DungeonBossAlreadyCleared.Format(dungeon.Name, dungeon.Boss)); return; } dungeon.Cleared = true; - Say(Responses.DungeonCleared.Format(dungeon.Name, dungeon.Boss)); + Say(Responses.DungeonBossCleared.Format(dungeon.Name, dungeon.Boss)); // Try to track the associated boss reward item Action? undoTrack = null; @@ -1697,13 +1725,13 @@ public void MarkDungeonAsIncomplete(DungeonInfo dungeon, float? confidence = nul { if (!dungeon.Cleared) { - Say(Responses.DungeonNotYetCleared.Format(dungeon.Name, dungeon.Boss)); + Say(Responses.DungeonBossNotYetCleared.Format(dungeon.Name, dungeon.Boss)); return; } UpdateTrackerProgression = true; dungeon.Cleared = false; - Say(Responses.DungeonUncleared.Format(dungeon.Name, dungeon.Boss)); + Say(Responses.DungeonBossUncleared.Format(dungeon.Name, dungeon.Boss)); // Try to untrack the associated boss reward item Action? undoUnclear = null; diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/LocationTrackingModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/LocationTrackingModule.cs index 62307c4e3..ef3ce2622 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/LocationTrackingModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/LocationTrackingModule.cs @@ -40,6 +40,11 @@ public LocationTrackingModule(Tracker tracker, ILogger l includeUnavailable: false, confidence: result.Confidence); } + else if (result.Semantics.ContainsKey(DungeonKey)) + { + var dungeon = GetDungeonFromResult(tracker, result); + tracker.ClearDungeon(dungeon, result.Confidence); + } else if (result.Semantics.ContainsKey(RegionKey)) { var region = GetRegionFromResult(tracker, result); @@ -106,8 +111,14 @@ private GrammarBuilder GetClearLocationRule() private GrammarBuilder GetClearAreaRule() { + var dungeonNames = GetDungeonNames(includeDungeonsWithoutReward: true); var roomNames = GetRoomNames(); - var regionNames = GetRegionNames(); + var regionNames = GetRegionNames(excludeDungeons: true); + + var clearDungeon = new GrammarBuilder() + .Append("Hey tracker,") + .OneOf("clear", "please clear") + .Append(DungeonKey, dungeonNames); var clearRoom = new GrammarBuilder() .Append("Hey tracker,") @@ -119,7 +130,7 @@ private GrammarBuilder GetClearAreaRule() .OneOf("clear", "please clear") .Append(RegionKey, regionNames); - return GrammarBuilder.Combine(clearRoom, clearRegion); + return GrammarBuilder.Combine(clearDungeon, clearRoom, clearRegion); } } } diff --git a/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModule.cs b/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModule.cs index 7eae154a9..e6b81078f 100644 --- a/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModule.cs +++ b/src/Randomizer.SMZ3.Tracking/VoiceCommands/TrackerModule.cs @@ -452,12 +452,15 @@ protected virtual Choices GetRoomNames() /// A new object representing all possible region /// names mapped to the primary region name. /// - protected virtual Choices GetRegionNames() + protected virtual Choices GetRegionNames(bool excludeDungeons = false) { var regionNames = new Choices(); foreach (var region in Tracker.WorldInfo.Regions) { + if (excludeDungeons && Tracker.WorldInfo.Dungeon(region.TypeName) != null) + continue; + foreach (var name in region.Name) regionNames.Add(new SemanticResultValue(name.Text, region.TypeName)); } diff --git a/src/Randomizer.SMZ3.Tracking/tracker.json b/src/Randomizer.SMZ3.Tracking/tracker.json index 1c8d9b4ea..3b7c71c76 100644 --- a/src/Randomizer.SMZ3.Tracking/tracker.json +++ b/src/Randomizer.SMZ3.Tracking/tracker.json @@ -1352,23 +1352,34 @@ "You did that already." ], "DungeonCleared": [ + "Cleared all items from {0}", + "Marked {0} as all clear" + ], + "DungeonAlreadyCleared": [ + "But you already cleared {0}.", + "But there's nothing left in {0}." + ], + "DungeonClearedWithInaccessibleItems": [ + "Including some out of logic checks that need {2}, like {1}.", + "Including some inaccessible areas that require {2}, such as {1}.", + "Also cleared some checks that require {2}. You might want to double-check {1}.", + "You might want to double-check {1} because it requires {2}." + ], + "DungeonBossCleared": [ "Marked {1} as defeated.", "Congratulations on beating off {1}.", "RIP {1}", [ "f", 0.05 ] ], - "DungeonAlreadyCleared": [ - "But you already cleared {0}.", + "DungeonBossAlreadyCleared": [ "But you already defeated {1}.", "But {1} is already dead." ], - "DungeonUncleared": [ - "Marked {0} as incomplete.", + "DungeonBossUncleared": [ "Marked {1} as still alive.", "Revived {1}." ], - "DungeonNotYetCleared": [ - "But you haven't cleared {0} yet.", + "DungeonBossNotYetCleared": [ "But you never defeated {1} in the first place." ], "DungeonRequirementMarked": [ diff --git a/src/Randomizer.Shared/EnumerableExtensions.cs b/src/Randomizer.Shared/EnumerableExtensions.cs index e0c0ffd79..eec8185f2 100644 --- a/src/Randomizer.Shared/EnumerableExtensions.cs +++ b/src/Randomizer.Shared/EnumerableExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Randomizer.Shared { @@ -36,5 +37,22 @@ public static int IndexOf(this IReadOnlyList source, Func predica return -1; } + + /// + /// Filters a sequence of nullable values and returns only elements that + /// have a value. + /// + /// + /// The type of element in . + /// + /// The collection to filter on. + /// + /// A new collection that has no values. + /// + public static IEnumerable NonNull(this IEnumerable source) + { + return source.Where(x => x != null) + .Select(x => x!); + } } } diff --git a/src/Randomizer.Shared/Randomizer.Shared.csproj b/src/Randomizer.Shared/Randomizer.Shared.csproj index c0eb56980..f164e9503 100644 --- a/src/Randomizer.Shared/Randomizer.Shared.csproj +++ b/src/Randomizer.Shared/Randomizer.Shared.csproj @@ -3,6 +3,7 @@ net5.0 2.0.0 + disable From da046315bcb9747fc4b9880af6ff42efbbfdf5e1 Mon Sep 17 00:00:00 2001 From: Laura Verdoes <1766841+Vivelin@users.noreply.github.com> Date: Sun, 20 Mar 2022 12:07:25 +0100 Subject: [PATCH 2/3] Fixed missing case where too many items are missing and the command would error out --- .../Configuration/ResponseConfig.cs | 11 ++++++++++ src/Randomizer.SMZ3.Tracking/Tracker.cs | 20 ++++++++++++------- src/Randomizer.SMZ3.Tracking/tracker.json | 5 +++++ src/Randomizer.Shared/EnumerableExtensions.cs | 2 ++ 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/Randomizer.SMZ3.Tracking/Configuration/ResponseConfig.cs b/src/Randomizer.SMZ3.Tracking/Configuration/ResponseConfig.cs index 074ed0f07..0d3827fa9 100644 --- a/src/Randomizer.SMZ3.Tracking/Configuration/ResponseConfig.cs +++ b/src/Randomizer.SMZ3.Tracking/Configuration/ResponseConfig.cs @@ -324,6 +324,17 @@ public class ResponseConfig public SchrodingersString DungeonClearedWithInaccessibleItems { get; init; } = new("Including some out of logic checks that require {2}, such as {1}."); + /// + /// Gets the phrases to respond with when clearing all locations in a + /// dungeon, but some of the cleared locations were out of logic with too many missing items. + /// + /// + /// {0} is a placeholder for the name of the dungeon. {1} + /// is a placeholder for the name of a location that was missed. + /// + public SchrodingersString DungeonClearedWithTooManyInaccessibleItems { get; init; } + = new("Are you sure you got everything?"); + /// /// Gets the phrases to respond with when clearing a dungeon. /// diff --git a/src/Randomizer.SMZ3.Tracking/Tracker.cs b/src/Randomizer.SMZ3.Tracking/Tracker.cs index dff9d8ec5..0a36e36d9 100644 --- a/src/Randomizer.SMZ3.Tracking/Tracker.cs +++ b/src/Randomizer.SMZ3.Tracking/Tracker.cs @@ -1537,13 +1537,19 @@ public void ClearDungeon(DungeonInfo dungeon, float? confidence = null) { var anyMissedLocation = inaccessibleLocations.Random(s_random); var locationInfo = WorldInfo.Location(anyMissedLocation); - var missingItems = Logic.GetMissingRequiredItems(anyMissedLocation, progress) - .Random(s_random) - .Select(FindItemByType) - .NonNull(); - var missingItemsText = NaturalLanguage.Join(missingItems, World.Config); - - Say(x => x.DungeonClearedWithInaccessibleItems, dungeon.Name, locationInfo.Name, missingItemsText); + var missingItemCombinations = Logic.GetMissingRequiredItems(anyMissedLocation, progress); + if (missingItemCombinations.Any()) + { + var missingItems = missingItemCombinations.Random(s_random) + .Select(FindItemByType) + .NonNull(); + var missingItemsText = NaturalLanguage.Join(missingItems, World.Config); + Say(x => x.DungeonClearedWithInaccessibleItems, dungeon.Name, locationInfo.Name, missingItemsText); + } + else + { + Say(x => x.DungeonClearedWithTooManyInaccessibleItems, dungeon.Name, locationInfo.Name); + } } } else diff --git a/src/Randomizer.SMZ3.Tracking/tracker.json b/src/Randomizer.SMZ3.Tracking/tracker.json index 3b7c71c76..30f1a592a 100644 --- a/src/Randomizer.SMZ3.Tracking/tracker.json +++ b/src/Randomizer.SMZ3.Tracking/tracker.json @@ -1365,6 +1365,11 @@ "Also cleared some checks that require {2}. You might want to double-check {1}.", "You might want to double-check {1} because it requires {2}." ], + "DungeonClearedWithTooManyInaccessibleItems": [ + "Are you sure you cleared {0}? You're missing way too many items to get to {1}.", + "I doubt you got {1} though. Not with that many missing items.", + "I doubt you got {1} though. Not without so many items." + ], "DungeonBossCleared": [ "Marked {1} as defeated.", "Congratulations on beating off {1}.", diff --git a/src/Randomizer.Shared/EnumerableExtensions.cs b/src/Randomizer.Shared/EnumerableExtensions.cs index eec8185f2..3825c9a53 100644 --- a/src/Randomizer.Shared/EnumerableExtensions.cs +++ b/src/Randomizer.Shared/EnumerableExtensions.cs @@ -38,6 +38,7 @@ public static int IndexOf(this IReadOnlyList source, Func predica return -1; } +#nullable enable /// /// Filters a sequence of nullable values and returns only elements that /// have a value. @@ -54,5 +55,6 @@ public static IEnumerable NonNull(this IEnumerable source) return source.Where(x => x != null) .Select(x => x!); } +#nullable restore } } From cdc7fed5ae291aabf03d6d49fa6859536617e062 Mon Sep 17 00:00:00 2001 From: Laura Verdoes <1766841+Vivelin@users.noreply.github.com> Date: Wed, 23 Mar 2022 11:03:11 +0100 Subject: [PATCH 3/3] Don't add undo history when nothing was cleared --- src/Randomizer.SMZ3.Tracking/Tracker.cs | 45 ++++++++++++------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/Randomizer.SMZ3.Tracking/Tracker.cs b/src/Randomizer.SMZ3.Tracking/Tracker.cs index 0a36e36d9..adceeedfc 100644 --- a/src/Randomizer.SMZ3.Tracking/Tracker.cs +++ b/src/Randomizer.SMZ3.Tracking/Tracker.cs @@ -1530,35 +1530,34 @@ public void ClearDungeon(DungeonInfo dungeon, float? confidence = null) locations.ForEach(x => x.Cleared = true); } - if (remaining > 0 || locations.Count > 0) + if (remaining <= 0 && locations.Count <= 0) { - Say(x => x.DungeonCleared, dungeon.Name); - if (inaccessibleLocations.Count > 0) - { - var anyMissedLocation = inaccessibleLocations.Random(s_random); - var locationInfo = WorldInfo.Location(anyMissedLocation); - var missingItemCombinations = Logic.GetMissingRequiredItems(anyMissedLocation, progress); - if (missingItemCombinations.Any()) - { - var missingItems = missingItemCombinations.Random(s_random) - .Select(FindItemByType) - .NonNull(); - var missingItemsText = NaturalLanguage.Join(missingItems, World.Config); - Say(x => x.DungeonClearedWithInaccessibleItems, dungeon.Name, locationInfo.Name, missingItemsText); - } - else - { - Say(x => x.DungeonClearedWithTooManyInaccessibleItems, dungeon.Name, locationInfo.Name); - } - } + // We didn't do anything + Say(x => x.DungeonAlreadyCleared, dungeon.Name); + return; } - else + + Say(x => x.DungeonCleared, dungeon.Name); + if (inaccessibleLocations.Count > 0) { - Say(x => x.DungeonAlreadyCleared, dungeon.Name); + var anyMissedLocation = inaccessibleLocations.Random(s_random); + var locationInfo = WorldInfo.Location(anyMissedLocation); + var missingItemCombinations = Logic.GetMissingRequiredItems(anyMissedLocation, progress); + if (missingItemCombinations.Any()) + { + var missingItems = missingItemCombinations.Random(s_random) + .Select(FindItemByType) + .NonNull(); + var missingItemsText = NaturalLanguage.Join(missingItems, World.Config); + Say(x => x.DungeonClearedWithInaccessibleItems, dungeon.Name, locationInfo.Name, missingItemsText); + } + else + { + Say(x => x.DungeonClearedWithTooManyInaccessibleItems, dungeon.Name, locationInfo.Name); + } } OnDungeonUpdated(new(confidence)); - AddUndo(() => { dungeon.TreasureRemaining = remaining;