diff --git a/src/Randomizer.App/RomGenerator.cs b/src/Randomizer.App/RomGenerator.cs
index e2feff65d..73d9fb7be 100644
--- a/src/Randomizer.App/RomGenerator.cs
+++ b/src/Randomizer.App/RomGenerator.cs
@@ -11,6 +11,7 @@
using Randomizer.App.ViewModels;
using Randomizer.Shared;
using Randomizer.Shared.Models;
+using Randomizer.SMZ3;
using Randomizer.SMZ3.FileData;
using Randomizer.SMZ3.Generation;
using Randomizer.SMZ3.Regions;
@@ -42,28 +43,36 @@ public RomGenerator(Smz3Randomizer randomizer,
/// True if the rom was generated successfully, false otherwise
public bool GenerateRom(RandomizerOptions options, out string path, out string error, out GeneratedRom rom)
{
- var bytes = GenerateRomBytes(options, out var seed);
-
- var folderPath = Path.Combine(options.RomOutputPath, $"{DateTimeOffset.Now:yyyyMMdd-HHmmss}_{seed.Seed}");
- Directory.CreateDirectory(folderPath);
-
- var romFileName = $"SMZ3_Cas_{DateTimeOffset.Now:yyyyMMdd-HHmmss}_{seed.Seed}.sfc";
- var romPath = Path.Combine(folderPath, romFileName);
- EnableMsu1Support(options, bytes, romPath, out var msuError);
- Rom.UpdateChecksum(bytes);
- File.WriteAllBytes(romPath, bytes);
+ try
+ {
+ var bytes = GenerateRomBytes(options, out var seed);
+ var folderPath = Path.Combine(options.RomOutputPath, $"{DateTimeOffset.Now:yyyyMMdd-HHmmss}_{seed.Seed}");
+ Directory.CreateDirectory(folderPath);
- var spoilerLog = GetSpoilerLog(options, seed);
- var spoilerPath = Path.ChangeExtension(romPath, ".txt");
- File.WriteAllText(spoilerPath, spoilerLog);
+ var romFileName = $"SMZ3_Cas_{DateTimeOffset.Now:yyyyMMdd-HHmmss}_{seed.Seed}.sfc";
+ var romPath = Path.Combine(folderPath, romFileName);
+ EnableMsu1Support(options, bytes, romPath, out var msuError);
+ Rom.UpdateChecksum(bytes);
+ File.WriteAllBytes(romPath, bytes);
- rom = SaveSeedToDatabase(options, seed, romPath, spoilerPath);
+ var spoilerLog = GetSpoilerLog(options, seed);
+ var spoilerPath = Path.ChangeExtension(romPath, ".txt");
+ File.WriteAllText(spoilerPath, spoilerLog);
- error = msuError;
- path = romPath;
+ rom = SaveSeedToDatabase(options, seed, romPath, spoilerPath);
- return true;
+ error = msuError;
+ path = romPath;
+ return true;
+ }
+ catch (RandomizerGenerationException e)
+ {
+ path = null;
+ error = $"Error generating rom\n{e.Message}\nPlease try again. If it persists, try modifying your seed settings.";
+ rom = null;
+ return false;
+ }
}
///
@@ -144,12 +153,7 @@ protected byte[] GenerateRomBytes(RandomizerOptions options, out SeedData seed)
/// The db entry for the generated rom
protected GeneratedRom SaveSeedToDatabase(RandomizerOptions options, SeedData seed, string romPath, string spoilerPath)
{
- var jsonOptions = new JsonSerializerOptions
- {
- Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
- };
-
- var settings = JsonSerializer.Serialize(options.ToConfig(), jsonOptions);
+ var settings = Config.ToConfigString(options.ToConfig(), true);
var rom = new GeneratedRom()
{
@@ -186,11 +190,21 @@ private string GetSpoilerLog(RandomizerOptions options, SeedData seed)
log.AppendLine(Underline($"SMZ3 Cas’ spoiler log", '='));
log.AppendLine($"Generated on {DateTime.Now:F}");
log.AppendLine($"Seed: {options.SeedOptions.Seed} (actual: {seed.Seed})");
- log.AppendLine($"Sword: {options.SeedOptions.SwordLocation}");
- log.AppendLine($"Morph: {options.SeedOptions.MorphLocation}");
- log.AppendLine($"Bombs: {options.SeedOptions.MorphBombsLocation}");
- log.AppendLine($"Shaktool: {options.SeedOptions.ShaktoolItem}");
- log.AppendLine($"Peg World: {options.SeedOptions.PegWorldItem}");
+ log.AppendLine($"Settings String: {Config.ToConfigString(seed.Playthrough.Config, true)}");
+ log.AppendLine($"Early Items: {string.Join(',', seed.Playthrough.Config.EarlyItems.Select(x => x.ToString()).ToArray())}");
+
+ var locationPrefs = new List();
+ foreach (var (locationId, value) in seed.Playthrough.Config.LocationItems)
+ {
+ var itemPref = value < Enum.GetValues(typeof(ItemPool)).Length ? ((ItemPool)value).ToString() : ((ItemType)value).ToString();
+ locationPrefs.Add($"{seed.Worlds[0].World.Locations.First(x => x.Id == locationId).Name} - {itemPref}");
+ }
+ log.AppendLine($"Location Preferences: {string.Join(',', locationPrefs.ToArray())}");
+
+ var type = options.LogicConfig.GetType();
+ var logicOptions = string.Join(',', type.GetProperties().Select(x => $"{x.Name}: {x.GetValue(seed.Playthrough.Config.LogicConfig)}"));
+ log.AppendLine($"Logic Options: {logicOptions}");
+
log.AppendLine((options.SeedOptions.Keysanity ? "[Keysanity] " : "")
+ (options.SeedOptions.Race ? "[Race] " : ""));
if (File.Exists(options.PatchOptions.Msu1Path))
diff --git a/src/Randomizer.App/ViewModels/RandomizerOptions.cs b/src/Randomizer.App/ViewModels/RandomizerOptions.cs
index d28240857..aad6f18ee 100644
--- a/src/Randomizer.App/ViewModels/RandomizerOptions.cs
+++ b/src/Randomizer.App/ViewModels/RandomizerOptions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
@@ -54,7 +55,7 @@ public RandomizerOptions(GeneralOptions generalOptions,
[JsonPropertyName("Logic")]
public LogicConfig LogicConfig { get; set; }
- public bool ItemLocationsExpanded { get; set; } = false;
+ public bool EarlyItemsExpanded { get; set; } = false;
public bool CustomizationExpanded { get; set; } = false;
public bool LogicExpanded { get; set; } = false;
@@ -65,6 +66,7 @@ public RandomizerOptions(GeneralOptions generalOptions,
public double WindowHeight { get; set; } = 600d;
+
public string RomOutputPath
{
get => Directory.Exists(GeneralOptions.RomOutputPath)
@@ -84,34 +86,75 @@ public void Save(string path)
File.WriteAllText(path, json);
}
- public Config ToConfig() => new()
+ public Config ToConfig()
{
- GameMode = GameMode.Normal,
- Z3Logic = Z3Logic.Normal,
- SMLogic = SMLogic.Normal,
- ItemLocations =
+ if (string.IsNullOrWhiteSpace(SeedOptions.ConfigString)) {
+ return new()
+ {
+ GameMode = GameMode.Normal,
+ Z3Logic = Z3Logic.Normal,
+ SMLogic = SMLogic.Normal,
+ ItemLocations =
+ {
+ [ItemType.ProgressiveSword] = SeedOptions.SwordLocation,
+ [ItemType.Morph] = SeedOptions.MorphLocation,
+ [ItemType.Bombs] = SeedOptions.MorphBombsLocation,
+ [ItemType.Boots] = SeedOptions.PegasusBootsLocation,
+ [ItemType.SpaceJump] = SeedOptions.SpaceJumpLocation,
+ },
+ ShaktoolItemPool = SeedOptions.ShaktoolItem,
+ PegWorldItemPool = SeedOptions.PegWorldItem,
+ KeyShuffle = SeedOptions.Keysanity ? KeyShuffle.Keysanity : KeyShuffle.None,
+ Race = SeedOptions.Race,
+ ExtendedMsuSupport = PatchOptions.CanEnableExtendedSoundtrack && PatchOptions.EnableExtendedSoundtrack,
+ ShuffleDungeonMusic = PatchOptions.ShuffleDungeonMusic,
+ HeartColor = PatchOptions.HeartColor,
+ LowHealthBeepSpeed = PatchOptions.LowHealthBeepSpeed,
+ DisableLowEnergyBeep = PatchOptions.DisableLowEnergyBeep,
+ CasualSMPatches = PatchOptions.CasualSuperMetroidPatches,
+ MenuSpeed = PatchOptions.MenuSpeed,
+ LinkName = PatchOptions.LinkSprite == Sprite.DefaultLink ? "Link" : PatchOptions.LinkSprite.Name,
+ SamusName = PatchOptions.SamusSprite == Sprite.DefaultSamus ? "Samus" : PatchOptions.SamusSprite.Name,
+ LocationItems = SeedOptions.LocationItems,
+ EarlyItems = SeedOptions.EarlyItems,
+ LogicConfig = LogicConfig.Clone()
+ };
+ }
+ else
{
- [ItemType.ProgressiveSword] = SeedOptions.SwordLocation,
- [ItemType.Morph] = SeedOptions.MorphLocation,
- [ItemType.Bombs] = SeedOptions.MorphBombsLocation,
- [ItemType.Boots] = SeedOptions.PegasusBootsLocation,
- [ItemType.SpaceJump] = SeedOptions.SpaceJumpLocation,
- },
- ShaktoolItemPool = SeedOptions.ShaktoolItem,
- PegWorldItemPool = SeedOptions.PegWorldItem,
- KeyShuffle = SeedOptions.Keysanity ? KeyShuffle.Keysanity : KeyShuffle.None,
- Race = SeedOptions.Race,
- ExtendedMsuSupport = PatchOptions.CanEnableExtendedSoundtrack && PatchOptions.EnableExtendedSoundtrack,
- ShuffleDungeonMusic = PatchOptions.ShuffleDungeonMusic,
- HeartColor = PatchOptions.HeartColor,
- LowHealthBeepSpeed = PatchOptions.LowHealthBeepSpeed,
- DisableLowEnergyBeep = PatchOptions.DisableLowEnergyBeep,
- CasualSMPatches = PatchOptions.CasualSuperMetroidPatches,
- MenuSpeed = PatchOptions.MenuSpeed,
- LinkName = PatchOptions.LinkSprite == Sprite.DefaultLink ? "Link" : PatchOptions.LinkSprite.Name,
- SamusName = PatchOptions.SamusSprite == Sprite.DefaultSamus ? "Samus" : PatchOptions.SamusSprite.Name,
- LogicConfig = LogicConfig.Clone()
- };
+ var oldConfig = Config.FromConfigString(SeedOptions.ConfigString);
+ return new Config()
+ {
+ GameMode = GameMode.Normal,
+ Z3Logic = Z3Logic.Normal,
+ SMLogic = SMLogic.Normal,
+ ItemLocations =
+ {
+ [ItemType.ProgressiveSword] = SeedOptions.SwordLocation,
+ [ItemType.Morph] = SeedOptions.MorphLocation,
+ [ItemType.Bombs] = SeedOptions.MorphBombsLocation,
+ [ItemType.Boots] = SeedOptions.PegasusBootsLocation,
+ [ItemType.SpaceJump] = SeedOptions.SpaceJumpLocation,
+ },
+ ShaktoolItemPool = SeedOptions.ShaktoolItem,
+ PegWorldItemPool = SeedOptions.PegWorldItem,
+ KeyShuffle = SeedOptions.Keysanity ? KeyShuffle.Keysanity : KeyShuffle.None,
+ Race = SeedOptions.Race,
+ ExtendedMsuSupport = PatchOptions.CanEnableExtendedSoundtrack && PatchOptions.EnableExtendedSoundtrack,
+ ShuffleDungeonMusic = PatchOptions.ShuffleDungeonMusic,
+ HeartColor = PatchOptions.HeartColor,
+ LowHealthBeepSpeed = PatchOptions.LowHealthBeepSpeed,
+ DisableLowEnergyBeep = PatchOptions.DisableLowEnergyBeep,
+ CasualSMPatches = PatchOptions.CasualSuperMetroidPatches,
+ MenuSpeed = PatchOptions.MenuSpeed,
+ LinkName = PatchOptions.LinkSprite == Sprite.DefaultLink ? "Link" : PatchOptions.LinkSprite.Name,
+ SamusName = PatchOptions.SamusSprite == Sprite.DefaultSamus ? "Samus" : PatchOptions.SamusSprite.Name,
+ LocationItems = oldConfig.LocationItems,
+ EarlyItems = oldConfig.EarlyItems,
+ LogicConfig = oldConfig.LogicConfig
+ };
+ }
+ }
public RandomizerOptions Clone()
{
diff --git a/src/Randomizer.App/ViewModels/SeedOptions.cs b/src/Randomizer.App/ViewModels/SeedOptions.cs
index d2a30e680..45f96fa0b 100644
--- a/src/Randomizer.App/ViewModels/SeedOptions.cs
+++ b/src/Randomizer.App/ViewModels/SeedOptions.cs
@@ -6,7 +6,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
-
+using Randomizer.Shared;
using Randomizer.SMZ3;
namespace Randomizer.App.ViewModels
@@ -19,6 +19,9 @@ public class SeedOptions
[JsonIgnore]
public string Seed { get; set; }
+ [JsonIgnore]
+ public string ConfigString { get; set; }
+
public ItemPlacement SwordLocation { get; set; }
public ItemPlacement MorphLocation { get; set; }
@@ -36,5 +39,9 @@ public class SeedOptions
public bool Keysanity { get; set; }
public bool Race { get; set; }
+
+ public ISet EarlyItems { get; set; } = new HashSet();
+
+ public IDictionary LocationItems { get; set; } = new Dictionary();
}
}
diff --git a/src/Randomizer.App/Windows/GenerateRomWindow.xaml b/src/Randomizer.App/Windows/GenerateRomWindow.xaml
index b749eefc7..1551be5c6 100644
--- a/src/Randomizer.App/Windows/GenerateRomWindow.xaml
+++ b/src/Randomizer.App/Windows/GenerateRomWindow.xaml
@@ -32,114 +32,33 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Margin="24,11,11,11">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -271,6 +190,11 @@
+
+
+
+
diff --git a/src/Randomizer.App/Windows/GenerateRomWindow.xaml.cs b/src/Randomizer.App/Windows/GenerateRomWindow.xaml.cs
index 355ce3d41..552c287e9 100644
--- a/src/Randomizer.App/Windows/GenerateRomWindow.xaml.cs
+++ b/src/Randomizer.App/Windows/GenerateRomWindow.xaml.cs
@@ -21,6 +21,7 @@
using Randomizer.Shared;
using Randomizer.SMZ3;
using Randomizer.SMZ3.Generation;
+using Randomizer.SMZ3.Tracking.Configuration;
namespace Randomizer.App
{
@@ -33,6 +34,7 @@ public partial class GenerateRomWindow : Window
private readonly IServiceProvider _serviceProvider;
private readonly Smz3Randomizer _randomizer;
private readonly RomGenerator _romGenerator;
+ private readonly LocationConfig _locationConfig;
private RandomizerOptions _options;
public GenerateRomWindow(IServiceProvider serviceProvider)
@@ -40,6 +42,7 @@ public GenerateRomWindow(IServiceProvider serviceProvider)
_serviceProvider = serviceProvider;
_randomizer = serviceProvider.GetService();
_romGenerator = serviceProvider.GetService();
+ _locationConfig = serviceProvider.GetService().GetLocationConfig();
InitializeComponent();
SamusSprites.Add(Sprite.DefaultSamus);
@@ -63,7 +66,38 @@ public RandomizerOptions Options
{
DataContext = value;
_options = value;
+ PopulateItemOptions();
PopulateLogicOptions();
+ PopulateLocationOptions();
+ }
+ }
+
+ ///
+ /// Populates the options for early items
+ ///
+ public void PopulateItemOptions()
+ {
+ foreach (ItemType itemType in Enum.GetValues(typeof(ItemType)))
+ {
+ if (itemType.IsInAnyCategory(new[] { ItemCategory.Junk, ItemCategory.Scam, ItemCategory.Map, ItemCategory.Compass,
+ ItemCategory.SmallKey, ItemCategory.BigKey, ItemCategory.Keycard, ItemCategory.NonRandomized }) || itemType == ItemType.Nothing || itemType == ItemType.SilverArrows)
+ {
+ continue;
+ }
+
+ var itemTypeField = itemType.GetType().GetField(itemType.ToString());
+ var description = itemTypeField.GetCustomAttributes().FirstOrDefault().Description;
+ var checkBox = new CheckBox
+ {
+ Name = itemTypeField.Name,
+ Content = description,
+ Tag = itemType,
+ IsChecked = Options.SeedOptions.EarlyItems.Contains(itemType),
+ ToolTip = description
+ };
+ checkBox.Checked += EarlyItemCheckbox_Checked;
+ checkBox.Unchecked += EarlyItemCheckbox_Checked;
+ EarlyItemsGrid.Children.Add(checkBox);
}
}
@@ -99,6 +133,121 @@ public void PopulateLogicOptions()
}
}
+ ///
+ /// Populates the grid with all of the locations and the item options
+ ///
+ public void PopulateLocationOptions()
+ {
+ var world = new World(new(), "", 0, "");
+
+ // Populate the regions filter dropdown
+ LocationsRegionFilter.Items.Add("");
+ foreach (var region in world.Regions.OrderBy(x => x is Z3Region))
+ {
+ var name = $"{(region is Z3Region ? "Zelda" : "Metroid")} - {region.Name}";
+ LocationsRegionFilter.Items.Add(name);
+ }
+
+ // Create rows for each location to be able to specify the items at that location
+ var row = 0;
+ foreach (var location in world.Locations.OrderBy(x => x.Room == null ? "" : x.Room.Name).ThenBy(x => x.Name))
+ {
+ var locationDetails = _locationConfig.Locations.Single(x => x.Id == location.Id);
+ var name = locationDetails.ToString();
+ var toolTip = "";
+ if (locationDetails.Name.Count > 1)
+ {
+ toolTip = "AKA: " + string.Join(", ", locationDetails.Name.Where(x => x.Text != name).Select(x => x.Text)) + "\n";
+ }
+ toolTip += $"Vanilla item: {location.VanillaItem}";
+
+ var textBlock = new TextBlock
+ {
+ Text = name,
+ ToolTip = toolTip,
+ Tag = location,
+ Margin = new(0, 0, 10, 0),
+ VerticalAlignment = VerticalAlignment.Center,
+ Visibility = Visibility.Collapsed
+ };
+
+ LocationsGrid.Children.Add(textBlock);
+ LocationsGrid.RowDefinitions.Add(new RowDefinition());
+ Grid.SetColumn(textBlock, 0);
+ Grid.SetRow(textBlock, row);
+
+ var comboBox = CreateLocationComboBox(location);
+ LocationsGrid.Children.Add(comboBox);
+ Grid.SetColumn(comboBox, 1);
+ Grid.SetRow(comboBox, row);
+
+ row++;
+ }
+ }
+
+ ///
+ /// Creates a combo box for the item options for a location
+ ///
+ /// The location to generate the combo box for
+ /// The generated combo box
+ private ComboBox CreateLocationComboBox(Location location)
+ {
+ var comboBox = new ComboBox
+ {
+ Tag = location,
+ Visibility = Visibility.Collapsed,
+ Margin = new(0, 2, 5, 2),
+ };
+
+ var prevValue = 0;
+ if (Options.SeedOptions.LocationItems.ContainsKey(location.Id))
+ {
+ prevValue = Options.SeedOptions.LocationItems[location.Id];
+ }
+
+ var curIndex = 0;
+ var selectedIndex = 0;
+
+ // Add generic item placement options (Any, Progressive Items, Junk)
+ foreach (var itemPlacement in Enum.GetValues(typeof(ItemPool)))
+ {
+ if ((int)itemPlacement == prevValue)
+ {
+ selectedIndex = curIndex;
+ }
+
+ var itemPlacementField = itemPlacement.GetType().GetField(itemPlacement.ToString());
+ var description = itemPlacementField.GetCustomAttributes().FirstOrDefault();
+ comboBox.Items.Add(new LocationItemOption { Value = (int)itemPlacement, Text = description == null ? itemPlacementField.Name : description.Description });
+ curIndex++;
+ }
+
+ // Add specific progressive items
+ foreach (ItemType itemType in Enum.GetValues(typeof(ItemType)))
+ {
+ if (itemType.IsInAnyCategory(new[] { ItemCategory.Junk, ItemCategory.Scam, ItemCategory.Map, ItemCategory.Compass,
+ ItemCategory.SmallKey, ItemCategory.BigKey, ItemCategory.Keycard, ItemCategory.NonRandomized }) || itemType == ItemType.Nothing || itemType == ItemType.SilverArrows)
+ {
+ continue;
+ }
+
+ if ((int)itemType == prevValue)
+ {
+ selectedIndex = curIndex;
+ }
+
+ var itemTypeField = itemType.GetType().GetField(itemType.ToString());
+ var description = itemTypeField.GetCustomAttributes().FirstOrDefault();
+ comboBox.Items.Add(new LocationItemOption { Value = (int)itemType, Text = description == null ? itemTypeField.Name : description.Description });
+ curIndex++;
+ }
+
+ comboBox.SelectedIndex = selectedIndex;
+ comboBox.SelectionChanged += LocationsItemDropdown_SelectionChanged;
+
+ return comboBox;
+ }
+
public void LoadSprites()
{
var spritesPath = Path.Combine(
@@ -171,7 +320,7 @@ private void GenerateStatsButton_Click(object sender, RoutedEventArgs e)
var config = Options.ToConfig();
var randomizer = _serviceProvider.GetRequiredService();
- const int numberOfSeeds = 10000;
+ const int numberOfSeeds = 1000;
var progressDialog = new ProgressDialog(this, $"Generating {numberOfSeeds} seeds...");
var stats = InitStats();
var itemCounts = new ConcurrentDictionary<(int itemId, int locationId), int>();
@@ -352,5 +501,84 @@ private void LogicCheckBox_Checked(object sender, RoutedEventArgs e)
var property = type.GetProperty(checkBox.Name);
property.SetValue(Options.LogicConfig, checkBox.IsChecked ?? false);
}
+
+ ///
+ /// Updates to the dropdown to filter locations to specific regions
+ ///
+ /// The dropdown that was updated
+ /// The event object
+ private void LocationsRegionFilter_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ var comboBox = sender as ComboBox;
+ var selectedRegion = comboBox.SelectedItem as string;
+ foreach (FrameworkElement obj in LocationsGrid.Children)
+ {
+ if (obj.Tag is Location location)
+ {
+ obj.Visibility = selectedRegion.Contains(location.Region.Name) ? Visibility.Visible : Visibility.Collapsed;
+ }
+ }
+ }
+
+ ///
+ /// Handles updates
+ ///
+ ///
+ ///
+ private void LocationsItemDropdown_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ var comboBox = sender as ComboBox;
+ var option = comboBox.SelectedItem as LocationItemOption;
+ var location = comboBox.Tag as Location;
+ if (option.Value > 0)
+ {
+ Options.SeedOptions.LocationItems[location.Id] = option.Value;
+ }
+ else
+ {
+ Options.SeedOptions.LocationItems.Remove(location.Id);
+ }
+ }
+
+ ///
+ /// Resets all locations to be any item
+ ///
+ ///
+ ///
+ private void ResetAllLocationsButton_Click(object sender, RoutedEventArgs e)
+ {
+ foreach (FrameworkElement obj in LocationsGrid.Children)
+ {
+ if (obj is not ComboBox comboBox) continue;
+ var location = comboBox.Tag as Location;
+ comboBox.SelectedIndex = 0;
+ Options.SeedOptions.LocationItems.Remove(location.Id);
+ }
+ }
+
+ ///
+ /// Updates the EarlyItems based on when a checkbox is checked/unchecked using reflection
+ ///
+ /// The checkbox that was checked
+ /// The event object
+ private void EarlyItemCheckbox_Checked(object sender, RoutedEventArgs e)
+ {
+ var checkBox = sender as CheckBox;
+ var itemType = (ItemType)checkBox.Tag;
+ if (checkBox.IsChecked.HasValue && checkBox.IsChecked.Value)
+ Options.SeedOptions.EarlyItems.Add(itemType);
+ else
+ Options.SeedOptions.EarlyItems.Remove(itemType);
+ }
+
+ ///
+ /// Internal class for the location item option combo box
+ ///
+ private class LocationItemOption
+ {
+ public int Value { get; set; }
+ public string Text { get; set; }
+ public override string ToString() => Text;
+ }
}
}
diff --git a/src/Randomizer.App/Windows/RomListWindow.xaml b/src/Randomizer.App/Windows/RomListWindow.xaml
index 0bb63c959..f750e324c 100644
--- a/src/Randomizer.App/Windows/RomListWindow.xaml
+++ b/src/Randomizer.App/Windows/RomListWindow.xaml
@@ -54,6 +54,7 @@
+
diff --git a/src/Randomizer.App/Windows/RomListWindow.xaml.cs b/src/Randomizer.App/Windows/RomListWindow.xaml.cs
index 535589e65..659059f63 100644
--- a/src/Randomizer.App/Windows/RomListWindow.xaml.cs
+++ b/src/Randomizer.App/Windows/RomListWindow.xaml.cs
@@ -86,7 +86,11 @@ private void QuickPlayButton_Click(object sender, RoutedEventArgs e)
{
var successful = _romGenerator.GenerateRom(Options, out var romPath, out var error, out var rom);
- if (successful)
+ if (!successful && !string.IsNullOrEmpty(error))
+ {
+ MessageBox.Show(this, error, "SMZ3 Cas’ Randomizer", MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ else
{
UpdateRomList();
QuickLaunchRom(rom);
@@ -467,6 +471,22 @@ private void CopySeedMenuItem_Click(object sender, RoutedEventArgs e)
Clipboard.SetText(rom.Seed);
}
+ ///
+ /// Menu item for copying the seed's config string for sending to someone else
+ ///
+ ///
+ ///
+ private void CopyConfigMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MenuItem menuItem)
+ return;
+
+ if (menuItem.Tag is not GeneratedRom rom)
+ return;
+
+ Clipboard.SetText(rom.Settings);
+ }
+
///
/// Menu item for deleting a rom from the db and filesystem
///
diff --git a/src/Randomizer.SMZ3.Tracking/TrackerState.cs b/src/Randomizer.SMZ3.Tracking/TrackerState.cs
index dc0096ea8..c52131c22 100644
--- a/src/Randomizer.SMZ3.Tracking/TrackerState.cs
+++ b/src/Randomizer.SMZ3.Tracking/TrackerState.cs
@@ -212,7 +212,7 @@ public static async Task LoadAsync(Stream stream)
var secondsElapsed = trackerState.SecondsElapsed;
- var config = GeneratedRom.IsValid(generatedRom) ? JsonSerializer.Deserialize(generatedRom.Settings, s_options) : new Config();
+ var config = GeneratedRom.IsValid(generatedRom) ? Config.FromConfigString(generatedRom.Settings) : new Config();
return new TrackerState(
itemStates,
@@ -232,7 +232,7 @@ public static async Task LoadAsync(Stream stream)
/// The deserialized config
public static Config LoadConfig(GeneratedRom generatedRom)
{
- return GeneratedRom.IsValid(generatedRom) ? JsonSerializer.Deserialize(generatedRom.Settings, s_options) ?? new Config() : new Config(); ;
+ return GeneratedRom.IsValid(generatedRom) ? Config.FromConfigString(generatedRom.Settings) ?? new Config() : new Config(); ;
}
///
@@ -395,7 +395,7 @@ public Task SaveAsync(RandomizerContext dbContext, GeneratedRom rom)
if (rom != null)
{
- rom.Settings = JsonSerializer.Serialize(SeedConfig, s_options);
+ rom.Settings = Config.ToConfigString(SeedConfig, true);
}
if (rom != null)
diff --git a/src/Randomizer.SMZ3/Config.cs b/src/Randomizer.SMZ3/Config.cs
index 4b90e26c5..7d108c6d9 100644
--- a/src/Randomizer.SMZ3/Config.cs
+++ b/src/Randomizer.SMZ3/Config.cs
@@ -1,6 +1,11 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.ComponentModel;
-
+using System.IO;
+using System.IO.Compression;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Json;
using Randomizer.Shared;
namespace Randomizer.SMZ3
@@ -59,8 +64,8 @@ public enum ItemPool
[Description("Progression items")]
Progression,
- [Description("Non-junk items")]
- NonJunk,
+ /*[Description("Non-junk items")]
+ NonJunk,*/
[Description("Junk items")]
Junk,
@@ -161,6 +166,11 @@ public enum MenuSpeed
public class Config
{
+ private static readonly JsonSerializerOptions s_options = new()
+ {
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+
public GameMode GameMode { get; set; } = GameMode.Normal;
public Z3Logic Z3Logic { get; set; } = Z3Logic.Normal;
public SMLogic SMLogic { get; set; } = SMLogic.Normal;
@@ -192,6 +202,8 @@ public class Config
public bool MultiWorld => GameMode == GameMode.Multiworld;
public bool Keysanity => KeyShuffle != KeyShuffle.None;
+ public IDictionary LocationItems { get; set; } = new Dictionary();
+ public ISet EarlyItems { get; set; } = new HashSet();
public LogicConfig LogicConfig { get; set; } = new LogicConfig();
public Config SeedOnly()
@@ -200,5 +212,64 @@ public Config SeedOnly()
clone.GenerateSeedOnly = true;
return clone;
}
+
+ ///
+ /// Converts the config into a compressed string of the json
+ ///
+ /// The config to convert
+ /// If the config should be compressed
+ /// The string representation
+ public static string ToConfigString(Config config, bool compress)
+ {
+ var json = JsonSerializer.Serialize(config, s_options);
+ if (!compress) return json;
+ var buffer = Encoding.UTF8.GetBytes(json);
+ var memoryStream = new MemoryStream();
+ using (var gZipStream = new GZipStream(memoryStream, CompressionMode.Compress, true))
+ {
+ gZipStream.Write(buffer, 0, buffer.Length);
+ }
+
+ memoryStream.Position = 0;
+
+ var compressedData = new byte[memoryStream.Length];
+ memoryStream.Read(compressedData, 0, compressedData.Length);
+
+ var gZipBuffer = new byte[compressedData.Length + 4];
+ Buffer.BlockCopy(compressedData, 0, gZipBuffer, 4, compressedData.Length);
+ Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, gZipBuffer, 0, 4);
+ return Convert.ToBase64String(gZipBuffer);
+ }
+
+ ///
+ /// Takes in a compressed json config string and converts it into a config
+ ///
+ ///
+ /// The converted json data
+ public static Config FromConfigString(string configString)
+ {
+ if (configString.Contains("{"))
+ {
+ return JsonSerializer.Deserialize(configString, s_options);
+ }
+
+ var gZipBuffer = Convert.FromBase64String(configString);
+ using (var memoryStream = new MemoryStream())
+ {
+ var dataLength = BitConverter.ToInt32(gZipBuffer, 0);
+ memoryStream.Write(gZipBuffer, 4, gZipBuffer.Length - 4);
+
+ var buffer = new byte[dataLength];
+
+ memoryStream.Position = 0;
+ using (var gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
+ {
+ gZipStream.Read(buffer, 0, buffer.Length);
+ }
+
+ var json = Encoding.UTF8.GetString(buffer);
+ return JsonSerializer.Deserialize(json, s_options);
+ }
+ }
}
}
diff --git a/src/Randomizer.SMZ3/Generation/Smz3Randomizer.cs b/src/Randomizer.SMZ3/Generation/Smz3Randomizer.cs
index 059fa838b..431c45342 100644
--- a/src/Randomizer.SMZ3/Generation/Smz3Randomizer.cs
+++ b/src/Randomizer.SMZ3/Generation/Smz3Randomizer.cs
@@ -5,7 +5,7 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
-
+using Microsoft.Extensions.Logging;
using Randomizer.Shared;
using Randomizer.SMZ3.FileData;
@@ -15,10 +15,12 @@ public class Smz3Randomizer : IWorldAccessor
{
private static readonly Regex s_illegalCharacters = new(@"[^A-Z0-9]", RegexOptions.IgnoreCase);
private static readonly Regex s_continousSpace = new(@" +");
+ private readonly ILogger _logger;
- public Smz3Randomizer(IFiller filler)
+ public Smz3Randomizer(IFiller filler, ILogger logger)
{
Filler = filler;
+ _logger = logger;
}
public static string Name => "Super Metroid & A Link to the Past Cas’ Randomizer";
@@ -60,6 +62,8 @@ public SeedData GenerateSeed(Config config, string seed, CancellationToken cance
if (config.Race)
rng = new Random(rng.Next());
+ _logger.LogDebug($"Seed: {seedNumber}");
+
var worlds = new List();
if (config.SingleWorld)
worlds.Add(new World(config, "Player", 0, Guid.NewGuid().ToString("N")));
diff --git a/src/Randomizer.SMZ3/Playthrough.cs b/src/Randomizer.SMZ3/Playthrough.cs
index 900ae0ab9..71bb212a0 100644
--- a/src/Randomizer.SMZ3/Playthrough.cs
+++ b/src/Randomizer.SMZ3/Playthrough.cs
@@ -51,7 +51,7 @@ public static Playthrough Generate(IReadOnlyCollection worlds, Config con
/* With no new items added we might have a problem, so list inaccessable items */
var inaccessibleLocations = worlds.SelectMany(w => w.Locations).Where(l => !locations.Contains(l)).ToList();
if (inaccessibleLocations.Select(l => l.Item).Count() >= (15 * worlds.Count))
- throw new Exception("Too many inaccessible items, seed likely impossible.");
+ throw new RandomizerGenerationException("Too many inaccessible items, seed likely impossible.");
sphere.InaccessibleLocations.AddRange(inaccessibleLocations);
break;
@@ -62,7 +62,7 @@ public static Playthrough Generate(IReadOnlyCollection worlds, Config con
spheres.Add(sphere);
if (spheres.Count > 100)
- throw new Exception("Too many spheres, seed likely impossible.");
+ throw new RandomizerGenerationException("Too many spheres, seed likely impossible.");
}
return new Playthrough(config, spheres);
diff --git a/src/Randomizer.SMZ3/RandomizerGenerationException.cs b/src/Randomizer.SMZ3/RandomizerGenerationException.cs
new file mode 100644
index 000000000..b1fc49ac8
--- /dev/null
+++ b/src/Randomizer.SMZ3/RandomizerGenerationException.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace Randomizer.SMZ3
+{
+ ///
+ /// Class for housing potentially expected exceptions when generating seeds
+ ///
+ public class RandomizerGenerationException : Exception
+ {
+ ///
+ /// Initializes a new instance of the
+ /// class.
+ ///
+ public RandomizerGenerationException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the
+ /// class with the specified message.
+ ///
+ /// The error message.
+ public RandomizerGenerationException(string? message)
+ : base(message)
+ {
+ }
+ }
+}
diff --git a/src/Randomizer.SMZ3/StandardFiller.cs b/src/Randomizer.SMZ3/StandardFiller.cs
index 173a45c91..a7f810705 100644
--- a/src/Randomizer.SMZ3/StandardFiller.cs
+++ b/src/Randomizer.SMZ3/StandardFiller.cs
@@ -83,7 +83,7 @@ public void Fill(List worlds, Config config, CancellationToken cancellati
});
}
- ApplyItemPoolPreferences(junkItems, locations, worlds, config);
+ ApplyItemPoolPreferences(progressionItems, junkItems, locations, worlds, config);
_logger.LogDebug("Filling GT with junk");
GanonTowerFill(worlds, junkItems, 2);
@@ -97,30 +97,78 @@ public void Fill(List worlds, Config config, CancellationToken cancellati
FastFill(junkItems, locations);
}
- private void ApplyItemPoolPreferences(List- junkItems, List locations, List worlds, Config config)
+ private void ApplyItemPoolPreferences(List
- progressionItems, List
- junkItems, List locations, List worlds, Config config)
{
- ApplyPreference(config.ShaktoolItemPool, world => world.InnerMaridia.ShaktoolItem);
- ApplyPreference(config.PegWorldItemPool, world => world.DarkWorldNorthWest.PegWorld);
+ // Populate items that were directly specified at locations
+ var configLocations = config.LocationItems.Shuffle(Random);
+ foreach (var (locationId, value) in configLocations)
+ {
+ var location = worlds[0].Locations.FirstOrDefault(x => x.Id == locationId && x.Item == null);
+
+ if (location == null)
+ {
+ _logger.LogDebug($"Location could not be found or already has an item");
+ continue;
+ }
+
+ if (value < Enum.GetValues(typeof(ItemPool)).Length)
+ {
+ var itemPool = (ItemPool)value;
+
+ if (itemPool == ItemPool.Progression && progressionItems.Any())
+ {
+ // Some locations (AKA Shaktool) get pretty tough to tell if an item is needed there, so a workaround is to
+ // grab an item from the opposite game to minimize chances of situations where an item required to access a
+ // location is picked to go there
+ var item = progressionItems.FirstOrDefault(x => (x.Type.IsInCategory(ItemCategory.Metroid) && location.Region is Z3Region) || (x.Type.IsInCategory(ItemCategory.Zelda) && location.Region is SMRegion));
+
+ if (item != null)
+ {
+ FillItemAtLocation(progressionItems, item.Type, location);
+ }
+ else
+ {
+ _logger.LogDebug($"Could not find item to place at {location.Name}");
+ }
+ }
+ else if(itemPool == ItemPool.Junk && junkItems.Any())
+ {
+ FastFill(junkItems, worlds.SelectMany(x => x.Locations.Where(y => y.Id == locationId)));
+ }
+ }
+ else
+ {
+ var itemType = (ItemType)value;
+
+ if (progressionItems.Any(x => x.Type == itemType))
+ {
+ //var location = worlds[0].Locations.First(x => x.Id == locationId);
+ var itemsRequired = Logic.GetMissingRequiredItems(location, new Progression());
- void ApplyPreference(ItemPool setting, Func selectLocation)
+ // If no items required or at least one combination of items required does not contain this item
+ if (!itemsRequired.Any() || itemsRequired.Any(x => !x.Contains(itemType)))
+ {
+ FillItemAtLocation(progressionItems, itemType, location);
+ }
+ else
+ {
+ throw new RandomizerGenerationException($"{itemType} was selected as the item for {location}, but it is required to get there.");
+ }
+ }
+ }
+ }
+
+ // Push requested progression items to the top
+ var configItems = config.EarlyItems.Shuffle(Random);
+ var addedItems = new List();
+ foreach (var itemType in configItems)
{
- switch (setting)
+ if (progressionItems.Any(x => x.Type == itemType))
{
- case ItemPool.Progression:
- // If we always want to have a progression item, move it
- // to the top of the list so it gets filled early while
- // we still have all progression items in the pool. We
- // also add a filter to prevent it from filling it with
- // the wrong items.
- var locationId = selectLocation(worlds[0]).Id;
- var location = locations
- .MoveToTop(x => x.Id == locationId)
- .Allow((item, items) => !item.Type.IsInCategory(ItemCategory.Scam));
- break;
-
- case ItemPool.Junk:
- FastFill(junkItems, worlds.Select(selectLocation));
- break;
+ var accessibleLocations = worlds[0].Locations.Where(x => x.Item == null && x.IsAvailable(new Progression(addedItems))).Shuffle(Random);
+ var location = accessibleLocations.First();
+ FillItemAtLocation(progressionItems, itemType, location);
+ addedItems.Add(itemType);
}
}
}
@@ -169,27 +217,6 @@ private void InitialFillInOwnWorld(List
- dungeonItems, List
- progressi
{
FillItemAtLocation(dungeonItems, ItemType.KeySW, world.SkullWoods.PinballRoom);
- foreach (var (itemType, itemPlacement) in config.ItemLocations)
- {
- switch (itemPlacement)
- {
- case ItemPlacement.Original:
- var vanilla = GetVanillaLocation(itemType, world);
- if (vanilla == null)
- {
- _logger.LogError("Unable to determine vanilla location for {item}", itemType);
- continue;
- }
-
- FillItemAtLocation(progressionItems, itemType, vanilla);
- break;
-
- case ItemPlacement.Early:
- FrontFillItemInOwnWorld(progressionItems, itemType, world);
- break;
- }
- }
-
/* We place a PB and Super in Sphere 1 to make sure the filler
* doesn't start locking items behind this when there are a
* high chance of the trash fill actually making them available */
@@ -202,6 +229,7 @@ private void AssumedFill(List
- itemPool, List
- baseItems,
CancellationToken cancellationToken)
{
var assumedItems = new List
- (itemPool);
+ var failedAttempts = new Dictionary
- ();
while (assumedItems.Count > 0)
{
/* Try placing next item */
@@ -214,6 +242,17 @@ private void AssumedFill(List
- itemPool, List
- baseItems,
{
_logger.LogDebug("Could not find anywhere to place {item}", item);
assumedItems.Add(item);
+
+ if (!failedAttempts.ContainsKey(item))
+ {
+ failedAttempts[item] = 0;
+ }
+ failedAttempts[item]++;
+
+ if (failedAttempts[item] > 500)
+ {
+ throw new RandomizerGenerationException("Infinite loop in generation found. Specified item location combinations may not be possible.");
+ }
continue;
}
@@ -247,7 +286,10 @@ private IEnumerable
- CollectItems(IEnumerable
- items, IEnumerable itemPool, ItemType itemType, World world)
{
var item = itemPool.Get(itemType);
- var location = world.Locations.Empty().Available(world.Items).Random(Random);
+ var location = world.Locations.Empty()
+ .Available(world.Items)
+ .Where(x => world.Config.LocationItems == null || !world.Config.LocationItems.ContainsKey(x.Id))
+ .Random(Random);
if (location == null)
{
throw new InvalidOperationException($"Tried to front fill {item.Name} in, but no location was available");
diff --git a/src/Randomizer.Shared/Enums/ItemCategory.cs b/src/Randomizer.Shared/Enums/ItemCategory.cs
index 58d59bf44..8f5830c48 100644
--- a/src/Randomizer.Shared/Enums/ItemCategory.cs
+++ b/src/Randomizer.Shared/Enums/ItemCategory.cs
@@ -61,6 +61,11 @@ public enum ItemCategory
/// The item is especially numerious and can be found in a large number
/// of locations.
///
- Plentiful
+ Plentiful,
+
+ ///
+ /// This is an item that is not randomized, such as filled bottles
+ ///
+ NonRandomized
}
}
diff --git a/src/Randomizer.Shared/Enums/ItemType.cs b/src/Randomizer.Shared/Enums/ItemType.cs
index cce28c59f..977bc7796 100644
--- a/src/Randomizer.Shared/Enums/ItemType.cs
+++ b/src/Randomizer.Shared/Enums/ItemType.cs
@@ -549,43 +549,43 @@ public enum ItemType : byte
SpeedBooster = 0xBA,
[Description("Bottle with Red Potion")]
- [ItemCategory(ItemCategory.Zelda)]
+ [ItemCategory(ItemCategory.Zelda, ItemCategory.NonRandomized)]
BottleWithRedPotion = 0x2B,
[Description("Bottle with Green Potion")]
- [ItemCategory(ItemCategory.Zelda)]
+ [ItemCategory(ItemCategory.Zelda, ItemCategory.NonRandomized)]
BottleWithGreenPotion = 0x2C,
[Description("Bottle with Blue Potion")]
- [ItemCategory(ItemCategory.Zelda)]
+ [ItemCategory(ItemCategory.Zelda, ItemCategory.NonRandomized)]
BottleWithBluePotion = 0x2D,
[Description("Bottle with Fairy")]
- [ItemCategory(ItemCategory.Zelda)]
+ [ItemCategory(ItemCategory.Zelda, ItemCategory.NonRandomized)]
BottleWithFairy = 0x3D,
[Description("Bottle with Bee")]
- [ItemCategory(ItemCategory.Zelda)]
+ [ItemCategory(ItemCategory.Zelda, ItemCategory.NonRandomized)]
BottleWithBee = 0x3C,
[Description("Bottle with Gold Bee")]
- [ItemCategory(ItemCategory.Zelda)]
+ [ItemCategory(ItemCategory.Zelda, ItemCategory.NonRandomized)]
BottleWithGoldBee = 0x48,
[Description("Red Potion Refill")]
- [ItemCategory(ItemCategory.Zelda)]
+ [ItemCategory(ItemCategory.Zelda, ItemCategory.NonRandomized)]
RedContent = 0x2E,
[Description("Green Potion Refill")]
- [ItemCategory(ItemCategory.Zelda)]
+ [ItemCategory(ItemCategory.Zelda, ItemCategory.NonRandomized)]
GreenContent = 0x2F,
[Description("Blue Potion Refill")]
- [ItemCategory(ItemCategory.Zelda)]
+ [ItemCategory(ItemCategory.Zelda, ItemCategory.NonRandomized)]
BlueContent = 0x30,
[Description("Bee Refill")]
- [ItemCategory(ItemCategory.Zelda)]
+ [ItemCategory(ItemCategory.Zelda, ItemCategory.NonRandomized)]
BeeContent = 0x0E,
}
}
diff --git a/tests/Randomizer.SMZ3.Tests/LogicTests/RandomizerTests.cs b/tests/Randomizer.SMZ3.Tests/LogicTests/RandomizerTests.cs
index d5c3f5306..33e36f469 100644
--- a/tests/Randomizer.SMZ3.Tests/LogicTests/RandomizerTests.cs
+++ b/tests/Randomizer.SMZ3.Tests/LogicTests/RandomizerTests.cs
@@ -21,9 +21,8 @@ public class RandomizerTests
[InlineData("test", 2042629884)] // Smz3Randomizer v1.0
public void StandardFillerWithSameSeedGeneratesSameWorld(string seed, int expectedHash)
{
- var logger = GetLogger();
- var filler = new StandardFiller(logger);
- var randomizer = new Smz3Randomizer(filler);
+ var filler = new StandardFiller(GetLogger());
+ var randomizer = new Smz3Randomizer(filler, GetLogger());
var config = new Config();
var seedData = randomizer.GenerateSeed(config, seed, default);
@@ -48,6 +47,55 @@ private static int GetHashForWorld(World world)
return NonCryptographicHash.Fnv1a(serializedWorld);
}
+ [Fact]
+ public void LocationItemConfig()
+ {
+ var filler = new StandardFiller(GetLogger());
+ var randomizer = new Smz3Randomizer(filler, GetLogger());
+
+ var config = new Config();
+ var region = new Regions.Zelda.LightWorld.LightWorldSouth(null, null);
+ var location1 = region.LinksHouse.Id;
+ var location2 = region.MazeRace.Id;
+ var location3 = region.IceCave.Id;
+ config.LocationItems[location1] = (int)ItemPool.Progression;
+ config.LocationItems[location2] = (int)ItemPool.Junk;
+ config.LocationItems[location3] = (int)ItemType.Firerod;
+
+ for (var i = 0; i < 3; i++)
+ {
+ var seedData = randomizer.GenerateSeed(config, null, default);
+ var world = seedData.Worlds.First().World;
+ world.Locations.First(x => x.Id == location1).Item.Progression.Should().BeTrue();
+ world.Locations.First(x => x.Id == location2).Item.Progression.Should().BeFalse();
+ var fireRodAtLocation = world.Locations.First(x => x.Id == location3).Item.Type == ItemType.Firerod;
+ var fireRodAccessible = !Logic.GetMissingRequiredItems(world.Locations.First(x => x.Item.Type == ItemType.Firerod), new Progression()).Any();
+ Assert.True(fireRodAtLocation || fireRodAccessible);
+ }
+ }
+
+ [Fact]
+ public void EarlyItemConfig()
+ {
+ var filler = new StandardFiller(GetLogger());
+ var randomizer = new Smz3Randomizer(filler, GetLogger());
+
+ var config = new Config();
+ config.EarlyItems.Add(ItemType.Firerod);
+ config.EarlyItems.Add(ItemType.Icerod);
+ config.EarlyItems.Add(ItemType.MoonPearl);
+
+ for (var i = 0; i < 3; i++)
+ {
+ var seedData = randomizer.GenerateSeed(config, null, default);
+ var world = seedData.Worlds.First().World;
+ var progression = new Progression();
+ Logic.GetMissingRequiredItems(world.Locations.First(x => x.Item.Type == ItemType.Firerod), progression).Should().BeEmpty();
+ Logic.GetMissingRequiredItems(world.Locations.First(x => x.Item.Type == ItemType.Icerod), progression).Should().BeEmpty();
+ Logic.GetMissingRequiredItems(world.Locations.First(x => x.Item.Type == ItemType.MoonPearl), progression).Should().BeEmpty();
+ }
+ }
+
private static ILogger GetLogger()
{
var serviceCollection = new ServiceCollection()