diff --git a/alttp_sm_combo_randomizer_rom/src/ips_patches/sm/LiftOffOnGoalCompletionAllDungeons.asm b/alttp_sm_combo_randomizer_rom/src/ips_patches/sm/LiftOffOnGoalCompletionAllDungeons.asm new file mode 100644 index 00000000..894dc6b1 --- /dev/null +++ b/alttp_sm_combo_randomizer_rom/src/ips_patches/sm/LiftOffOnGoalCompletionAllDungeons.asm @@ -0,0 +1,33 @@ +lorom + +org $a2aaa5 + JSL check_goal + +org $92ef00 +check_goal: + PHX + PHY + PHP + REP #$30 + + LDA $A17B74 : AND #$0007 : CMP #$0007 : BNE .refuel ; require all pendants + LDA $A17B7A : AND #$007F : CMP #$007F : BNE .refuel ; require all crystals + LDA $7ED832 : AND #$000F : CMP #$000F : BNE .refuel ; required all boss tokens + LDA $A17BC5 : CMP #$0003 : BCC .refuel ; require post-aga world state + LDA $A17ADB : AND #$0020 : CMP #$0020 : BNE .refuel ; require aga2 defeated (pyramid hole open) + + BRA .liftoff + +.refuel + PLP + PLY + PLX + CLC + RTL + +.liftoff + PLP + PLY + PLX + SEC + RTL \ No newline at end of file diff --git a/src/TrackerCouncil.Smz3.Abstractions/AutoTrackerBase.cs b/src/TrackerCouncil.Smz3.Abstractions/AutoTrackerBase.cs index 6150c195..449c49e5 100644 --- a/src/TrackerCouncil.Smz3.Abstractions/AutoTrackerBase.cs +++ b/src/TrackerCouncil.Smz3.Abstractions/AutoTrackerBase.cs @@ -69,6 +69,11 @@ public abstract class AutoTrackerBase : IDisposable /// public event EventHandler? GameChanged; + /// + /// Occurs when the tracker detects that the pyramid has been opened + /// + public event EventHandler? OpenPyramidChanged; + /// /// The action to run when the player asks Tracker to look at the game /// @@ -99,6 +104,11 @@ public abstract class AutoTrackerBase : IDisposable /// public bool PlayerHasFairy { get; set; } + /// + /// If the pyramid has been opened + /// + public bool OpenPyramid { get; set; } + /// /// If the user is activately in an SMZ3 rom /// @@ -169,5 +179,13 @@ protected virtual void OnGameChanged() GameChanged?.Invoke(this, EventArgs.Empty); } + /// + /// Invokes the OpenPyramidChanged event + /// + public virtual void OnOpenPyramidChanged() + { + OpenPyramidChanged?.Invoke(this, EventArgs.Empty); + } + public abstract void Dispose(); } diff --git a/src/TrackerCouncil.Smz3.Abstractions/AltGameModeBase.cs b/src/TrackerCouncil.Smz3.Abstractions/GameModeBase.cs similarity index 51% rename from src/TrackerCouncil.Smz3.Abstractions/AltGameModeBase.cs rename to src/TrackerCouncil.Smz3.Abstractions/GameModeBase.cs index 68a7c907..286243d6 100644 --- a/src/TrackerCouncil.Smz3.Abstractions/AltGameModeBase.cs +++ b/src/TrackerCouncil.Smz3.Abstractions/GameModeBase.cs @@ -1,5 +1,6 @@ using TrackerCouncil.Smz3.Data.GeneratedData; using TrackerCouncil.Smz3.Data.Options; +using TrackerCouncil.Smz3.Data.ParsedRom; using TrackerCouncil.Smz3.Data.ViewModels; using TrackerCouncil.Smz3.Data.WorldData; using TrackerCouncil.Smz3.Shared.Enums; @@ -7,30 +8,25 @@ namespace TrackerCouncil.Smz3.Abstractions; -public abstract class AltGameModeBase +public abstract class GameModeBase { public abstract bool IsComplete(World world); - public virtual void UpdateWorld(World world, int seed, GameModeOptions gameModeOptions) {} + public abstract void UpdateWorld(World world, Random rng, GameModeOptions gameModeOptions); - public virtual void UpdateInitialTrackerState(GameModeOptions gameModeOptions, TrackerState trackerState) {} + public abstract void UpdateInitialTrackerState(GameModeOptions gameModeOptions, TrackerState trackerState, ParsedRomDetails? parsedRomDetails); public virtual void InitializeTracker(TrackerBase tracker) {} - public virtual AltGameModeInGameText? GetInGameText(World world) - { - return null; - } + public abstract GameModeInGameText GetInGameText(World world); public virtual string? GetGameStartText(World world) { return null; } - public virtual bool IsKnowinglyComplete(World world) - { - return false; - } + public abstract bool IsKnowinglyComplete(World world); + public virtual bool OnViewingPyramidText(World world) { @@ -42,13 +38,9 @@ public virtual bool OnViewingPyramidText(World world) return null; } - public virtual List? GetGoalUiDetails(World world) - { - return null; - } + public abstract List GetGoalUiDetails(World world, Progression progression); - public virtual string GetSpoilerText(GameModeOptions gameModeOptions) - { - return string.Empty; - } + public abstract string GetSpoilerText(GameModeOptions gameModeOptions); + + public abstract Stream GetLiftOffOnGoalCompletionIpsPatch(); } diff --git a/src/TrackerCouncil.Smz3.Abstractions/ITrackerAltGameModeService.cs b/src/TrackerCouncil.Smz3.Abstractions/ITrackerAltGameModeService.cs deleted file mode 100644 index 9ed4d5c7..00000000 --- a/src/TrackerCouncil.Smz3.Abstractions/ITrackerAltGameModeService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using TrackerCouncil.Smz3.Data.ViewModels; -using TrackerCouncil.Smz3.Shared.Enums; - -namespace TrackerCouncil.Smz3.Abstractions; - -public interface ITrackerAltGameModeService -{ - public GameModeType GameModeType { get; } - public bool HasAltGameMode { get; } - public bool IsAltGameModeComplete { get; } - public event EventHandler? GoalStateChanged; - public void MarkAltGameModeAsComplete(); - public bool IsAltGameModeKnowinglyComplete(); - public void OnViewingPyramidText(); - public void NotifyOfGoalStateChange(); - public List GetGoalUiDetails(); -} diff --git a/src/TrackerCouncil.Smz3.Abstractions/ITrackerGameModeService.cs b/src/TrackerCouncil.Smz3.Abstractions/ITrackerGameModeService.cs new file mode 100644 index 00000000..e9d1a47e --- /dev/null +++ b/src/TrackerCouncil.Smz3.Abstractions/ITrackerGameModeService.cs @@ -0,0 +1,59 @@ +using TrackerCouncil.Smz3.Data.ViewModels; +using TrackerCouncil.Smz3.Shared.Enums; + +namespace TrackerCouncil.Smz3.Abstractions; + +/// +/// Tracker service for various events that need to interact with the game mode +/// +public interface ITrackerGameModeService +{ + /// + /// The current game mode type + /// + public GameModeType GameModeType { get; } + + /// + /// If this is a game mode not handled by the base assembly code + /// + public bool HasAltGameMode { get; } + + /// + /// If the game mode has been completed + /// + public bool IsGameModeComplete { get; } + + /// + /// Event called when the goal progress has changed + /// + public event EventHandler? GoalStateChanged; + + /// + /// Marks the game mode as complete + /// + public void MarkGameModeAsComplete(); + + /// + /// If the game mode is complete and the player should be aware of it + /// as they know the required amounts to be considered complete + /// + /// + public bool IsGameModeKnowinglyComplete(); + + /// + /// Informs the game mode that the player has viewed the pyramid so + /// that any unknown values are populated + /// + public void OnViewingPyramidText(); + + /// + /// Notifies all listeners that the progress of the game mode has changed + /// + public void NotifyOfGoalStateChange(); + + /// + /// Retrieve the UI to show the progress of the goals + /// + /// A list of goal details to show in the tracker UI + public List GetGoalUiDetails(); +} diff --git a/src/TrackerCouncil.Smz3.Abstractions/ITrackerGameStateService.cs b/src/TrackerCouncil.Smz3.Abstractions/ITrackerGameStateService.cs index b24a44bc..8c505a6d 100644 --- a/src/TrackerCouncil.Smz3.Abstractions/ITrackerGameStateService.cs +++ b/src/TrackerCouncil.Smz3.Abstractions/ITrackerGameStateService.cs @@ -14,11 +14,6 @@ public interface ITrackerGameStateService /// public bool HasBeatenGame { get; } - /// - /// Gets if the local player has finished the goal - /// - public bool HasFinishedGoal { get; set; } - /// /// The last viewed hint tile or set of locations /// diff --git a/src/TrackerCouncil.Smz3.Abstractions/ITrackerRewardService.cs b/src/TrackerCouncil.Smz3.Abstractions/ITrackerRewardService.cs index 165c575b..a66a1dad 100644 --- a/src/TrackerCouncil.Smz3.Abstractions/ITrackerRewardService.cs +++ b/src/TrackerCouncil.Smz3.Abstractions/ITrackerRewardService.cs @@ -6,6 +6,11 @@ namespace TrackerCouncil.Smz3.Abstractions; public interface ITrackerRewardService { + /// + /// Event called when rewards have been added or removed + /// + public event EventHandler? RewardsChanged; + /// /// Sets the dungeon's reward to the specific pendant or crystal. /// diff --git a/src/TrackerCouncil.Smz3.Abstractions/TrackerBase.cs b/src/TrackerCouncil.Smz3.Abstractions/TrackerBase.cs index 726df993..9c6dba67 100644 --- a/src/TrackerCouncil.Smz3.Abstractions/TrackerBase.cs +++ b/src/TrackerCouncil.Smz3.Abstractions/TrackerBase.cs @@ -54,7 +54,7 @@ public abstract class TrackerBase : IDisposable public ITrackerSpoilerService SpoilerService { get; protected set; } = null!; - public ITrackerAltGameModeService AltGameModeService { get; protected set; } = null!; + public ITrackerGameModeService GameModeService { get; protected set; } = null!; /// /// Gets the world for the currently tracked playthrough. diff --git a/src/TrackerCouncil.Smz3.Data.SchemaGenerator/Program.cs b/src/TrackerCouncil.Smz3.Data.SchemaGenerator/Program.cs index 8a28c4d5..37239e4b 100644 --- a/src/TrackerCouncil.Smz3.Data.SchemaGenerator/Program.cs +++ b/src/TrackerCouncil.Smz3.Data.SchemaGenerator/Program.cs @@ -36,7 +36,7 @@ public static class Program (typeof(RoomConfig), "rooms.json"), (typeof(UIConfig), "ui.json"), (typeof(MetadataConfig), "metadata.json"), - (typeof(AltGameModesConfig), "alt_game_modes.json"), + (typeof(GameModesConfig), "game_modes.json"), }; private static IServiceProvider? _services; @@ -221,11 +221,11 @@ private static void CreateTemplates(string outputPath) var exampleMetadataConfig = MetadataConfig.Example(); WriteTemplate(templatePath, "metadata", templateMetadataConfig, exampleMetadataConfig); - // Alt Game Modes Template - var altGameModeConfig = new AltGameModesConfig(); - PopulateExample(altGameModeConfig, true); - var exampleAltGameModeConfig = AltGameModesConfig.Example(); - WriteTemplate(templatePath, "alt_game_modes", altGameModeConfig, exampleAltGameModeConfig); + // Game Modes Template + var gameModeConfig = new GameModesConfig(); + PopulateExample(gameModeConfig, true); + var exampleGameModeConfig = GameModesConfig.Example(); + WriteTemplate(templatePath, "game_modes", gameModeConfig, exampleGameModeConfig); } private static void WriteTemplate(string templatePath, string type, object data, object? example) diff --git a/src/TrackerCouncil.Smz3.Data/Configuration/ConfigFiles/AltGameModesConfig.cs b/src/TrackerCouncil.Smz3.Data/Configuration/ConfigFiles/GameModesConfig.cs similarity index 56% rename from src/TrackerCouncil.Smz3.Data/Configuration/ConfigFiles/AltGameModesConfig.cs rename to src/TrackerCouncil.Smz3.Data/Configuration/ConfigFiles/GameModesConfig.cs index 7040a4b5..357c143d 100644 --- a/src/TrackerCouncil.Smz3.Data/Configuration/ConfigFiles/AltGameModesConfig.cs +++ b/src/TrackerCouncil.Smz3.Data/Configuration/ConfigFiles/GameModesConfig.cs @@ -6,11 +6,11 @@ namespace TrackerCouncil.Smz3.Data.Configuration.ConfigFiles; [Description("Config file for various tracker lines and in game text for alt game modes.\r\n" + "Note that all lines in game need to be no more than 19 characters.")] -public class AltGameModesConfig : IMergeable, IConfigFile +public class GameModesConfig : IMergeable, IConfigFile { - public static AltGameModesConfig Default() + public static GameModesConfig Default() { - return new AltGameModesConfig(); + return new GameModesConfig(); } /// @@ -35,11 +35,32 @@ public static AltGameModesConfig Default() /// public SchrodingersString? SpazerHuntGameStarted { get; set; } + /// + /// In game text for the intro to the Ganon battle for All Dungeons mode + /// + public SchrodingersString? AllDungeonsGanonIntro { get; set; } + + /// + /// In game text for the intro to the Ganon battle when you have not met the requirements for All Dungeons mode + /// + public SchrodingersString? AllDungeonsGanonIntroGoalsNotMet { get; set; } + + /// + /// In game text for the sign on Ganon's pyramid to explain the goal for All Dungeons mode + /// + public SchrodingersString? AllDungeonsGanonGoalSign { get; set; } + + /// + /// Tracker line for when starting a new game in All Dungeons mode + /// + public SchrodingersString? AllDungeonsGameStarted { get; set; } + public static object Example() { return new Dictionary() { - { "SpazerHuntGanonIntro", new SchrodingersString("In game text with\neach line being\n19 characters\nor less", new SchrodingersString.Possibility("Another example\nof in game text", 0.1)) } + { "SpazerHuntGanonIntro", new SchrodingersString("In game text with\neach line being\n19 characters\nor less", new SchrodingersString.Possibility("Another example\nof in game text", 0.1)) }, + { "AllDungeonsGanonIntro", new SchrodingersString("In game text with\neach line being\n19 characters\nor less", new SchrodingersString.Possibility("Another example\nof in game text", 0.1)) } }; } } diff --git a/src/TrackerCouncil.Smz3.Data/Configuration/ConfigProvider.cs b/src/TrackerCouncil.Smz3.Data/Configuration/ConfigProvider.cs index 0f78bf94..4826776b 100644 --- a/src/TrackerCouncil.Smz3.Data/Configuration/ConfigProvider.cs +++ b/src/TrackerCouncil.Smz3.Data/Configuration/ConfigProvider.cs @@ -292,13 +292,13 @@ public virtual MetadataConfig GetMetadataConfig(IReadOnlyCollection prof LoadYamlConfigs("metadata.yml", profiles, mood); /// - /// Returns the configs with alt game mode text + /// Returns the configs with game mode text /// /// The selected tracker profile(s) to load /// The current tracker mood to pick the specific mood file /// - public virtual AltGameModesConfig GetAltGameModesConfig(IReadOnlyCollection profiles, string? mood) => - LoadYamlConfigs("alt_game_modes.yml", profiles, mood); + public virtual GameModesConfig GetGameModesConfig(IReadOnlyCollection profiles, string? mood) => + LoadYamlConfigs("game_modes.yml", profiles, mood); /// /// Returns a collection of all possible config profiles to diff --git a/src/TrackerCouncil.Smz3.Data/GeneratedData/AltGameModeInGameText.cs b/src/TrackerCouncil.Smz3.Data/GeneratedData/AltGameModeInGameText.cs deleted file mode 100644 index 00ed8356..00000000 --- a/src/TrackerCouncil.Smz3.Data/GeneratedData/AltGameModeInGameText.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TrackerCouncil.Smz3.Data.GeneratedData; - -public class AltGameModeInGameText -{ - public string? GanonIntro { get; set; } - public string? GanonIntroGoalsNotMet { get; set; } - public string? GanonGoalSign { get; set; } -} diff --git a/src/TrackerCouncil.Smz3.Data/GeneratedData/GameModeInGameText.cs b/src/TrackerCouncil.Smz3.Data/GeneratedData/GameModeInGameText.cs new file mode 100644 index 00000000..b2430111 --- /dev/null +++ b/src/TrackerCouncil.Smz3.Data/GeneratedData/GameModeInGameText.cs @@ -0,0 +1,10 @@ +using TrackerCouncil.Smz3.Data.Configuration.ConfigTypes; + +namespace TrackerCouncil.Smz3.Data.GeneratedData; + +public class GameModeInGameText +{ + public required string? GanonIntro { get; set; } + public required string? GanonIntroGoalsNotMet { get; set; } + public required string? GanonGoalSign { get; set; } +} diff --git a/src/TrackerCouncil.Smz3.Data/Options/GameModeOptions.cs b/src/TrackerCouncil.Smz3.Data/Options/GameModeOptions.cs index a7483e32..79ee5042 100644 --- a/src/TrackerCouncil.Smz3.Data/Options/GameModeOptions.cs +++ b/src/TrackerCouncil.Smz3.Data/Options/GameModeOptions.cs @@ -34,47 +34,13 @@ public class GameModeOptions public bool ShuffleMetroidBossTokens { get; set; } public bool OpenPyramid { get; set; } - public void Randomize(Random rnd) + public GameModeOptions Clone() { - if (!RandomizeNumericAmounts) - { - return; - } - - if (MaxGanonsTowerCrystalCount > MinGanonsTowerCrystalCount) - { - GanonsTowerCrystalCount = rnd.Next(MinGanonsTowerCrystalCount, MaxGanonsTowerCrystalCount + 1); - } - else - { - GanonsTowerCrystalCount = MinGanonsTowerCrystalCount; - } - - // Only randomize vanilla crystals as alt game modes should do their own randomization - if (SelectedGameModeType == GameModeType.Vanilla) - { - if (MaxGanonCrystalCount > MinGanonCrystalCount) - { - GanonCrystalCount = rnd.Next(MinGanonCrystalCount, MaxGanonCrystalCount + 1); - } - else - { - GanonCrystalCount = MinGanonCrystalCount; - } - - if (MaxTourianBossCount > MinTourianBossCount) - { - TourianBossCount = rnd.Next(MinTourianBossCount, MaxTourianBossCount + 1); - } - else - { - TourianBossCount = MinTourianBossCount; - } - } + return (GameModeOptions)MemberwiseClone(); } - public GameModeOptions Clone() + public bool IsAltGameMode() { - return (GameModeOptions)MemberwiseClone(); + return SelectedGameModeType != GameModeType.Vanilla && SelectedGameModeType != GameModeType.AllDungeons; } } diff --git a/src/TrackerCouncil.Smz3.Data/ParsedRom/ParsedRomDetails.cs b/src/TrackerCouncil.Smz3.Data/ParsedRom/ParsedRomDetails.cs index e86f3977..c6b4011f 100644 --- a/src/TrackerCouncil.Smz3.Data/ParsedRom/ParsedRomDetails.cs +++ b/src/TrackerCouncil.Smz3.Data/ParsedRom/ParsedRomDetails.cs @@ -19,6 +19,7 @@ public class ParsedRomDetails public required bool OpenPyarmid { get; set; } public required bool SkipTourianBossDoor { get; set; } public required bool SpinJumpAnimations { get; set; } + public required GameModeType GameModeType { get; set; } public required List Players { get; set; } public required List Locations { get; set; } public required List Bosses { get; set; } diff --git a/src/TrackerCouncil.Smz3.Data/Services/GenerationSettingsWindowService.cs b/src/TrackerCouncil.Smz3.Data/Services/GenerationSettingsWindowService.cs index b7ce1c8f..e471641c 100644 --- a/src/TrackerCouncil.Smz3.Data/Services/GenerationSettingsWindowService.cs +++ b/src/TrackerCouncil.Smz3.Data/Services/GenerationSettingsWindowService.cs @@ -41,6 +41,7 @@ public GenerationWindowViewModel GetViewModel(Dictionary g _model.Basic.SelectedPreset = _model.Basic.Presets.First(); _model.Basic.MsuShuffleStyle = _options.PatchOptions.MsuShuffleStyle; _model.Basic.MsuRandomizationStyle = _options.PatchOptions.MsuRandomizationStyle; + _model.Basic.LiftOffOnGoalCompletion = _options.SeedOptions.GameModeOptions.LiftOffOnGoalCompletion; _model.GameSettings.RandomizeNumericValues = _options.SeedOptions.GameModeOptions.RandomizeNumericAmounts; _model.GameSettings.GoalSettings.GameModeTypeDescriptions = gameModeDescriptions; @@ -215,6 +216,11 @@ public void SaveSettings() _options.PatchOptions.MetroidControls.AimUp = _model.Customizations.AimUpButton; _options.PatchOptions.MetroidControls.AimDown = _model.Customizations.AimDownButton; + if (_model.IsImportMode) + { + _options.SeedOptions.GameModeOptions.LiftOffOnGoalCompletion = _model.Basic.LiftOffOnGoalCompletion; + } + _options.Save(); } diff --git a/src/TrackerCouncil.Smz3.Data/Services/IMetadataService.cs b/src/TrackerCouncil.Smz3.Data/Services/IMetadataService.cs index 7e8c3cf9..a901bc67 100644 --- a/src/TrackerCouncil.Smz3.Data/Services/IMetadataService.cs +++ b/src/TrackerCouncil.Smz3.Data/Services/IMetadataService.cs @@ -83,7 +83,7 @@ public interface IMetadataService /// /// Lines for alt game modes /// - public AltGameModesConfig AltGameModes { get; } + public GameModesConfig GameModes { get; } /// /// The current selected mood diff --git a/src/TrackerCouncil.Smz3.Data/Services/MetadataService.cs b/src/TrackerCouncil.Smz3.Data/Services/MetadataService.cs index fb2aa7ea..73e8a54f 100644 --- a/src/TrackerCouncil.Smz3.Data/Services/MetadataService.cs +++ b/src/TrackerCouncil.Smz3.Data/Services/MetadataService.cs @@ -61,7 +61,7 @@ public MetadataService(ConfigProvider configs, ILogger logger, public UIConfig UILayouts { get; private set; } = null!; - public AltGameModesConfig AltGameModes { get; private set; } = null!; + public GameModesConfig GameModes { get; private set; } = null!; public string Mood { get; private set; } = null!; @@ -111,7 +111,7 @@ public void ReloadConfigs() Rewards = _configProvider.GetRewardConfig(profiles, Mood); Rooms = _configProvider.GetRoomConfig(profiles, Mood); UILayouts = _configProvider.GetUIConfig(profiles, Mood); - AltGameModes = _configProvider.GetAltGameModesConfig(profiles, Mood); + GameModes = _configProvider.GetGameModesConfig(profiles, Mood); } else { @@ -129,7 +129,7 @@ public void ReloadConfigs() Rewards = IMergeable.Combine(_configProvider.GetRewardConfig(profiles, Mood), TrackerSpriteProfile.RewardConfig); Rooms = IMergeable.Combine(_configProvider.GetRoomConfig(profiles, Mood), TrackerSpriteProfile.RoomConfig); UILayouts = _configProvider.GetUIConfig(profiles, Mood); - AltGameModes = _configProvider.GetAltGameModesConfig(profiles, Mood); + GameModes = _configProvider.GetGameModesConfig(profiles, Mood); } } diff --git a/src/TrackerCouncil.Smz3.Data/ViewModels/GenerationWindowBasicViewModel.cs b/src/TrackerCouncil.Smz3.Data/ViewModels/GenerationWindowBasicViewModel.cs index fc64010b..ac7dd3b0 100644 --- a/src/TrackerCouncil.Smz3.Data/ViewModels/GenerationWindowBasicViewModel.cs +++ b/src/TrackerCouncil.Smz3.Data/ViewModels/GenerationWindowBasicViewModel.cs @@ -120,6 +120,12 @@ public MsuShuffleStyle MsuShuffleStyle public bool IsShuffleStyleVisible => MsuRandomizationStyle is MSURandomizerLibrary.MsuRandomizationStyle.Continuous or MSURandomizerLibrary.MsuRandomizationStyle.Shuffled; + public bool LiftOffOnGoalCompletion + { + get; + set => SetField(ref field, value); + } + public List MsuPaths { get => _msuPaths; diff --git a/src/TrackerCouncil.Smz3.Data/ViewModels/GenerationWindowGoalSettingsViewModel..cs b/src/TrackerCouncil.Smz3.Data/ViewModels/GenerationWindowGoalSettingsViewModel..cs index ea9702c7..20f80794 100644 --- a/src/TrackerCouncil.Smz3.Data/ViewModels/GenerationWindowGoalSettingsViewModel..cs +++ b/src/TrackerCouncil.Smz3.Data/ViewModels/GenerationWindowGoalSettingsViewModel..cs @@ -223,7 +223,6 @@ public bool RandomizeGoalCounts public bool ShowRandomizedSpazerCounts => RandomizeGoalCounts && IsSpazerHunt; public bool StaticGoalCounts => !RandomizeGoalCounts; public bool IsDefaultGameMode => SelectedGameModeType == GameModeType.Vanilla; - public bool IsAltGameMode => SelectedGameModeType != GameModeType.Vanilla; public bool IsSpazerHunt => SelectedGameModeType == GameModeType.SpazerHunt; public Dictionary GameModeTypeDescriptions = new(); diff --git a/src/TrackerCouncil.Smz3.Data/WorldData/Progression.cs b/src/TrackerCouncil.Smz3.Data/WorldData/Progression.cs index bd363890..d1f1fa33 100644 --- a/src/TrackerCouncil.Smz3.Data/WorldData/Progression.cs +++ b/src/TrackerCouncil.Smz3.Data/WorldData/Progression.cs @@ -122,7 +122,8 @@ public Progression(IEnumerable items, IEnumerable rewards, IEnumer public int Rupees => GetRupeeCount(); public bool Agahnim => Rewards.Contains(RewardType.Agahnim); public bool GreenPendant => Rewards.Contains(RewardType.PendantGreen); - public bool AllPendants => Rewards.Count(r => r is RewardType.PendantGreen or RewardType.PendantRed or RewardType.PendantBlue) >= 3; + public int PendantCount => Rewards.Count(r => r is RewardType.PendantGreen or RewardType.PendantRed or RewardType.PendantBlue); + public bool AllPendants => PendantCount >= 3; public bool BothRedCrystals => Rewards.Count(r => r == RewardType.CrystalRed) >= 2; public bool AllCrystals => CrystalCount >= 7; public bool Kraid => DefeatedBosses.Contains(BossType.Kraid); diff --git a/src/TrackerCouncil.Smz3.Data/WorldData/Regions/Zelda/DarkWorld/DarkWorldNorthEast.cs b/src/TrackerCouncil.Smz3.Data/WorldData/Regions/Zelda/DarkWorld/DarkWorldNorthEast.cs index 15a1b419..eff047e9 100644 --- a/src/TrackerCouncil.Smz3.Data/WorldData/Regions/Zelda/DarkWorld/DarkWorldNorthEast.cs +++ b/src/TrackerCouncil.Smz3.Data/WorldData/Regions/Zelda/DarkWorld/DarkWorldNorthEast.cs @@ -93,7 +93,9 @@ public void UpdateGanonAccessibility(Progression progression, Progression assume var isPyramidOpen = World.Config.GameModeOptions.OpenPyramid || canClimbGt; var hasMetGoal = hasAltGameMode ? isAltGameModeComplete - : numAcquiredCrystals >= ganonCrystalRequirement; + : World.Config.GameModeOptions.SelectedGameModeType == GameModeType.Vanilla + ? numAcquiredCrystals >= ganonCrystalRequirement + : progression is { AllCrystals: true, AllPendants: true, MetroidBossCount: >= 4, Agahnim: true }; var canHurtGanon = hasMetGoal && progression.MasterSword && (progression.Lamp || progression.FireRod); @@ -113,8 +115,11 @@ public void UpdateGanonAccessibility(Progression progression, Progression assume assumedKeyProgression.LegacyProgression))); var isPyramidOpen = World.Config.GameModeOptions.OpenPyramid || canClimbGt; - var canHurtGanon = numAcquiredCrystals >= ganonCrystalRequirement && progression.MasterSword && - (progression.Lamp || progression.FireRod); + var hasMetGoal = World.Config.GameModeOptions.SelectedGameModeType == GameModeType.Vanilla + ? numAcquiredCrystals >= ganonCrystalRequirement + : progression is { AllCrystals: true, AllPendants: true, MetroidBossCount: >= 4, Agahnim: true }; + + var canHurtGanon = hasMetGoal && progression.MasterSword && (progression.Lamp || progression.FireRod); NewAccessibility = canAccessPyramid && isPyramidOpen && canHurtGanon ? Accessibility.Available diff --git a/src/TrackerCouncil.Smz3.Data/WorldData/World.cs b/src/TrackerCouncil.Smz3.Data/WorldData/World.cs index bae76ff4..0bf173cb 100644 --- a/src/TrackerCouncil.Smz3.Data/WorldData/World.cs +++ b/src/TrackerCouncil.Smz3.Data/WorldData/World.cs @@ -273,7 +273,6 @@ public Boss GetBossOfType(BossType type) public void Setup(Random rnd) { - Config.GameModeOptions.Randomize(rnd); SetMedallions(rnd); SetRewards(rnd); SetBottles(rnd); diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/FileData/IpsPatches/IpsPatch.cs b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/IpsPatches/IpsPatch.cs index f9301499..5a121610 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/FileData/IpsPatches/IpsPatch.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/IpsPatches/IpsPatch.cs @@ -224,5 +224,11 @@ public static Stream GetStream(string name) /// A new stream that contains the IPS patch. public static Stream LiftOffOnGoalCompletionAlt() => GetStream("LiftOffOnGoalCompletionAlt.ips"); + /// + /// Gets a stream for the IPS patch to allow you to lift off from ship after completing goal + /// + /// A new stream that contains the IPS patch. + public static Stream LiftOffOnGoalCompletionAllDungeons() => GetStream("LiftOffOnGoalCompletionAllDungeons.ips"); + } diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/FileData/IpsPatches/LiftOffOnGoalCompletionAllDungeons.ips b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/IpsPatches/LiftOffOnGoalCompletionAllDungeons.ips new file mode 100644 index 00000000..230df9dc Binary files /dev/null and b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/IpsPatches/LiftOffOnGoalCompletionAllDungeons.ips differ diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/GoalsPatch.cs b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/GoalsPatch.cs index f3ac2a9b..9dfff858 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/GoalsPatch.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/GoalsPatch.cs @@ -5,11 +5,17 @@ namespace TrackerCouncil.Smz3.SeedGenerator.FileData.Patches; -[SkipForParsedRoms] public class GoalsPatch : RomPatch { public override IEnumerable GetChanges(GetPatchesRequest data) { + if (data.IsParsedRom && data.Config.ParsedRomDetails!.RomGenerator != RomGenerator.Cas) + { + yield return new GeneratedPatch(Snes(0xF47008), [(byte)data.Config.ParsedRomDetails.TourianBossCount, 0x00]); + yield return new GeneratedPatch(Snes(0xF47012), [(byte)data.Config.ParsedRomDetails.GanonCrystalCount, 0x00]); + yield break; + } + // Open pyramid if (data.Config.GameModeOptions.OpenPyramid) { @@ -22,11 +28,12 @@ public override IEnumerable GetChanges(GetPatchesRequest data) yield return new GeneratedPatch(0x40008C, gtCrystals == 0 ? [0x01] : [0x00]); // Crystals to damage Ganon - yield return new GeneratedPatch(0x40005F, [(byte)data.Config.GameModeOptions.GanonCrystalCount]); + var ganonCrystals = data.Config.GameModeOptions.SelectedGameModeType == GameModeType.AllDungeons ? 7 : data.Config.GameModeOptions.GanonCrystalCount; + yield return new GeneratedPatch(0x40005F, [(byte)ganonCrystals]); yield return new GeneratedPatch(Snes(0xF47012), [(byte)data.Config.GameModeOptions.GanonCrystalCount, 0x00]); // Bosses to enter Tourian - var numBosses = data.Config.GameModeOptions.TourianBossCount; + var numBosses = data.Config.GameModeOptions.SelectedGameModeType == GameModeType.AllDungeons ? 4 : data.Config.GameModeOptions.TourianBossCount; yield return new GeneratedPatch(Snes(0xF47008), [(byte)numBosses, 0x00]); yield return new GeneratedPatch(Snes(0xF4700B), numBosses == 0 ? [0x00, 0x04] : [0x00, 0x01]); @@ -39,6 +46,10 @@ public override IEnumerable GetChanges(GetPatchesRequest data) { yield return new GeneratedPatch(Snes(0x30803E), [0x03]); } + else if (data.Config.GameModeOptions.SelectedGameModeType == GameModeType.AllDungeons) + { + yield return new GeneratedPatch(Snes(0x30803E), [0x02]); + } else { yield return new GeneratedPatch(Snes(0x30803E), [0x05]); @@ -72,4 +83,17 @@ public static bool GetOpenPyramid(byte[] rom) { return rom[Snes(0x30808B)] == 0x01; } + + public static GameModeType GetGameModeType(byte[] rom) + { + var val = rom[Snes(0x30803E)]; + if (val == 0x02) + { + return GameModeType.AllDungeons; + } + else + { + return GameModeType.Vanilla; + } + } } diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/ZeldaTextsPatch.cs b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/ZeldaTextsPatch.cs index 6a90f17a..532e2a48 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/ZeldaTextsPatch.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/ZeldaTextsPatch.cs @@ -9,8 +9,8 @@ using TrackerCouncil.Smz3.Data.Services; using TrackerCouncil.Smz3.Data.WorldData; using TrackerCouncil.Smz3.Data.WorldData.Regions; -using TrackerCouncil.Smz3.SeedGenerator.AltGameModes; using TrackerCouncil.Smz3.SeedGenerator.Contracts; +using TrackerCouncil.Smz3.SeedGenerator.GameModes; using TrackerCouncil.Smz3.SeedGenerator.Text; using TrackerCouncil.Smz3.Shared.Enums; using TrackerCouncil.Smz3.Shared.Models; @@ -18,7 +18,7 @@ namespace TrackerCouncil.Smz3.SeedGenerator.FileData.Patches; [Order(-5)] -public class ZeldaTextsPatch(IMetadataService metadataService, IGameHintService gameHintService, AltGameModeFactory altGameModeFactory) : RomPatch +public class ZeldaTextsPatch(IMetadataService metadataService, IGameHintService gameHintService, GameModeFactory gameModeFactory) : RomPatch { private StringTable _stringTable = null!; private PlandoTextConfig _plandoText = null!; @@ -74,7 +74,7 @@ private IEnumerable GetFullTextPatchList(GetPatchesRequest data) { var regions = data.World.Regions.OfType().ToList(); - var altGameModeText = altGameModeFactory.GetInGameText(data.World); + var gameModeText = gameModeFactory.GetInGameText(data.World); var greenPendantDungeon = regions .Where(x => x.RewardType == RewardType.PendantGreen) @@ -100,7 +100,7 @@ private IEnumerable GetFullTextPatchList(GetPatchesRequest data) yield return SetText(0x308C00, StringTable.TavernMan, GameLines.TavernMan, _plandoText.TavernMan); - foreach (var text in GanonText(data, altGameModeText)) + foreach (var text in GanonText(data, gameModeText)) { yield return text; } @@ -123,9 +123,7 @@ private IEnumerable GetFullTextPatchList(GetPatchesRequest data) SetText(StringTable.GanonsTowerGoalSign, GameLines.GanonsTowerGoalSign, _plandoText.GanonsTowerGoalSign, false, gtCrystalCount == 1 ? "1 crystal" : $"{gtCrystalCount} crystals"); - var ganonCrystalCount = data.World.Config.GameModeOptions.GanonCrystalCount; - SetText(StringTable.GanonGoalSign, - GameLines.GanonGoalSign, _plandoText.GanonGoalSign ?? altGameModeText?.GanonGoalSign, false, ganonCrystalCount == 1 ? "1 crystal" : $"{ganonCrystalCount} crystals"); + SetText(StringTable.GanonGoalSign, null, _plandoText.GanonGoalSign ?? gameModeText?.GanonGoalSign); if (data.World.HintTiles.Any() || _plandoText.HasHintTileText) { @@ -213,13 +211,10 @@ private void SetChoiceText(string textKey, SchrodingersString? defaultText, stri } - private IEnumerable GanonText(GetPatchesRequest data, AltGameModeInGameText? altGameModeInGameText) + private IEnumerable GanonText(GetPatchesRequest data, GameModeInGameText? gameModeInGameText) { - yield return SetText(0x308600, StringTable.GanonIntro, - GameLines.GanonIntro, _plandoText.GanonIntro ?? altGameModeInGameText?.GanonIntro); - - yield return SetText(0x309100, StringTable.GanonIntroAlt, - GameLines.GanonIntroGoalsNotMet, _plandoText.GanonIntroGoalsNotMet ?? altGameModeInGameText?.GanonIntroGoalsNotMet); + yield return SetText(0x308600, StringTable.GanonIntro, null, _plandoText.GanonIntro ?? gameModeInGameText?.GanonIntro); + yield return SetText(0x309100, StringTable.GanonIntroAlt, null, _plandoText.GanonIntroGoalsNotMet ?? gameModeInGameText?.GanonIntroGoalsNotMet); var silversLocation = data.Worlds.SelectMany(world => world.Locations) .FirstOrDefault(l => l.ItemIs(ItemType.SilverArrows, data.World)); diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/AllDungeonsGameMode.cs b/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/AllDungeonsGameMode.cs new file mode 100644 index 00000000..302afb3d --- /dev/null +++ b/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/AllDungeonsGameMode.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using TrackerCouncil.Smz3.Abstractions; +using TrackerCouncil.Smz3.Data.GeneratedData; +using TrackerCouncil.Smz3.Data.Options; +using TrackerCouncil.Smz3.Data.ParsedRom; +using TrackerCouncil.Smz3.Data.Services; +using TrackerCouncil.Smz3.Data.Tracking; +using TrackerCouncil.Smz3.Data.ViewModels; +using TrackerCouncil.Smz3.Data.WorldData; +using TrackerCouncil.Smz3.SeedGenerator.FileData.IpsPatches; +using TrackerCouncil.Smz3.Shared.Enums; +using TrackerCouncil.Smz3.Shared.Models; + +namespace TrackerCouncil.Smz3.SeedGenerator.GameModes; + +[GameModeType(GameModeType.AllDungeons)] +[Description("Complete all dungeons including Castle Tower and Ganon's Tower and defeat all 4 main Metroid bosses to be able to defeat Ganon. Defeat all 4 main bosses in Metroid to be able to enter Tourian and defeat Mother Brain.")] +public class AllDungeonsGameMode(IMetadataService metadata) : GameModeBase +{ + private TrackerBase _tracker = null!; + + public override void InitializeTracker(TrackerBase tracker) + { + _tracker = tracker; + _tracker.BossTracker.BossUpdated += BossTrackerOnBossUpdated; + _tracker.RewardTracker.RewardsChanged += RewardTrackerOnRewardsChanged; + } + + private void RewardTrackerOnRewardsChanged(object? sender, EventArgs e) + { + if (IsComplete(_tracker.World)) + { + _tracker.GameModeService.MarkGameModeAsComplete(); + } + + _tracker.GameModeService.NotifyOfGoalStateChange(); + } + + private void BossTrackerOnBossUpdated(object? sender, BossTrackedEventArgs e) + { + if (IsComplete(_tracker.World)) + { + _tracker.GameModeService.MarkGameModeAsComplete(); + } + + _tracker.GameModeService.NotifyOfGoalStateChange(); + } + + public override bool IsComplete(World world) + { + if (world.State == null) + { + return false; + } + + var progression = _tracker.PlayerProgressionService.GetProgression(false); + + return progression is { AllPendants: true, AllCrystals: true } && + progression.MetroidBossCount >= 4 && + world.AllBosses.First(x => x.Type == BossType.Agahnim).Defeated && + (_tracker.AutoTracker?.OpenPyramid == true || world.AllBosses.First(x => x.Type == BossType.Ganon).Defeated || world.Config.GameModeOptions.OpenPyramid); + } + + public override void UpdateWorld(World world, Random rng, GameModeOptions gameModeOptions) + { + gameModeOptions.GanonCrystalCount = 7; + gameModeOptions.TourianBossCount = 4; + gameModeOptions.OpenPyramid = false; + } + + public override void UpdateInitialTrackerState(GameModeOptions gameModeOptions, TrackerState trackerState, + ParsedRomDetails? parsedRomDetails) + { + trackerState.MarkedGanonCrystalCount = 7; + trackerState.MarkedTourianBossCount = 4; + } + + public override GameModeInGameText GetInGameText(World world) + { + return new GameModeInGameText + { + GanonIntro = metadata.GameModes.AllDungeonsGanonIntro, + GanonIntroGoalsNotMet = metadata.GameModes.AllDungeonsGanonIntroGoalsNotMet, + GanonGoalSign = metadata.GameModes.AllDungeonsGanonGoalSign, + }; + } + + public override string? GetGameStartText(World world) + { + return metadata.GameModes.AllDungeonsGameStarted?.ToString(); + } + + public override bool IsKnowinglyComplete(World world) + { + return IsComplete(world); + } + + public override List GetGoalUiDetails(World world, Progression progression) + { + var dungeons = progression.CrystalCount + progression.PendantCount; + + if (world.AllBosses.First(x => x.Type == BossType.Agahnim).Defeated) + { + dungeons++; + } + + if (_tracker.AutoTracker?.OpenPyramid == true || world.AllBosses.First(x => x.Type == BossType.Ganon).Defeated || world.Config.GameModeOptions.OpenPyramid) + { + dungeons++; + } + + var bosses = progression.MetroidBossCount; + return + [ + new GoalUiDetails { IconCategory = "Items", Icon = "map_full.png", Text = $"{dungeons}/12" }, + new GoalUiDetails { IconCategory = "Dungeons", Icon = "metroid boss token.png", Text = $"{bosses}/4" } + ]; + } + + public override string GetSpoilerText(GameModeOptions gameModeOptions) + { + return ""; + } + + public override Stream GetLiftOffOnGoalCompletionIpsPatch() + { + return IpsPatch.LiftOffOnGoalCompletionAllDungeons(); + } +} diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/AltGameModes/AltGameModeFactory.cs b/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/GameModeFactory.cs similarity index 59% rename from src/TrackerCouncil.Smz3.SeedGenerator/AltGameModes/AltGameModeFactory.cs rename to src/TrackerCouncil.Smz3.SeedGenerator/GameModes/GameModeFactory.cs index f7d165e9..87de0db8 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/AltGameModes/AltGameModeFactory.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/GameModeFactory.cs @@ -1,27 +1,23 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.IO; using System.Reflection; using TrackerCouncil.Smz3.Abstractions; using TrackerCouncil.Smz3.Data.GeneratedData; using TrackerCouncil.Smz3.Data.Options; +using TrackerCouncil.Smz3.Data.ParsedRom; using TrackerCouncil.Smz3.Data.WorldData; +using TrackerCouncil.Smz3.Shared; using TrackerCouncil.Smz3.Shared.Enums; using TrackerCouncil.Smz3.Shared.Models; -namespace TrackerCouncil.Smz3.SeedGenerator.AltGameModes; +namespace TrackerCouncil.Smz3.SeedGenerator.GameModes; -public class AltGameModeFactory(IServiceProvider serviceProvider) +public class GameModeFactory(IServiceProvider serviceProvider) { private static readonly Dictionary s_gameModeTypes = []; - - private static readonly Dictionary s_gameModeDescriptions = new() - { - { - GameModeType.Vanilla, - "Complete Zelda dungeons to obtain crystals to enter Ganon's Tower and defeat Ganon. Defeat bosses in Super Metroid to be able to enter Tourian and defeat Mother Brain." - } - }; + private static readonly Dictionary s_gameModeDescriptions = new(); public static void AddGameModeClass(Type gameModeType) { @@ -41,14 +37,14 @@ public static void AddGameModeClass(Type gameModeType) s_gameModeDescriptions.Add(gameModeTypeAttribute.GameModeType, descriptionAttribute.Description); } - public AltGameModeBase GetGameMode(GameModeType gameMode) + public GameModeBase GetGameMode(GameModeType gameMode) { if (!s_gameModeTypes.TryGetValue(gameMode, out var gameModeClassType)) { throw new InvalidOperationException($"GameModeType {gameMode} does not have a matching GameMode class"); } - return serviceProvider.GetService(gameModeClassType) as AltGameModeBase ?? + return serviceProvider.GetService(gameModeClassType) as GameModeBase ?? throw new InvalidOperationException($"GameModeType {gameMode} does not have a valid GameMode class"); } @@ -56,36 +52,31 @@ public World UpdateWorld(World world, int seed) { var gameModeOptions = world.Config.GameModeOptions; - if (gameModeOptions.SelectedGameModeType == GameModeType.Vanilla) + var rng = new Random(seed); + rng.Sanitize(); + + var gameMode = GetGameMode(gameModeOptions.SelectedGameModeType); + + gameMode.UpdateWorld(world, rng, gameModeOptions); + + if (gameModeOptions.RandomizeNumericAmounts) { - return world; + gameModeOptions.GanonCrystalCount = rng.Next(gameModeOptions.MinGanonCrystalCount, gameModeOptions.MaxGanonCrystalCount + 1); + gameModeOptions.TourianBossCount = rng.Next(gameModeOptions.MinTourianBossCount, gameModeOptions.MaxTourianBossCount + 1); } - var gameMode = GetGameMode(gameModeOptions.SelectedGameModeType); - gameMode.UpdateWorld(world, seed, gameModeOptions); return world; } public string? GetGameStartText(World world) { var gameModeOptions = world.Config.GameModeOptions; - - if (gameModeOptions.SelectedGameModeType == GameModeType.Vanilla) - { - return null; - } - var gameMode = GetGameMode(gameModeOptions.SelectedGameModeType); return gameMode.GetGameStartText(world); } - public AltGameModeInGameText? GetInGameText(World world) + public GameModeInGameText GetInGameText(World world) { - if (world.Config.GameModeOptions.SelectedGameModeType == GameModeType.Vanilla) - { - return null; - } - var gameMode = GetGameMode(world.Config.GameModeOptions.SelectedGameModeType); return gameMode.GetInGameText(world); } @@ -95,45 +86,47 @@ public Dictionary GetGameModeDescriptions() return s_gameModeDescriptions; } - public void UpdateInitialTrackerState(GameModeOptions gameModeOptions, TrackerState trackerState) + public void UpdateInitialTrackerState(GameModeOptions gameModeOptions, TrackerState trackerState, ParsedRomDetails? parsedRomDetails) { - if (gameModeOptions.SelectedGameModeType == GameModeType.Vanilla) + var gameMode = GetGameMode(gameModeOptions.SelectedGameModeType); + gameMode.UpdateInitialTrackerState(gameModeOptions, trackerState, parsedRomDetails); + + int? markedGanonsTowerCrystalCount = null; + + if (parsedRomDetails == null) { - return; + if (gameModeOptions.RandomizeNumericAmounts) + { + markedGanonsTowerCrystalCount = + gameModeOptions.MinGanonsTowerCrystalCount == gameModeOptions.MaxGanonsTowerCrystalCount + ? gameModeOptions.MinGanonsTowerCrystalCount + : null; + } + else + { + markedGanonsTowerCrystalCount = gameModeOptions.GanonsTowerCrystalCount; + } } - var gameMode = GetGameMode(gameModeOptions.SelectedGameModeType); - gameMode.UpdateInitialTrackerState(gameModeOptions, trackerState); + trackerState.MarkedGanonsTowerCrystalCount = markedGanonsTowerCrystalCount; } public string GetSpoilerText(GameModeOptions gameModeOptions) { - - string goalText; - - if (gameModeOptions.SelectedGameModeType == GameModeType.Vanilla) - { - goalText = - $"GanonCrystalCount = {gameModeOptions.GanonCrystalCount}, TourianBossCount = {gameModeOptions.TourianBossCount}"; - } - else - { - var gameMode = GetGameMode(gameModeOptions.SelectedGameModeType); - goalText = gameMode.GetSpoilerText(gameModeOptions); - } - - return - $"Goal: {gameModeOptions.SelectedGameModeType} | {goalText}, LiftOffOnGoalCompletion = {gameModeOptions.LiftOffOnGoalCompletion}"; + var gameMode = GetGameMode(gameModeOptions.SelectedGameModeType); + var goalText = gameMode.GetSpoilerText(gameModeOptions); + return $"Goal: {gameModeOptions.SelectedGameModeType} | {goalText}, LiftOffOnGoalCompletion = {gameModeOptions.LiftOffOnGoalCompletion}"; } public List? GetGameModeLocations(World world, List allWorlds) { - if (world.Config.GameModeOptions.SelectedGameModeType == GameModeType.Vanilla) - { - return null; - } - var gameMode = GetGameMode(world.Config.GameModeOptions.SelectedGameModeType); return gameMode.GetGameModeLocations(world, allWorlds); } + + public Stream GetLiftOffOnGoalCompletionIpsPatch(GameModeOptions gameModeOptions) + { + var gameMode = GetGameMode(gameModeOptions.SelectedGameModeType); + return gameMode.GetLiftOffOnGoalCompletionIpsPatch(); + } } diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/AltGameModes/SpazerHuntAltGameMode.cs b/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/SpazerHuntGameMode.cs similarity index 84% rename from src/TrackerCouncil.Smz3.SeedGenerator/AltGameModes/SpazerHuntAltGameMode.cs rename to src/TrackerCouncil.Smz3.SeedGenerator/GameModes/SpazerHuntGameMode.cs index 631b6035..6b984dfe 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/AltGameModes/SpazerHuntAltGameMode.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/SpazerHuntGameMode.cs @@ -1,23 +1,25 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.IO; using System.Linq; using TrackerCouncil.Smz3.Abstractions; using TrackerCouncil.Smz3.Data.GeneratedData; using TrackerCouncil.Smz3.Data.Options; +using TrackerCouncil.Smz3.Data.ParsedRom; using TrackerCouncil.Smz3.Data.Services; using TrackerCouncil.Smz3.Data.Tracking; using TrackerCouncil.Smz3.Data.ViewModels; using TrackerCouncil.Smz3.Data.WorldData; -using TrackerCouncil.Smz3.Shared; +using TrackerCouncil.Smz3.SeedGenerator.FileData.IpsPatches; using TrackerCouncil.Smz3.Shared.Enums; using TrackerCouncil.Smz3.Shared.Models; -namespace TrackerCouncil.Smz3.SeedGenerator.AltGameModes; +namespace TrackerCouncil.Smz3.SeedGenerator.GameModes; [GameModeType(GameModeType.SpazerHunt)] [Description("You thought they were worthless, but Samus needs to collect all of the spazers and use them to save the day. After all, who needs a Triforce when you have lots of firepower?")] -public class SpazerHuntAltGameMode(IMetadataService metadata) : AltGameModeBase +public class SpazerHuntGameMode(IMetadataService metadata) : GameModeBase { private TrackerBase _tracker = null!; @@ -31,7 +33,7 @@ public override bool IsComplete(World world) return GetCurrentCount(world) >= world.Config.GameModeOptions.SpazersRequired; } - public override void UpdateInitialTrackerState(GameModeOptions gameModeOptions, TrackerState trackerState) + public override void UpdateInitialTrackerState(GameModeOptions gameModeOptions, TrackerState trackerState, ParsedRomDetails? parsedRomDetails) { if (gameModeOptions.RandomizeNumericAmounts) { @@ -43,14 +45,15 @@ public override void UpdateInitialTrackerState(GameModeOptions gameModeOptions, } } - public override void UpdateWorld(World world, int seed, GameModeOptions gameModeOptions) + public override void UpdateWorld(World world, Random rng, GameModeOptions gameModeOptions) { - var rng = new Random(seed); - rng.Sanitize(); - var swaps = new List(); var spazersInPool = gameModeOptions.SpazersInPool; + + gameModeOptions.GanonCrystalCount = 7; + gameModeOptions.TourianBossCount = 4; + if (gameModeOptions.RandomizeNumericAmounts) { spazersInPool = rng.Next(gameModeOptions.MinSpazersInPool, gameModeOptions.MaxSpazersInPool + 1); @@ -174,22 +177,22 @@ public override void InitializeTracker(TrackerBase tracker) _tracker.ModeTracker.MultiplayerSyncReceived += ModeTrackerOnMultiplayerSyncReceived; } - public override AltGameModeInGameText? GetInGameText(World world) + public override GameModeInGameText GetInGameText(World world) { var spazersRequired = world.Config.GameModeOptions.SpazersRequired; var spazersInPool = world.Config.GameModeOptions.SpazersInPool; - return new AltGameModeInGameText + return new GameModeInGameText { - GanonIntro = metadata.AltGameModes.SpazerHuntGanonIntro?.Format(spazersRequired, spazersInPool), + GanonIntro = metadata.GameModes.SpazerHuntGanonIntro, GanonIntroGoalsNotMet = - metadata.AltGameModes.SpazerHuntGanonIntroGoalsNotMet?.Format(spazersRequired, spazersInPool), - GanonGoalSign = metadata.AltGameModes.SpazerHuntGanonGoalSign?.Format(spazersRequired, spazersInPool), + metadata.GameModes.SpazerHuntGanonIntroGoalsNotMet?.Format(spazersRequired, spazersInPool), + GanonGoalSign = metadata.GameModes.SpazerHuntGanonGoalSign?.Format(spazersRequired, spazersInPool), }; } public override string? GetGameStartText(World world) { - return metadata.AltGameModes.SpazerHuntGameStarted?.ToString(); + return metadata.GameModes.SpazerHuntGameStarted?.ToString(); } public override bool IsKnowinglyComplete(World world) @@ -213,11 +216,11 @@ public override bool OnViewingPyramidText(World world) return allWorlds.SelectMany(w => w.Locations).Where(l => l.ItemType is ItemType.Spazer && l.Item.World.Id == world.Id).ToList(); } - public override List? GetGoalUiDetails(World world) + public override List GetGoalUiDetails(World world, Progression progression) { if (world.State == null) { - return null; + return []; } var spazerCount = GetCurrentCount(world); @@ -234,6 +237,11 @@ public override string GetSpoilerText(GameModeOptions gameModeOptions) $"SpazersRequired = {gameModeOptions.SpazersRequired}, SpazersInPool = {gameModeOptions.SpazersInPool}"; } + public override Stream GetLiftOffOnGoalCompletionIpsPatch() + { + return IpsPatch.LiftOffOnGoalCompletionAlt(); + } + private int GetCurrentCount(World world) { var allSpazerLocations = world.State?.LocationStates @@ -250,10 +258,10 @@ private void LocationTrackerOnLocationCleared(object? sender, LocationClearedEve if (IsComplete(_tracker.World)) { - _tracker.AltGameModeService.MarkAltGameModeAsComplete(); + _tracker.GameModeService.MarkGameModeAsComplete(); } - _tracker.AltGameModeService.NotifyOfGoalStateChange(); + _tracker.GameModeService.NotifyOfGoalStateChange(); } private void ItemTrackerOnItemTracked(object? sender, ItemTrackedEventArgs e) @@ -265,19 +273,19 @@ private void ItemTrackerOnItemTracked(object? sender, ItemTrackedEventArgs e) if (IsComplete(_tracker.World)) { - _tracker.AltGameModeService.MarkAltGameModeAsComplete(); + _tracker.GameModeService.MarkGameModeAsComplete(); } - _tracker.AltGameModeService.NotifyOfGoalStateChange(); + _tracker.GameModeService.NotifyOfGoalStateChange(); } private void ModeTrackerOnMultiplayerSyncReceived(object? sender, EventArgs e) { if (IsComplete(_tracker.World)) { - _tracker.AltGameModeService.MarkAltGameModeAsComplete(); + _tracker.GameModeService.MarkGameModeAsComplete(); } - _tracker.AltGameModeService.NotifyOfGoalStateChange(); + _tracker.GameModeService.NotifyOfGoalStateChange(); } } diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/VanillaGameMode.cs b/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/VanillaGameMode.cs new file mode 100644 index 00000000..b052baf4 --- /dev/null +++ b/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/VanillaGameMode.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using TrackerCouncil.Smz3.Abstractions; +using TrackerCouncil.Smz3.Data.GeneratedData; +using TrackerCouncil.Smz3.Data.Options; +using TrackerCouncil.Smz3.Data.ParsedRom; +using TrackerCouncil.Smz3.Data.Services; +using TrackerCouncil.Smz3.Data.ViewModels; +using TrackerCouncil.Smz3.Data.WorldData; +using TrackerCouncil.Smz3.SeedGenerator.FileData.IpsPatches; +using TrackerCouncil.Smz3.Shared.Enums; +using TrackerCouncil.Smz3.Shared.Models; + +namespace TrackerCouncil.Smz3.SeedGenerator.GameModes; + +[GameModeType(GameModeType.Vanilla)] +[Description("Complete Zelda dungeons to obtain crystals to be able to defeat Ganon. Defeat bosses in Super Metroid to be able to enter Tourian and defeat Mother Brain.")] +public class VanillaGameMode(IMetadataService metadata) : GameModeBase +{ + private TrackerBase _tracker = null!; + + public override void InitializeTracker(TrackerBase tracker) + { + _tracker = tracker; + _tracker.RewardTracker.RewardsChanged += RewardTrackerOnRewardsChanged; + } + + public override bool IsComplete(World world) + { + if (world.State == null) + { + return false; + } + + var progression = _tracker.PlayerProgressionService.GetProgression(false); + + return progression.CrystalCount >= world.Config.GameModeOptions.GanonCrystalCount && + progression.MetroidBossCount >= world.Config.GameModeOptions.TourianBossCount; + } + + public override void UpdateWorld(World world, Random rng, GameModeOptions gameModeOptions) + { + if (gameModeOptions.RandomizeNumericAmounts) + { + gameModeOptions.GanonCrystalCount = rng.Next(gameModeOptions.MinGanonCrystalCount, gameModeOptions.MaxGanonCrystalCount + 1); + gameModeOptions.TourianBossCount = rng.Next(gameModeOptions.MinTourianBossCount, gameModeOptions.MaxTourianBossCount + 1); + } + } + + public override GameModeInGameText GetInGameText(World world) + { + var ganonCrystalCount = world.Config.GameModeOptions.GanonCrystalCount; + return new GameModeInGameText + { + GanonIntro = metadata.GameLines.GanonIntro, + GanonIntroGoalsNotMet = metadata.GameLines.GanonIntroGoalsNotMet, + GanonGoalSign = metadata.GameLines.GanonGoalSign?.Format(ganonCrystalCount == 1 ? "1 crystal" : $"{ganonCrystalCount} crystals"), + }; + } + + public override void UpdateInitialTrackerState(GameModeOptions gameModeOptions, TrackerState trackerState, ParsedRomDetails? parsedRomDetails) + { + int? markedGanonCrystalCount = null; + int? markedTourianBossCount = null; + + if (parsedRomDetails == null) + { + if (gameModeOptions.RandomizeNumericAmounts) + { + markedGanonCrystalCount = + gameModeOptions.MinGanonCrystalCount == gameModeOptions.MaxGanonCrystalCount + ? gameModeOptions.MinGanonCrystalCount + : null; + markedTourianBossCount = + gameModeOptions.MinTourianBossCount == gameModeOptions.MaxTourianBossCount + ? gameModeOptions.MinTourianBossCount + : null; + } + else + { + markedGanonCrystalCount = gameModeOptions.GanonCrystalCount; + markedTourianBossCount = gameModeOptions.TourianBossCount; + } + } + + trackerState.MarkedGanonCrystalCount = markedGanonCrystalCount; + trackerState.MarkedTourianBossCount = markedTourianBossCount; + } + + public override List GetGoalUiDetails(World world, Progression progression) + { + var crystals = progression.CrystalCount; + var bosses = progression.MetroidBossCount; + var crystalsRequired = world.State?.MarkedGanonCrystalCount != null + ? $"{world.State?.MarkedGanonCrystalCount}" + : "?"; + var bossesRequired = world.State?.MarkedTourianBossCount != null + ? $"{world.State?.MarkedTourianBossCount}" + : "?"; + return + [ + new GoalUiDetails { IconCategory = "Dungeons", Icon = "crystal.png", Text = $"{crystals}/{crystalsRequired}" }, + new GoalUiDetails { IconCategory = "Dungeons", Icon = "metroid boss token.png", Text = $"{bosses}/{bossesRequired}" } + ]; + } + + public override string GetSpoilerText(GameModeOptions gameModeOptions) + { + return $"GanonCrystalCount = {gameModeOptions.GanonCrystalCount}, TourianBossCount = {gameModeOptions.TourianBossCount}"; + } + + public override Stream GetLiftOffOnGoalCompletionIpsPatch() + { + return IpsPatch.LiftOffOnGoalCompletionPrimary(); + } + + public override bool IsKnowinglyComplete(World world) + { + return world.State?.MarkedGanonCrystalCount == world.Config.GameModeOptions.GanonCrystalCount && + world.State?.MarkedTourianBossCount == world.Config.GameModeOptions.TourianBossCount && + IsComplete(world); + } + + public override bool OnViewingPyramidText(World world) + { + if (world.State != null && world.State.MarkedGanonCrystalCount != world.Config.GameModeOptions.GanonCrystalCount) + { + _tracker.GameStateTracker.UpdateGanonRequirement(world.State?.GanonCrystalCount ?? world.Config.GameModeOptions.GanonCrystalCount, true); + return true; + } + + return false; + } + + private void RewardTrackerOnRewardsChanged(object? sender, EventArgs e) + { + if (IsComplete(_tracker.World)) + { + _tracker.GameModeService.MarkGameModeAsComplete(); + } + + _tracker.GameModeService.NotifyOfGoalStateChange(); + } +} diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/GameHintService.cs b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/GameHintService.cs index 1e70d3c2..cb473c19 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/GameHintService.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/GameHintService.cs @@ -10,8 +10,8 @@ using TrackerCouncil.Smz3.Data.Services; using TrackerCouncil.Smz3.Data.WorldData; using TrackerCouncil.Smz3.Data.WorldData.Regions; -using TrackerCouncil.Smz3.SeedGenerator.AltGameModes; using TrackerCouncil.Smz3.SeedGenerator.Contracts; +using TrackerCouncil.Smz3.SeedGenerator.GameModes; using TrackerCouncil.Smz3.SeedGenerator.Infrastructure; using TrackerCouncil.Smz3.Shared.Enums; using TrackerCouncil.Smz3.Shared.Models; @@ -27,7 +27,7 @@ public class GameHintService( IMetadataService metadataService, ILogger logger, PlaythroughService playthroughService, - AltGameModeFactory altgameModeFactory) + GameModeFactory gameModeFactory) : IGameHintService { private readonly GameLinesConfig _gameLines = metadataService.GameLines; @@ -66,7 +66,7 @@ public void GetInGameHints(World hintPlayerWorld, List allWorlds, Playthr } _random = new Random(seed).Sanitize(); - var goalLocations = allWorlds.SelectMany(x => altgameModeFactory.GetGameModeLocations(x, allWorlds) ?? []).ToList(); + var goalLocations = allWorlds.SelectMany(x => gameModeFactory.GetGameModeLocations(x, allWorlds) ?? []).ToList(); var allHints = new List(); @@ -131,7 +131,7 @@ public void GetInGameHints(World hintPlayerWorld, List allWorlds, Playthr return null; } - goalLocations ??= allWorlds.SelectMany(x => altgameModeFactory.GetGameModeLocations(x, allWorlds) ?? []).ToList(); + goalLocations ??= allWorlds.SelectMany(x => gameModeFactory.GetGameModeLocations(x, allWorlds) ?? []).ToList(); var locationUsefulness = new ConcurrentBag<(Location Location, LocationUsefulness Usefulness)>(); @@ -148,7 +148,7 @@ public void GetInGameHints(World hintPlayerWorld, List allWorlds, Playthr public LocationUsefulness GetUsefulness(List locations, List allWorlds, Reward? ignoredReward, List? goalLocations = null) { - goalLocations ??= allWorlds.SelectMany(x => altgameModeFactory.GetGameModeLocations(x, allWorlds) ?? []).ToList(); + goalLocations ??= allWorlds.SelectMany(x => gameModeFactory.GetGameModeLocations(x, allWorlds) ?? []).ToList(); return CheckIfLocationsAreImportant(allWorlds, locations, goalLocations, ignoredReward); } @@ -458,6 +458,11 @@ private LocationUsefulness VerifyWorld(Playthrough.Sphere sphere, World world, L { return LocationUsefulness.Mandatory; } + // If all dungeons game mode, make sure the player can get the required crystal counts + if (world.Config.GameModeOptions.SelectedGameModeType == GameModeType.AllDungeons && (!progression.AllCrystals || !progression.AllPendants || progression.MetroidBossCount < 4 || !worldLocations.ContainsKey(LocationId.GanonsTowerMoldormChest))) + { + return LocationUsefulness.Mandatory; + } // If it's a different game mode, return that it's goal blocking if it's either a goal location or prevents access to a goal location else if (world.Config.GameModeOptions.SelectedGameModeType != GameModeType.Vanilla && goalLocations?.Any(x => !x.IsAvailable(progression) || checkedLocations.Any(y => y.Id == x.Id && y.World == x.World)) == true) diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomGenerationService.cs b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomGenerationService.cs index c5ff78a6..9b05a337 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomGenerationService.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomGenerationService.cs @@ -15,9 +15,9 @@ using TrackerCouncil.Smz3.Data.Options; using TrackerCouncil.Smz3.Data.ParsedRom; using TrackerCouncil.Smz3.Data.Services; -using TrackerCouncil.Smz3.SeedGenerator.AltGameModes; using TrackerCouncil.Smz3.SeedGenerator.FileData; using TrackerCouncil.Smz3.SeedGenerator.FileData.IpsPatches; +using TrackerCouncil.Smz3.SeedGenerator.GameModes; using TrackerCouncil.Smz3.Shared.Enums; using TrackerCouncil.Smz3.Shared.Models; @@ -35,7 +35,7 @@ public class RomGenerationService( IMsuSelectorService? msuSelectorService, IMsuTypeService? msuTypeService, RomTextService romTextService, - AltGameModeFactory altGameModeFactory) + GameModeFactory gameModeFactory) : IRomGenerationService { private readonly ILogger _logger = logger; @@ -174,9 +174,9 @@ public byte[] GenerateRomBytes(RandomizerOptions options, SeedData? seed, Parsed if (seed is { WorldGenerationData.LocalWorld.Config.GameModeOptions.LiftOffOnGoalCompletion: true }) { - using var goalPatch = seed.WorldGenerationData.LocalWorld.Config.GameModeOptions.SelectedGameModeType == GameModeType.Vanilla - ? IpsPatch.LiftOffOnGoalCompletionPrimary() - : IpsPatch.LiftOffOnGoalCompletionAlt(); + using var goalPatch = + gameModeFactory.GetLiftOffOnGoalCompletionIpsPatch(seed.WorldGenerationData.LocalWorld.Config + .GameModeOptions); Rom.ApplySuperMetroidIps(rom, goalPatch); } @@ -392,7 +392,7 @@ public async Task GeneratePreSeededRomAsync(RandomizerOption romTextService.PrepareAutoTrackerFiles(options); - var rom = await SaveSeedToDatabaseAsync(options, seed, romPath, spoilerPath, multiplayerGameDetails); + var rom = await SaveSeedToDatabaseAsync(options, seed, romPath, spoilerPath, multiplayerGameDetails, parsedRomDetails); try { @@ -415,7 +415,7 @@ public async Task GeneratePreSeededRomAsync(RandomizerOption /// The path to the spoiler file /// Details of the connected multiplayer game /// The db entry for the generated rom - private async Task SaveSeedToDatabaseAsync(RandomizerOptions options, SeedData seed, string romPath, string spoilerPath, MultiplayerGameDetails? multiplayerGameDetails) + private async Task SaveSeedToDatabaseAsync(RandomizerOptions options, SeedData seed, string romPath, string spoilerPath, MultiplayerGameDetails? multiplayerGameDetails, ParsedRomDetails? parsedRomDetails) { var settingsString = seed.Configs.Count() > 1 ? Config.ToConfigString(seed.Configs) @@ -445,7 +445,7 @@ private async Task SaveSeedToDatabaseAsync(RandomizerOptions optio if (rom.TrackerState != null) { - altGameModeFactory.UpdateInitialTrackerState(seed.WorldGenerationData.LocalWorld.Config.GameModeOptions, rom.TrackerState); + gameModeFactory.UpdateInitialTrackerState(seed.WorldGenerationData.LocalWorld.Config.GameModeOptions, rom.TrackerState, parsedRomDetails); await dbContext.SaveChangesAsync(); } diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomTextService.cs b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomTextService.cs index db20a629..3917d362 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomTextService.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomTextService.cs @@ -10,14 +10,14 @@ using TrackerCouncil.Smz3.Data.GeneratedData; using TrackerCouncil.Smz3.Data.Options; using TrackerCouncil.Smz3.Data.WorldData.Regions; -using TrackerCouncil.Smz3.SeedGenerator.AltGameModes; using TrackerCouncil.Smz3.SeedGenerator.Contracts; +using TrackerCouncil.Smz3.SeedGenerator.GameModes; using TrackerCouncil.Smz3.Shared; using TrackerCouncil.Smz3.Shared.Enums; namespace TrackerCouncil.Smz3.SeedGenerator.Generation; -public class RomTextService(ILogger logger, IGameHintService gameHintService, ISnesConnectorService snesConnectorService, AltGameModeFactory altGameModeFactory) +public class RomTextService(ILogger logger, IGameHintService gameHintService, ISnesConnectorService snesConnectorService, GameModeFactory gameModeFactory) { private static readonly string s_plandoSchemaPath = @"https://raw.githubusercontent.com/TheTrackerCouncil/SMZ3CasConfigs/refs/heads/main/Schemas/plando.json"; @@ -197,7 +197,7 @@ private string GetSpoilerLog(RandomizerOptions options, SeedData seed, bool conf if (!isParsedRom) { - log.AppendLine(altGameModeFactory.GetSpoilerText(gameModeOptions)); + log.AppendLine(gameModeFactory.GetSpoilerText(gameModeOptions)); } string keysanityOptions; diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/Smz3Randomizer.cs b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/Smz3Randomizer.cs index 1f2b3da6..bbd345f9 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/Smz3Randomizer.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/Smz3Randomizer.cs @@ -9,9 +9,9 @@ using TrackerCouncil.Smz3.Data.Options; using TrackerCouncil.Smz3.Data.WorldData; using TrackerCouncil.Smz3.Data.WorldData.Regions; -using TrackerCouncil.Smz3.SeedGenerator.AltGameModes; using TrackerCouncil.Smz3.SeedGenerator.Contracts; using TrackerCouncil.Smz3.SeedGenerator.FileData; +using TrackerCouncil.Smz3.SeedGenerator.GameModes; using TrackerCouncil.Smz3.SeedGenerator.Infrastructure; using TrackerCouncil.Smz3.Shared.Enums; @@ -24,7 +24,7 @@ public class Smz3Randomizer( ILogger logger, IPatcherService patcherService, PlaythroughService playthroughService, - AltGameModeFactory altGameModeFactory) + GameModeFactory gameModeFactory) : ISeededRandomizer { public static string Name => "Super Metroid & A Link to the Past Cas’ Randomizer"; @@ -54,7 +54,7 @@ public SeedData GenerateSeed(List configs, string? seed, CancellationTok var worlds = new List(); if (primaryConfig.SingleWorld) { - worlds.Add(altGameModeFactory.UpdateWorld(new World(primaryConfig, "Player", 0, + worlds.Add(gameModeFactory.UpdateWorld(new World(primaryConfig, "Player", 0, Guid.NewGuid().ToString("N")), seedNumber)); logger.LogDebug( "Seed: {SeedNumber} | Race: {PrimaryConfigRace} | Keysanity: {PrimaryConfigKeysanityMode} | Item placement: {PrimaryConfigItemPlacementRule}", @@ -63,7 +63,7 @@ public SeedData GenerateSeed(List configs, string? seed, CancellationTok else { worlds.AddRange(configs.OrderBy(x => x.Id).Select(config => - altGameModeFactory.UpdateWorld(new World(config, config.PlayerName, config.Id, config.PlayerGuid, config.IsLocalConfig), rng.Next()))); + gameModeFactory.UpdateWorld(new World(config, config.PlayerName, config.Id, config.PlayerGuid, config.IsLocalConfig), rng.Next()))); logger.LogDebug( "Seed: {SeedNumber} | Race: {PrimaryConfigRace} | World Count: {Count}", seedNumber, primaryConfig.Race, configs.Count); diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/Smz3RomParser.cs b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/Smz3RomParser.cs index 63b3f2fc..5ed6a681 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/Smz3RomParser.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/Smz3RomParser.cs @@ -121,6 +121,7 @@ public ParsedRomDetails ParseRomFile(string filePath) var ganonCrystalCount = GoalsPatch.GetGanonCrystalCountFromRom(rom); var tourianBossCount = GoalsPatch.GetTourianBossCountFromRom(rom, romGenerator == RomGenerator.Cas); var openPyramid = GoalsPatch.GetOpenPyramid(rom); + var gameModeType = GoalsPatch.GetGameModeType(rom); var text = ZeldaTextsPatch.ParseRomText(rom); var spinJumpAnimations = romGenerator != RomGenerator.Cas && MetadataPatch.GetSpinJumpAnimationsFromRom(rom); @@ -149,7 +150,8 @@ public ParsedRomDetails ParseRomFile(string filePath) Prerequisites = prerequisites, StartingItems = startingItems, ParsedText = text, - SpinJumpAnimations = spinJumpAnimations + SpinJumpAnimations = spinJumpAnimations, + GameModeType = gameModeType }; } catch (Exception e) @@ -174,6 +176,7 @@ public SeedData GenerateSeedData(RandomizerOptions options, ParsedRomDetails par config.GameModeOptions.TourianBossCount = parsedRomDetails.TourianBossCount; config.GameModeOptions.OpenPyramid = parsedRomDetails.OpenPyarmid; config.GameModeOptions.SkipTourianBossDoor = parsedRomDetails.SkipTourianBossDoor; + config.GameModeOptions.SelectedGameModeType = parsedRomDetails.GameModeType; config.GameModeOptions.ShuffleMetroidBossTokens = true; config.LocationItems.Clear(); config.ItemOptions = parsedRomDetails.StartingItems.ToDictionary(x => $"ItemType:{x.Key}", x => x.Value); diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/RandomizerServiceCollectionExtensions.cs b/src/TrackerCouncil.Smz3.SeedGenerator/RandomizerServiceCollectionExtensions.cs index 45f115ec..56051966 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/RandomizerServiceCollectionExtensions.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/RandomizerServiceCollectionExtensions.cs @@ -3,9 +3,9 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using TrackerCouncil.Smz3.Abstractions; using TrackerCouncil.Smz3.Data.Interfaces; -using TrackerCouncil.Smz3.SeedGenerator.AltGameModes; using TrackerCouncil.Smz3.SeedGenerator.Contracts; using TrackerCouncil.Smz3.SeedGenerator.FileData.Patches; +using TrackerCouncil.Smz3.SeedGenerator.GameModes; using TrackerCouncil.Smz3.SeedGenerator.Generation; using TrackerCouncil.Smz3.SeedGenerator.Infrastructure; using TrackerCouncil.Smz3.Shared.Models; @@ -17,7 +17,7 @@ public static class RandomizerServiceCollectionExtensions public static IServiceCollection AddRandomizerServices(this IServiceCollection services) { services.AddRomPatches(); - services.AddGameModes(); + services.AddGameModes(); services.AddSingleton(); services.AddGeneratedRomLoader(); services.AddSmz3Randomizer(); @@ -88,15 +88,15 @@ private static IServiceCollection AddRomPatches(this IServiceCollecti private static IServiceCollection AddGameModes(this IServiceCollection services) { var moduleTypes = typeof(TAssembly).Assembly.GetTypes() - .Where(x => x.IsSubclassOf(typeof(AltGameModeBase))); + .Where(x => x.IsSubclassOf(typeof(GameModeBase))); foreach (var moduleType in moduleTypes) { services.TryAddScoped(moduleType); - AltGameModeFactory.AddGameModeClass(moduleType); + GameModeFactory.AddGameModeClass(moduleType); } - services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/TrackerCouncil.Smz3.SeedGenerator.csproj b/src/TrackerCouncil.Smz3.SeedGenerator/TrackerCouncil.Smz3.SeedGenerator.csproj index c6bf4a41..9bdf743d 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/TrackerCouncil.Smz3.SeedGenerator.csproj +++ b/src/TrackerCouncil.Smz3.SeedGenerator/TrackerCouncil.Smz3.SeedGenerator.csproj @@ -49,6 +49,7 @@ + diff --git a/src/TrackerCouncil.Smz3.Shared/Enums/GameModeType.cs b/src/TrackerCouncil.Smz3.Shared/Enums/GameModeType.cs index 7068968c..2b7a6211 100644 --- a/src/TrackerCouncil.Smz3.Shared/Enums/GameModeType.cs +++ b/src/TrackerCouncil.Smz3.Shared/Enums/GameModeType.cs @@ -7,6 +7,9 @@ public enum GameModeType [Description("Collect crystals and defeat Metroid bosses")] Vanilla, + [Description("All dungeons and defeat Metroid bosses")] + AllDungeons, + [Description("Spazer Hunt (auto tracking required)")] SpazerHunt } diff --git a/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTrackerModules/GameMonitor.cs b/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTrackerModules/GameMonitor.cs index 77cfb329..ad6a71fa 100644 --- a/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTrackerModules/GameMonitor.cs +++ b/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTrackerModules/GameMonitor.cs @@ -10,13 +10,13 @@ using SNI; using TrackerCouncil.Smz3.Abstractions; using TrackerCouncil.Smz3.Data.Tracking; -using TrackerCouncil.Smz3.SeedGenerator.AltGameModes; +using TrackerCouncil.Smz3.SeedGenerator.GameModes; using TrackerCouncil.Smz3.Shared.Enums; using TrackerCouncil.Smz3.Tracking.Services; namespace TrackerCouncil.Smz3.Tracking.AutoTracking.AutoTrackerModules; -public class GameMonitor(TrackerBase tracker, ISnesConnectorService snesConnector, ILogger logger, IWorldQueryService worldQueryService, AltGameModeFactory altGameModeFactory) : AutoTrackerModule(tracker, snesConnector, logger) +public class GameMonitor(TrackerBase tracker, ISnesConnectorService snesConnector, ILogger logger, IWorldQueryService worldQueryService, GameModeFactory gameModeFactory) : AutoTrackerModule(tracker, snesConnector, logger) { private bool bIsCheckingGameStart; @@ -52,7 +52,7 @@ public override void Initialize() }); // If the player is using an alt game mode, make sure the flag is set - if (worldQueryService.World.Config.GameModeOptions.SelectedGameModeType != GameModeType.Vanilla) + if (worldQueryService.World.Config.GameModeOptions.IsAltGameMode()) { SnesConnector.AddRecurringMemoryRequest(new SnesRecurringMemoryRequest() { @@ -64,7 +64,7 @@ public override void Initialize() Length = 1, OnResponse = CheckAltGameModeFlag, FrequencySeconds = 15, - Filter = () => HasValidState && Tracker.AltGameModeService.IsAltGameModeComplete + Filter = () => HasValidState && Tracker.GameModeService.IsGameModeComplete }); } } @@ -165,7 +165,7 @@ private void MarkAsStarted() if (bIsFirstStart) { - var gameModeString = altGameModeFactory.GetGameStartText(Tracker.World); + var gameModeString = gameModeFactory.GetGameStartText(Tracker.World); if (!string.IsNullOrEmpty(gameModeString)) { Tracker.Say(text: gameModeString); diff --git a/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTrackerModules/PlayerEnteredShip.cs b/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTrackerModules/PlayerEnteredShip.cs index 12e777d4..bbc82ba4 100644 --- a/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTrackerModules/PlayerEnteredShip.cs +++ b/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTrackerModules/PlayerEnteredShip.cs @@ -20,7 +20,7 @@ public override void Initialize() Address = 0x7e0FB2, Length = 0x2, OnResponse = CheckShip, - Filter = () => IsInMetroid && (AutoTracker.HasDefeatedBothBosses || (Tracker.GameStateTracker.HasFinishedGoal && Tracker.LocalConfig?.GameModeOptions.LiftOffOnGoalCompletion == true)) + Filter = () => IsInMetroid && (AutoTracker.HasDefeatedBothBosses || (Tracker.GameModeService.IsGameModeComplete && Tracker.LocalConfig?.GameModeOptions.LiftOffOnGoalCompletion == true)) }); } diff --git a/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTrackerModules/ZeldaMiscData.cs b/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTrackerModules/ZeldaMiscData.cs index bf9eb3e9..3699136e 100644 --- a/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTrackerModules/ZeldaMiscData.cs +++ b/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTrackerModules/ZeldaMiscData.cs @@ -102,6 +102,13 @@ private void CheckZeldaMisc(SnesData data, SnesData? prevData) AutoTracker.PlayerHasFairy = hasFairy; + var openPyramid = data.CheckUInt8Flag(0x5B, 0x20); + if (openPyramid != AutoTracker.OpenPyramid) + { + AutoTracker.OpenPyramid = openPyramid; + AutoTracker.OnOpenPyramidChanged(); + } + // Activated flute if (data.CheckUInt8Flag(0x10C, 0x01) && !prevData.CheckUInt8Flag(0x10C, 0x01)) { diff --git a/src/TrackerCouncil.Smz3.Tracking/AutoTracking/ZeldaStateChecks/ViewedText.cs b/src/TrackerCouncil.Smz3.Tracking/AutoTracking/ZeldaStateChecks/ViewedText.cs index 8e825c65..2762b0cc 100644 --- a/src/TrackerCouncil.Smz3.Tracking/AutoTracking/ZeldaStateChecks/ViewedText.cs +++ b/src/TrackerCouncil.Smz3.Tracking/AutoTracking/ZeldaStateChecks/ViewedText.cs @@ -80,15 +80,7 @@ public bool ExecuteCheck(TrackerBase tracker, AutoTrackerZeldaState currentState } else if (currentState.OverworldScreen == 91 && currentState.IsWithinRegion(1961, 1784, 1976, 1785)) { - if (tracker.LocalConfig?.GameModeOptions.SelectedGameModeType == GameModeType.Vanilla && - World.State?.GanonCrystalCount != World.State?.MarkedGanonCrystalCount) - { - tracker.GameStateTracker.UpdateGanonRequirement(World.State?.GanonCrystalCount ?? World.Config.GameModeOptions.GanonCrystalCount, true); - } - else if (tracker.AltGameModeService.HasAltGameMode) - { - tracker.AltGameModeService.OnViewingPyramidText(); - } + tracker.GameModeService.OnViewingPyramidText(); } // Viewed Bombos tablet text else if (currentState is { OverworldScreen: 48, LinkState: 0 } && diff --git a/src/TrackerCouncil.Smz3.Tracking/Tracker.cs b/src/TrackerCouncil.Smz3.Tracking/Tracker.cs index d6d7757d..4488c89a 100644 --- a/src/TrackerCouncil.Smz3.Tracking/Tracker.cs +++ b/src/TrackerCouncil.Smz3.Tracking/Tracker.cs @@ -731,8 +731,8 @@ public override void UpdateAllAccessibility(bool forceRefreshAll, params Item[] var actualProgression = PlayerProgressionService.GetProgression(false); var assumedKeysProgression = PlayerProgressionService.GetProgression(true); - var hasAltGameMode = AltGameModeService.HasAltGameMode; - var isAltGameModeComplete = hasAltGameMode && AltGameModeService.IsAltGameModeKnowinglyComplete(); + var hasAltGameMode = GameModeService.HasAltGameMode; + var isAltGameModeComplete = hasAltGameMode && GameModeService.IsGameModeKnowinglyComplete(); World.DarkWorldNorthEast.UpdateGanonAccessibility(actualProgression, assumedKeysProgression, hasAltGameMode, isAltGameModeComplete); World.WestCrateria.UpdateMotherBrainAccessibility(actualProgression, hasAltGameMode, isAltGameModeComplete); diff --git a/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerAltGameModeService.cs b/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerAltGameModeService.cs deleted file mode 100644 index 5982fa0a..00000000 --- a/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerAltGameModeService.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections.Generic; -using TrackerCouncil.Smz3.Abstractions; -using TrackerCouncil.Smz3.Data.Services; -using TrackerCouncil.Smz3.Data.ViewModels; -using TrackerCouncil.Smz3.SeedGenerator.AltGameModes; -using TrackerCouncil.Smz3.SeedGenerator.Contracts; -using TrackerCouncil.Smz3.Shared.Enums; - -namespace TrackerCouncil.Smz3.Tracking.TrackingServices; - -internal class TrackerAltGameModeService(AltGameModeFactory altGameModeFactory, IWorldAccessor worldAccessor) : TrackerService, ITrackerAltGameModeService -{ - private GameModeType _gameModeType = GameModeType.Vanilla; - private AltGameModeBase? _gameMode; - - public GameModeType GameModeType => _gameModeType; - public bool HasAltGameMode => _gameModeType != GameModeType.Vanilla; - public bool IsAltGameModeComplete { get; private set; } - public event EventHandler? GoalStateChanged; - - public void MarkAltGameModeAsComplete() - { - IsAltGameModeComplete = true; - Tracker.GameStateTracker.HasFinishedGoal = true; - Tracker.GameService?.TrySetGameModeComplete(); - Tracker.UpdateAllAccessibility(false); - } - - public bool IsAltGameModeKnowinglyComplete() - { - return HasAltGameMode && _gameMode!.IsKnowinglyComplete(Tracker.World); - } - - public void OnViewingPyramidText() - { - if (!HasAltGameMode) - { - return; - } - - if (_gameMode!.OnViewingPyramidText(Tracker.World)) - { - _ = Tracker.SaveAsync(); - Tracker.UpdateAllAccessibility(false); - Tracker.AltGameModeService.NotifyOfGoalStateChange(); - } - } - - public void NotifyOfGoalStateChange() - { - GoalStateChanged?.Invoke(this, EventArgs.Empty); - } - - public List GetGoalUiDetails() - { - var state = _gameMode?.GetGoalUiDetails(Tracker.World); - if (state?.Count > 0) return state; - var progression = Tracker.PlayerProgressionService.GetProgression(false); - var crystals = progression.CrystalCount; - var bosses = progression.MetroidBossCount; - var crystalsRequired = Tracker.World.State?.MarkedGanonCrystalCount != null - ? $"{Tracker.World.State?.MarkedGanonCrystalCount}" - : "?"; - var bossesRequired = Tracker.World.State?.MarkedTourianBossCount != null - ? $"{Tracker.World.State?.MarkedTourianBossCount}" - : "?"; - return - [ - new GoalUiDetails { IconCategory = "Dungeons", Icon = "crystal.png", Text = $"{crystals}/{crystalsRequired}" }, - new GoalUiDetails { IconCategory = "Dungeons", Icon = "metroid boss token.png", Text = $"{bosses}/{bossesRequired}" } - ]; - } - - public override void PostInitialize() - { - _gameModeType = worldAccessor.World.Config.GameModeOptions.SelectedGameModeType; - if (_gameModeType == GameModeType.Vanilla) - { - return; - } - _gameMode = altGameModeFactory.GetGameMode(_gameModeType); - - IsAltGameModeComplete = _gameMode.IsComplete(worldAccessor.World); - Tracker.GameStateTracker.HasFinishedGoal = IsAltGameModeComplete; - - _gameMode.InitializeTracker(Tracker); - } -} diff --git a/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerBossService.cs b/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerBossService.cs index aea017e1..4cdc7c1c 100644 --- a/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerBossService.cs +++ b/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerBossService.cs @@ -111,6 +111,7 @@ public void MarkBossAsDefeated(Boss boss, bool admittedGuilt = true, float? conf if (boss.Region != null) { MarkBossAsDefeated(boss.Region, confidence, autoTracked, admittedGuilt, force: force); + UpdateAllAccessibility(false); return; } diff --git a/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerGameModeService.cs b/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerGameModeService.cs new file mode 100644 index 00000000..0696b48a --- /dev/null +++ b/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerGameModeService.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using TrackerCouncil.Smz3.Abstractions; +using TrackerCouncil.Smz3.Data.ViewModels; +using TrackerCouncil.Smz3.SeedGenerator.Contracts; +using TrackerCouncil.Smz3.SeedGenerator.GameModes; +using TrackerCouncil.Smz3.Shared.Enums; + +namespace TrackerCouncil.Smz3.Tracking.TrackingServices; + +internal class TrackerGameModeService(GameModeFactory gameModeFactory, IWorldAccessor worldAccessor) : TrackerService, ITrackerGameModeService +{ + private GameModeType _gameModeType = GameModeType.Vanilla; + private GameModeBase? _gameMode; + + public GameModeType GameModeType => _gameModeType; + public bool HasAltGameMode => _gameModeType != GameModeType.Vanilla && _gameModeType != GameModeType.AllDungeons; + public bool IsGameModeComplete { get; private set; } + public event EventHandler? GoalStateChanged; + + public void MarkGameModeAsComplete() + { + IsGameModeComplete = true; + Tracker.GameService?.TrySetGameModeComplete(); + Tracker.UpdateAllAccessibility(false); + } + + public bool IsGameModeKnowinglyComplete() + { + return _gameMode!.IsKnowinglyComplete(Tracker.World); + } + + public void OnViewingPyramidText() + { + if (_gameMode!.OnViewingPyramidText(Tracker.World)) + { + _ = Tracker.SaveAsync(); + Tracker.UpdateAllAccessibility(false); + Tracker.GameModeService.NotifyOfGoalStateChange(); + } + } + + public void NotifyOfGoalStateChange() + { + GoalStateChanged?.Invoke(this, EventArgs.Empty); + } + + public List GetGoalUiDetails() + { + var progression = Tracker.PlayerProgressionService.GetProgression(false); + return _gameMode?.GetGoalUiDetails(Tracker.World, progression) ?? []; + } + + public override void PostInitialize() + { + _gameModeType = worldAccessor.World.Config.GameModeOptions.SelectedGameModeType; + _gameMode = gameModeFactory.GetGameMode(_gameModeType); + _gameMode.InitializeTracker(Tracker); + IsGameModeComplete = _gameMode.IsComplete(worldAccessor.World); + NotifyOfGoalStateChange(); + } +} diff --git a/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerGameStateService.cs b/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerGameStateService.cs index d6b9f516..ee1b57fa 100644 --- a/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerGameStateService.cs +++ b/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerGameStateService.cs @@ -8,16 +8,14 @@ using TrackerCouncil.Smz3.Data.Tracking; using TrackerCouncil.Smz3.Data.WorldData; using TrackerCouncil.Smz3.Data.WorldData.Regions; -using TrackerCouncil.Smz3.SeedGenerator.Contracts; using TrackerCouncil.Smz3.Shared.Enums; using TrackerCouncil.Smz3.Shared.Models; namespace TrackerCouncil.Smz3.Tracking.TrackingServices; -internal class TrackerGameStateService(IMetadataService metadataService, IWorldAccessor worldAccessor) : TrackerService, ITrackerGameStateService +internal class TrackerGameStateService(IMetadataService metadataService) : TrackerService, ITrackerGameStateService { public bool HasBeatenGame { get; protected set; } - public bool HasFinishedGoal { get; set; } public ViewedObject? LastViewedObject { get; set; } public Region? CurrentRegion { get; protected set; } public string CurrentMap { get; protected set; } = ""; @@ -323,14 +321,12 @@ public void UpdateGanonRequirement(int crystalAmount, bool autoTracked) World.State.MarkedGanonCrystalCount = crystalAmount; World.UpdateLegacyWorld(World.State); UpdateAllAccessibility(true); - Tracker.AltGameModeService.NotifyOfGoalStateChange(); Tracker.Say(x => x.UpdatedGanonCrystalRequirement, args: [crystalAmount]); AddUndo(autoTracked, () => { World.State.MarkedGanonCrystalCount = previousAmount; World.UpdateLegacyWorld(World.State); - Tracker.AltGameModeService.NotifyOfGoalStateChange(); }); } @@ -345,7 +341,6 @@ public void UpdateTourianRequirement(int bossAmount, bool autoTracked) World.State.MarkedTourianBossCount = bossAmount; World.UpdateLegacyWorld(World.State); - Tracker.AltGameModeService.NotifyOfGoalStateChange(); UpdateAllAccessibility(true); Tracker.Say(x => x.UpdatedTourianBossRequirement, args: [bossAmount]); @@ -353,7 +348,6 @@ public void UpdateTourianRequirement(int bossAmount, bool autoTracked) { World.State.MarkedTourianBossCount = previousAmount; World.UpdateLegacyWorld(World.State); - Tracker.AltGameModeService.NotifyOfGoalStateChange(); }); } @@ -402,18 +396,4 @@ public void UpdateTalkingToZora(bool isTalking, AutoTrackerZeldaState? zeldaStat IsTalkingToZora = false; } } - - public override void PostInitialize() - { - base.PostInitialize(); - if (worldAccessor.World.Config.GameModeOptions.SelectedGameModeType == GameModeType.Vanilla) - { - var progression = Tracker.PlayerProgressionService.GetProgression(true); - if (progression.CrystalCount >= Tracker.LocalConfig?.GameModeOptions.GanonCrystalCount && - progression.MetroidBossCount >= Tracker.LocalConfig?.GameModeOptions.TourianBossCount) - { - Tracker.GameStateTracker.HasFinishedGoal = true; - } - } - } } diff --git a/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerRewardService.cs b/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerRewardService.cs index 6f7a243f..44c9cde7 100644 --- a/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerRewardService.cs +++ b/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerRewardService.cs @@ -3,18 +3,18 @@ using System.Linq; using Microsoft.Extensions.Logging; using TrackerCouncil.Smz3.Abstractions; -using TrackerCouncil.Smz3.Data.Options; using TrackerCouncil.Smz3.Data.Services; using TrackerCouncil.Smz3.Data.WorldData; using TrackerCouncil.Smz3.Data.WorldData.Regions; using TrackerCouncil.Smz3.Shared; using TrackerCouncil.Smz3.Shared.Enums; -using TrackerCouncil.Smz3.Tracking.Services; namespace TrackerCouncil.Smz3.Tracking.TrackingServices; internal class TrackerRewardService(ILogger logger, IPlayerProgressionService playerProgressionService, IMetadataService metadataService) : TrackerService, ITrackerRewardService { + public event EventHandler? RewardsChanged; + public void SetAreaReward(IHasReward rewardRegion, RewardType? reward = null, float? confidence = null, bool autoTracked = false) { var originalReward = rewardRegion.MarkedReward; @@ -41,13 +41,13 @@ public void SetAreaReward(IHasReward rewardRegion, RewardType? reward = null, fl } UpdateAllAccessibility(false); - Tracker.AltGameModeService.NotifyOfGoalStateChange(); + RewardsChanged?.Invoke(this, EventArgs.Empty); AddUndo(autoTracked, () => { rewardRegion.MarkedReward = originalReward; UpdateAllAccessibility(false); - Tracker.AltGameModeService.NotifyOfGoalStateChange(); + RewardsChanged?.Invoke(this, EventArgs.Empty); }); } @@ -75,18 +75,7 @@ public void GiveAreaReward(IHasReward rewardRegion, bool isAutoTracked, bool sta } UpdateAllAccessibility(false); - Tracker.AltGameModeService.NotifyOfGoalStateChange(); - - // Check if the player has defeated all bosses and gotten all crystals - if (config.GameModeOptions.SelectedGameModeType == GameModeType.Vanilla) - { - var progression = Tracker.PlayerProgressionService.GetProgression(true); - if (progression.CrystalCount >= config.GameModeOptions.GanonCrystalCount && - progression.MetroidBossCount >= config.GameModeOptions.TourianBossCount) - { - Tracker.GameStateTracker.HasFinishedGoal = true; - } - } + RewardsChanged?.Invoke(this, EventArgs.Empty); // TODO: Add a response @@ -95,7 +84,7 @@ public void GiveAreaReward(IHasReward rewardRegion, bool isAutoTracked, bool sta rewardRegion.HasReceivedReward = false; rewardRegion.MarkedReward = previousMarkedReward; UpdateAllAccessibility(true); - Tracker.AltGameModeService.NotifyOfGoalStateChange(); + RewardsChanged?.Invoke(this, EventArgs.Empty); }); } @@ -109,7 +98,7 @@ public void RemoveAreaReward(IHasReward rewardRegion, bool stateResponse) rewardRegion.HasReceivedReward = false; UpdateAllAccessibility(true); - Tracker.AltGameModeService.NotifyOfGoalStateChange(); + RewardsChanged?.Invoke(this, EventArgs.Empty); // TODO: Add a response @@ -117,7 +106,7 @@ public void RemoveAreaReward(IHasReward rewardRegion, bool stateResponse) { rewardRegion.HasReceivedReward = false; UpdateAllAccessibility(false); - Tracker.AltGameModeService.NotifyOfGoalStateChange(); + RewardsChanged?.Invoke(this, EventArgs.Empty); }); } @@ -131,9 +120,11 @@ public void SetUnmarkedRewards(RewardType reward, float? confidence = null) { Tracker.Say(response: Responses.RemainingDungeonsMarked, args: [metadataService.GetName(reward)]); unmarkedRegions.ForEach(dungeon => dungeon.MarkedReward = reward); + RewardsChanged?.Invoke(this, EventArgs.Empty); AddUndo(() => { unmarkedRegions.ForEach(dungeon => dungeon.MarkedReward = RewardType.None); + RewardsChanged?.Invoke(this, EventArgs.Empty); }); } else diff --git a/src/TrackerCouncil.Smz3.Tracking/VoiceCommands/GoalModule.cs b/src/TrackerCouncil.Smz3.Tracking/VoiceCommands/GoalModule.cs index 86d8a7e0..dbaa22f1 100644 --- a/src/TrackerCouncil.Smz3.Tracking/VoiceCommands/GoalModule.cs +++ b/src/TrackerCouncil.Smz3.Tracking/VoiceCommands/GoalModule.cs @@ -76,12 +76,14 @@ public override void AddCommands() { var count = int.Parse(result.Semantics[ItemCountKey].Value); TrackerBase.GameStateTracker.UpdateGanonRequirement(count, false); + TrackerBase.GameModeService.NotifyOfGoalStateChange(); }); AddCommand("Tourian Boss Count", GetTourianBossCountRule(), (result) => { var count = int.Parse(result.Semantics[ItemCountKey].Value); TrackerBase.GameStateTracker.UpdateTourianRequirement(count, false); + TrackerBase.GameModeService.NotifyOfGoalStateChange(); }); } } diff --git a/src/TrackerCouncil.Smz3.UI/Services/TrackerWindowService.cs b/src/TrackerCouncil.Smz3.UI/Services/TrackerWindowService.cs index 60546563..387e5417 100644 --- a/src/TrackerCouncil.Smz3.UI/Services/TrackerWindowService.cs +++ b/src/TrackerCouncil.Smz3.UI/Services/TrackerWindowService.cs @@ -238,19 +238,19 @@ public void StartTracker() tracker.AutoTracker.AutoTrackerConnected += AutoTrackerOnAutoTrackerEnabled; tracker.AutoTracker.AutoTrackerDisconnected += AutoTrackerOnAutoTrackerEnabled; tracker.AutoTracker.AutoTrackerConnectorChanged += AutoTrackerOnAutoTrackerConnectorChanged; - tracker.AltGameModeService.GoalStateChanged += AltGameModeServiceOnGoalStateChanged; + tracker.GameModeService.GoalStateChanged += GameModeServiceOnGoalStateChanged; tracker.AutoTracker.SetConnector(Options.GeneralOptions.SnesConnectorSettings); } - private void AltGameModeServiceOnGoalStateChanged(object? sender, EventArgs e) + private void GameModeServiceOnGoalStateChanged(object? sender, EventArgs e) { UpdateGoals(); } private void UpdateGoals() { - _model.Goals = tracker.AltGameModeService.GetGoalUiDetails().Select(x => new GoalUiDetails + _model.Goals = tracker.GameModeService.GetGoalUiDetails().Select(x => new GoalUiDetails { Icon = uiService.GetSpritePath(x.IconCategory, x.Icon, out _) ?? "", Text = x.Text diff --git a/src/TrackerCouncil.Smz3.UI/Views/GenerationSettingsBasicPanel.axaml b/src/TrackerCouncil.Smz3.UI/Views/GenerationSettingsBasicPanel.axaml index 9fec8cb1..88e7338a 100644 --- a/src/TrackerCouncil.Smz3.UI/Views/GenerationSettingsBasicPanel.axaml +++ b/src/TrackerCouncil.Smz3.UI/Views/GenerationSettingsBasicPanel.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:AvaloniaControls.Controls;assembly=AvaloniaControls" xmlns:viewModels1="clr-namespace:TrackerCouncil.Smz3.Data.ViewModels;assembly=TrackerCouncil.Smz3.Data" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" x:Class="TrackerCouncil.Smz3.UI.Views.GenerationSettingsBasicPanel" x:DataType="viewModels1:GenerationWindowViewModel" Loaded="Control_OnLoaded"> @@ -139,7 +139,6 @@ IsVisible="{Binding Basic.IsShuffleStyleVisible}" /> - diff --git a/src/TrackerCouncil.Smz3.UI/Views/GenerationSettingsWindow.axaml b/src/TrackerCouncil.Smz3.UI/Views/GenerationSettingsWindow.axaml index 0d8278b6..bf4030e8 100644 --- a/src/TrackerCouncil.Smz3.UI/Views/GenerationSettingsWindow.axaml +++ b/src/TrackerCouncil.Smz3.UI/Views/GenerationSettingsWindow.axaml @@ -30,7 +30,16 @@ - + + + + + + Skip Ganon and Tourian by lifting off from the ship after completing the goal + + + + diff --git a/src/TrackerCouncil.Smz3.UI/Views/GenerationSettingsWindow.axaml.cs b/src/TrackerCouncil.Smz3.UI/Views/GenerationSettingsWindow.axaml.cs index b215636d..1d2881c7 100644 --- a/src/TrackerCouncil.Smz3.UI/Views/GenerationSettingsWindow.axaml.cs +++ b/src/TrackerCouncil.Smz3.UI/Views/GenerationSettingsWindow.axaml.cs @@ -10,7 +10,7 @@ using TrackerCouncil.Smz3.Data.ParsedRom; using TrackerCouncil.Smz3.Data.Services; using TrackerCouncil.Smz3.Data.ViewModels; -using TrackerCouncil.Smz3.SeedGenerator.AltGameModes; +using TrackerCouncil.Smz3.SeedGenerator.GameModes; using TrackerCouncil.Smz3.Shared.Enums; using TrackerCouncil.Smz3.Shared.Models; using Dispatcher = Avalonia.Threading.Dispatcher; @@ -29,12 +29,12 @@ public GenerationSettingsWindow() DataContext = new GenerationWindowViewModel(); } - public GenerationSettingsWindow(GenerationSettingsWindowService generationSettingsWindowService, IServiceProvider serviceProvider, AltGameModeFactory altGameModeFactory) + public GenerationSettingsWindow(GenerationSettingsWindowService generationSettingsWindowService, IServiceProvider serviceProvider, GameModeFactory gameModeFactory) { _generationSettingsWindowService = generationSettingsWindowService; _serviceProvider = serviceProvider; InitializeComponent(); - DataContext = BasicPanel.Data = _model = _generationSettingsWindowService.GetViewModel(altGameModeFactory.GetGameModeDescriptions()); + DataContext = BasicPanel.Data = _model = _generationSettingsWindowService.GetViewModel(gameModeFactory.GetGameModeDescriptions()); BasicPanel.SetServices(serviceProvider, generationSettingsWindowService); } diff --git a/tests/TrackerCouncil.Smz3.Tests/LogicTests/RandomizerTests.cs b/tests/TrackerCouncil.Smz3.Tests/LogicTests/RandomizerTests.cs index 0e1959f3..02ba826e 100644 --- a/tests/TrackerCouncil.Smz3.Tests/LogicTests/RandomizerTests.cs +++ b/tests/TrackerCouncil.Smz3.Tests/LogicTests/RandomizerTests.cs @@ -11,12 +11,13 @@ using TrackerCouncil.Smz3.Data.Options; using TrackerCouncil.Smz3.Data.Services; using TrackerCouncil.Smz3.Data.WorldData; -using TrackerCouncil.Smz3.SeedGenerator.AltGameModes; using TrackerCouncil.Smz3.SeedGenerator.Contracts; using TrackerCouncil.Smz3.SeedGenerator.FileData.Patches; +using TrackerCouncil.Smz3.SeedGenerator.GameModes; using TrackerCouncil.Smz3.SeedGenerator.Generation; using TrackerCouncil.Smz3.SeedGenerator.Infrastructure; using TrackerCouncil.Smz3.Shared.Enums; +using TrackerCouncil.Smz3.Tests.Text; using Xunit; namespace TrackerCouncil.Smz3.Tests.LogicTests; @@ -175,8 +176,9 @@ private static Smz3Randomizer GetRandomizer() .AddSingleton() .AddSingleton() .AddSingleton() - .AddTransient() + .AddTransient() .AddTransient() + .AddGameModes() .AddConfigs() .BuildServiceProvider(); @@ -186,6 +188,6 @@ private static Smz3Randomizer GetRandomizer() GetLogger(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService()); + serviceProvider.GetRequiredService()); } } diff --git a/tests/TrackerCouncil.Smz3.Tests/Text/GameHintTests.cs b/tests/TrackerCouncil.Smz3.Tests/Text/GameHintTests.cs index 98a90107..f572db04 100644 --- a/tests/TrackerCouncil.Smz3.Tests/Text/GameHintTests.cs +++ b/tests/TrackerCouncil.Smz3.Tests/Text/GameHintTests.cs @@ -14,9 +14,9 @@ using TrackerCouncil.Smz3.Data.WorldData.Regions.Zelda; using TrackerCouncil.Smz3.Data.WorldData.Regions.Zelda.DarkWorld; using TrackerCouncil.Smz3.Data.WorldData.Regions.Zelda.LightWorld; -using TrackerCouncil.Smz3.SeedGenerator.AltGameModes; using TrackerCouncil.Smz3.SeedGenerator.Contracts; using TrackerCouncil.Smz3.SeedGenerator.FileData.Patches; +using TrackerCouncil.Smz3.SeedGenerator.GameModes; using TrackerCouncil.Smz3.SeedGenerator.Generation; using TrackerCouncil.Smz3.SeedGenerator.Infrastructure; using TrackerCouncil.Smz3.Shared.Enums; @@ -356,6 +356,26 @@ void SpazerHuntMultiworldHints() Assert.Equal(LocationUsefulness.Mandatory, hintService.GetUsefulness(worlds[0].Locations.Where(x => x.Room is LightWorldNorthWest.BlindsHideoutRoom).ToList(), worlds, null)); } + [Fact] + void AllDungeonsHints() + { + var world = GetVanillaWorld(new GameModeOptions() { SelectedGameModeType = GameModeType.AllDungeons }); + var hintService = GetGameHintService(); + + // Move the bow and boots out of eastern palace for testing + var bow = world.FindLocation(LocationId.EasternPalaceBigChest); + var bowOtherItem = world.Locations.First(x => x is { Room: LightWorldNorthWest.BlindsHideoutRoom, ItemType: ItemType.ThreeBombs }); + SwapLocationItems(bow, bowOtherItem); + var boots = world.FindLocation(LocationId.Sahasrahla); + var bootsOtherItem = world.Locations.First(x => x is { Room: LightWorldNorthWest.BlindsHideoutRoom, ItemType: ItemType.ThreeBombs }); + SwapLocationItems(boots, bootsOtherItem); + + // Eastern palace should be mandatory for the all dungeons, but useless for vanilla + Assert.Equal(LocationUsefulness.Mandatory, hintService.GetUsefulness(world.Locations.Where(x => x.Region is EasternPalace).ToList(), [world], null)); + world.Config.GameModeOptions.SelectedGameModeType = GameModeType.Vanilla; + Assert.Equal(LocationUsefulness.Useless, hintService.GetUsefulness(world.Locations.Where(x => x.Region is EasternPalace).ToList(), [world], null)); + } + #region Private methods private void SwapLocationItems(Location one, Location two) { @@ -433,9 +453,9 @@ private static IGameHintService GetGameHintService() .AddSingleton() .AddSingleton() .AddSingleton() - .AddTransient() + .AddTransient() .AddTransient() - .AddGameModes() + .AddGameModes() .AddConfigs() .BuildServiceProvider(); @@ -454,7 +474,7 @@ internal static IServiceCollection AddGameModes(this IServiceCollecti lock (s_lockObject) { var moduleTypes = typeof(TAssembly).Assembly.GetTypes() - .Where(x => x.IsSubclassOf(typeof(AltGameModeBase))); + .Where(x => x.IsSubclassOf(typeof(GameModeBase))); foreach (var moduleType in moduleTypes) { @@ -462,12 +482,13 @@ internal static IServiceCollection AddGameModes(this IServiceCollecti if (!s_addedGameModes) { - AltGameModeFactory.AddGameModeClass(moduleType); - s_addedGameModes = true; + GameModeFactory.AddGameModeClass(moduleType); } } - services.AddSingleton(); + s_addedGameModes = true; + + services.AddSingleton(); return services; }