From 2284517c790c883088d280e4a1649d8bfe6f93de Mon Sep 17 00:00:00 2001 From: Ellendar Date: Thu, 25 Jun 2026 03:11:03 -0400 Subject: [PATCH 01/10] Works but still not great. Internal rooms don't connect back, which doesn't currently matter but should be added for later Script management not implemented Possibly unrelated palace bug exists. --- CommandLine/Program.cs | 1 + .../ViewModels/GenerateRomViewModel.cs | 1 + DumpRooms/Program.cs | 1 + RandomizerCore/CustomTexts.cs | 128 +-- RandomizerCore/Enemy/Enemies.cs | 44 + RandomizerCore/Enemy/PalaceEnemyShuffler.cs | 1 + RandomizerCore/Hyrule.cs | 921 ++++++++---------- RandomizerCore/Music.cs | 9 +- RandomizerCore/Overworld/DeathMountain.cs | 42 +- RandomizerCore/Overworld/EastHyrule.cs | 235 ++--- RandomizerCore/Overworld/Location.cs | 110 ++- RandomizerCore/Overworld/LocationID.cs | 27 +- RandomizerCore/Overworld/MazeIsland.cs | 26 +- RandomizerCore/Overworld/WestHyrule.cs | 211 ++-- RandomizerCore/Overworld/World.cs | 83 +- RandomizerCore/ROM.cs | 80 +- RandomizerCore/RandomizerConfiguration.cs | 2 +- RandomizerCore/RandomizerProperties.cs | 2 +- RandomizerCore/RequirementType.cs | 2 + RandomizerCore/Requirements.cs | 3 + RandomizerCore/RomMap.cs | 1 + RandomizerCore/Shuffler.cs | 2 +- ...ranceDirectionItemRoomSelectionStrategy.cs | 2 +- .../ByShapeItemRoomSelectionStrategy.cs | 2 +- .../{ => Palace}/ChaosPalaceGenerator.cs | 4 +- .../{ => Palace}/CoordinatePalaceGenerator.cs | 2 +- .../{ => Palace}/ItemRoomSelectionStrategy.cs | 2 +- .../Sidescroll/{ => Palace}/Palace.cs | 126 ++- .../Sidescroll/{ => Palace}/PalaceColors.cs | 3 +- .../{ => Palace}/PalaceGenerator.cs | 2 +- .../Sidescroll/{ => Palace}/PalaceRooms.cs | 2 +- .../Sidescroll/{ => Palace}/Palaces.cs | 3 +- .../RandomItemRoomSelectionStrategy.cs | 2 +- .../RandomWalkCoordinatePalaceGenerator.cs | 2 +- .../ReconstructedLoopyPalaceGenerator.cs | 2 +- .../ReconstructedPalaceGenerator.cs | 6 +- .../Sidescroll/{ => Palace}/Room.cs | 2 +- .../Sidescroll/{ => Palace}/RoomExitType.cs | 2 +- RandomizerCore/Sidescroll/Palace/RoomGroup.cs | 6 + .../Sidescroll/{ => Palace}/RoomPool.cs | 2 +- .../{ => Palace}/SegmentConnectionType.cs | 2 +- ...ntialPlacementCoordinatePalaceGenerator.cs | 4 +- .../ShapeFirstCoordinatePalaceGenerator.cs | 4 +- .../TowerCoordinatePalaceGenerator.cs | 2 +- .../{ => Palace}/VanillaPalaceGenerator.cs | 8 +- .../{ => Palace}/VanillaRoomPool.cs | 2 +- .../VanillaShufflePalaceGenerator.cs | 2 +- .../VanillaWeightedPalaceGenerator.cs | 2 +- RandomizerCore/Sidescroll/RoomGroup.cs | 6 - RandomizerCore/Sidescroll/SideviewEnums.cs | 33 + RandomizerCore/Sidescroll/Town/Town.cs | 213 ++++ RandomizerCore/Sidescroll/Town/TownMap.cs | 49 + RandomizerCore/Sidescroll/Town/TownType.cs | 57 ++ RandomizerCore/Sidescroll/Town/Towns.cs | 182 ++++ .../Sidescroll/Town/VanillaTownMap.cs | 51 + RandomizerCore/Text.cs | 38 +- RandomizerCore/Town.cs | 135 --- RandomizerCore/TownEnum.cs | 137 +++ RandomizerCore/Towns.cs | 33 - RandomizerCore/Util.cs | 13 +- Statistics/Result.cs | 1 + Statistics/Statistics.cs | 1 + Tests/FlagsTests.cs | 2 +- Tests/RoomSerializationTests.cs | 1 + Tests/Utils.cs | 1 + ValidateRooms/Program.cs | 1 + Z2Randomizer.sln | 12 +- 67 files changed, 1779 insertions(+), 1317 deletions(-) rename RandomizerCore/Sidescroll/{ => Palace}/ByEntranceDirectionItemRoomSelectionStrategy.cs (96%) rename RandomizerCore/Sidescroll/{ => Palace}/ByShapeItemRoomSelectionStrategy.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/ChaosPalaceGenerator.cs (98%) rename RandomizerCore/Sidescroll/{ => Palace}/CoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/ItemRoomSelectionStrategy.cs (90%) rename RandomizerCore/Sidescroll/{ => Palace}/Palace.cs (96%) rename RandomizerCore/Sidescroll/{ => Palace}/PalaceColors.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/PalaceGenerator.cs (98%) rename RandomizerCore/Sidescroll/{ => Palace}/PalaceRooms.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/Palaces.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/RandomItemRoomSelectionStrategy.cs (98%) rename RandomizerCore/Sidescroll/{ => Palace}/RandomWalkCoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/ReconstructedLoopyPalaceGenerator.cs (96%) rename RandomizerCore/Sidescroll/{ => Palace}/ReconstructedPalaceGenerator.cs (98%) rename RandomizerCore/Sidescroll/{ => Palace}/Room.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/RoomExitType.cs (98%) create mode 100644 RandomizerCore/Sidescroll/Palace/RoomGroup.cs rename RandomizerCore/Sidescroll/{ => Palace}/RoomPool.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/SegmentConnectionType.cs (95%) rename RandomizerCore/Sidescroll/{ => Palace}/SequentialPlacementCoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/ShapeFirstCoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/TowerCoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/VanillaPalaceGenerator.cs (94%) rename RandomizerCore/Sidescroll/{ => Palace}/VanillaRoomPool.cs (97%) rename RandomizerCore/Sidescroll/{ => Palace}/VanillaShufflePalaceGenerator.cs (95%) rename RandomizerCore/Sidescroll/{ => Palace}/VanillaWeightedPalaceGenerator.cs (99%) delete mode 100644 RandomizerCore/Sidescroll/RoomGroup.cs create mode 100644 RandomizerCore/Sidescroll/Town/Town.cs create mode 100644 RandomizerCore/Sidescroll/Town/TownMap.cs create mode 100644 RandomizerCore/Sidescroll/Town/TownType.cs create mode 100644 RandomizerCore/Sidescroll/Town/Towns.cs create mode 100644 RandomizerCore/Sidescroll/Town/VanillaTownMap.cs delete mode 100644 RandomizerCore/Town.cs create mode 100644 RandomizerCore/TownEnum.cs delete mode 100644 RandomizerCore/Towns.cs diff --git a/CommandLine/Program.cs b/CommandLine/Program.cs index 63c085c7f..b30c5052f 100644 --- a/CommandLine/Program.cs +++ b/CommandLine/Program.cs @@ -6,6 +6,7 @@ using NLog; using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.CommandLine; diff --git a/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs b/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs index 20e1c532c..5a0075651 100644 --- a/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs +++ b/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs @@ -16,6 +16,7 @@ using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; using CrossPlatformUI.Services; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace CrossPlatformUI.ViewModels; diff --git a/DumpRooms/Program.cs b/DumpRooms/Program.cs index 70c0788c0..e65401844 100644 --- a/DumpRooms/Program.cs +++ b/DumpRooms/Program.cs @@ -5,6 +5,7 @@ using System.Text.Json; using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; ROM ROMData = new ROM(args[0], true); diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index 0e09c2b12..22a5c6e2e 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using Z2Randomizer.RandomizerCore.Overworld; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -76,16 +77,16 @@ public class CustomTexts ]; //Indexes in the hints list - private static readonly Dictionary TOWN_SIGN_INDEXES = new() + private static readonly Dictionary TOWN_SIGN_INDEXES = new() { - {Town.RAURU, 11}, - {Town.RUTO, 20}, - {Town.SARIA_NORTH, 29}, - {Town.MIDO_WEST, 41}, - {Town.NABOORU, 62}, - {Town.DARUNIA_WEST, 76}, - {Town.NEW_KASUTO, 86}, - {Town.OLD_KASUTO, 94}, + {TownType.RAURU, 11}, + {TownType.RUTO, 20}, + {TownType.SARIA, 29}, + {TownType.MIDO, 41}, + {TownType.NABOORU, 62}, + {TownType.DARUNIA, 76}, + {TownType.NEW_KASUTO, 86}, + {TownType.OLD_KASUTO, 94}, }; public const int WEST_TEXT_COUNT = 52; @@ -113,18 +114,16 @@ public class CustomTexts private const int HELPFUL_HINTS_COUNT = 4; //private static readonly Dictionary spellTextIndexes = { 15, 24, 35, 46, 70, 81, 93, 96 }; - private static readonly Dictionary townWizardTextIndexes = new() + private static readonly Dictionary townWizardTextIndexes = new() { - { Town.RAURU, 15 }, - { Town.RUTO, 24 }, - { Town.SARIA_NORTH, 35 }, - { Town.MIDO_WEST, 46 }, - { Town.MIDO_CHURCH, downstabGuyGotItemTextIndex }, - { Town.NABOORU, 70 }, - { Town.DARUNIA_ROOF, upstabGuyGotItemTextIndex }, - { Town.DARUNIA_WEST, 81 }, - { Town.NEW_KASUTO, 93 }, - { Town.OLD_KASUTO, 96 }, + { TownType.RAURU, 15 }, + { TownType.RUTO, 24 }, + { TownType.SARIA, 35 }, + { TownType.MIDO, 46 }, + { TownType.NABOORU, 70 }, + { TownType.DARUNIA, 81 }, + { TownType.NEW_KASUTO, 93 }, + { TownType.OLD_KASUTO, 96 }, }; private static readonly Dictionary wizardTextIndexesBySpell = new() @@ -387,18 +386,16 @@ public class CustomTexts "i wish$i had a$%%$like yours" ]; - public static readonly Dictionary WIZARD_SPELL_TEXTS_BY_TOWN = new() + public static readonly Dictionary WIZARD_SPELL_TEXTS_BY_TOWN = new() { - { Town.RAURU, [] }, - { Town.RUTO, ["A winner$is you"] }, - { Town.SARIA_NORTH, ["Water$you$doing?"] }, - { Town.MIDO_WEST, [] }, - { Town.MIDO_CHURCH, [] }, - { Town.NABOORU, ["Do not$drink the$fountain$water here$...$trust me."] }, - { Town.DARUNIA_ROOF, [] }, - { Town.DARUNIA_WEST, ["You saved$a kid$for this?", "Dont$forget to$get upstab"] }, - { Town.NEW_KASUTO, [] }, - { Town.OLD_KASUTO, ["Sorry$about the$moas"] } + { TownType.RAURU, [] }, + { TownType.RUTO, ["A winner$is you"] }, + { TownType.SARIA, ["Water$you$doing?"] }, + { TownType.MIDO, [] }, + { TownType.NABOORU, ["Do not$drink the$fountain$water here$...$trust me."] }, + { TownType.DARUNIA, ["You saved$a kid$for this?", "Dont$forget to$get upstab"] }, + { TownType.NEW_KASUTO, [] }, + { TownType.OLD_KASUTO, ["Sorry$about the$moas"] } }; public static readonly Dictionary WIZARD_SPELL_TEXTS_BY_COLLECTABLE = new() @@ -535,26 +532,29 @@ public static List GenerateTexts( GenerateSpellHints(locations, texts, props); } - Location baguLocation = locations.FirstOrDefault(i => i.ActualTown == Town.BAGU)!; + Location baguLocation = locations.FirstOrDefault(i => i.Town?.Type == TownType.BAGU)!; if(baguLocation == null) { throw new Exception("Bagu location not found while generating text"); } - Text? baguText = GenerateBaguText(baguLocation.Collectables[0], nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); + Text? baguText = GenerateBaguText(baguLocation.GetAllCollectables()[0], nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); if(baguText != null) { texts[baguTextIndex] = baguText; } - Location tableLocation = locations.FirstOrDefault(i => i.ActualTown == Town.SARIA_TABLE)!; - Text? mirrorText = GenerateMirrorTableText(tableLocation.Collectables[0], nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); + Collectable tableColletable = (Collectable)locations.First(i => i.Town?.Type == TownType.SARIA) + .Town!.GetTownMap(VanillaTownMap.SARIA_TABLE)!.Collectable!; + Text? mirrorText = GenerateMirrorTableText(tableColletable, nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); if (mirrorText != null) { texts[gotMirrorTextIndex] = mirrorText; } - Location fountainLocation = locations.FirstOrDefault(i => i.ActualTown == Town.NABOORU_FOUNTAIN)!; - Text? fountainText = GenerateFountainText(fountainLocation.Collectables[0], nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); + + Collectable fountainCollectable = (Collectable)locations.First(i => i.Town?.Type == TownType.NABOORU) + .Town!.GetTownMap(VanillaTownMap.NABOORU_MID)!.Collectable!; + Text? fountainText = GenerateFountainText(fountainCollectable, nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); if (fountainText != null) { texts[gotWaterTextIndex] = fountainText; @@ -571,17 +571,17 @@ public static List GenerateTexts( if (props.SpellItemHints && props.IncludeSwordTechsInShuffle) { - var downstabLoc = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.DOWNSTAB)); - var upstabLoc = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.UPSTAB)); + var downstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.DOWNSTAB)); + var upstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.UPSTAB)); Text hint; - if (downstabLoc == null) + if (props.StartWithDownstab) { hint = props.UseCommunityText ? new Text(STARTED_WITH_ITEM_TEXTS.Sample(nonhashRNG)!, Collectable.DOWNSTAB) : new Text(NON_COMMUNITY_STARTED_WITH_TEXT, Collectable.DOWNSTAB); } else { - hint = Text.GenerateHelpfulHint(locations.ToList(), downstabLoc, Collectable.DOWNSTAB, props.IncludeSpellsInShuffle); + hint = Text.GenerateHelpfulHint(downstabLoc, Collectable.DOWNSTAB, props.IncludeSpellsInShuffle); } texts[downstabClosedDoorTextIndex] = hint; if (upstabLoc == null) @@ -591,7 +591,7 @@ public static List GenerateTexts( } else { - hint = Text.GenerateHelpfulHint(locations.ToList(), upstabLoc, Collectable.UPSTAB, props.IncludeSpellsInShuffle); + hint = Text.GenerateHelpfulHint(upstabLoc, Collectable.UPSTAB, props.IncludeSpellsInShuffle); } texts[upstabClosedDoorTextIndex] = hint; if (props.SwapUpAndDownStab) @@ -683,9 +683,9 @@ private static Text GenerateBaguWoodsHint(Location bagu) private static void GenerateTownNameHints(List texts, IEnumerable locations, bool linkedFire) { - foreach (Location location in locations.Where(i => i.ActualTown != null && i.VanillaCollectable.IsSpell())) + foreach (Location location in locations.Where(i => i.Town?.GetWizard() != null)) { - texts[TOWN_SIGN_INDEXES[(Town)location.ActualTown!]] = GenerateTownSignHint(location.Collectables[0], linkedFire); + texts[TOWN_SIGN_INDEXES[(TownType)location.Town!.Type!]] = GenerateTownSignHint((Collectable)location.Town.GetWizard()!.Collectable!, linkedFire); } } private static Text GenerateTownSignHint(Collectable spell, bool linkedFire) @@ -701,12 +701,12 @@ private static Text GenerateTownSignHint(Collectable spell, bool linkedFire) private static Text GenerateWizardText(List texts, Random r, Location location, bool useCommunityText) { - if(location.ActualTown == null) + if(location.Town == null) { throw new Exception("Cannot generate text for Wizard outside of town"); } - Town town = (Town)location.ActualTown; - Collectable collectable = location.Collectables[0]; + TownType town = (TownType)location.Town.Type!; + Collectable collectable = (Collectable)location.Town.GetWizard()!.Collectable!; if (collectable.IsSpell() || collectable == Collectable.DOWNSTAB || collectable == Collectable.UPSTAB) @@ -860,7 +860,7 @@ private static List GenerateHelpfulHints(List hints, IEnumerable placedTowns = []; - List items = locations.SelectMany(i => i.Collectables).ToList(); + List items = locations.SelectMany(i => i.GetAllCollectables()).ToList(); items = items.Where(i => !i.IsInternalUse()).ToList(); if (props.SpellItemHints && props.IncludeSwordTechsInShuffle) @@ -921,7 +921,7 @@ private static List GenerateHelpfulHints(List hints, IEnumerable possibleHintLocations = locations.Where(i => i.Collectables.Contains(hintCollectable)).ToList(); + List possibleHintLocations = locations.Where(i => i.GetAllCollectables().Contains(hintCollectable)).ToList(); Location hintLocation; int town; @@ -941,9 +941,9 @@ private static List GenerateHelpfulHints(List hints, IEnumerable locations, List i.Collectables.Contains(Collectable.TROPHY))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.TROPHY))!; if(itemLocation != null) { - Text trophyHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.TROPHY, props.IncludeSpellsInShuffle); + Text trophyHint = Text.GenerateHelpfulHint(itemLocation, Collectable.TROPHY, props.IncludeSpellsInShuffle); hints[trophySpellHintIndex] = trophyHint; } - itemLocation = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.MEDICINE))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.MEDICINE))!; if (itemLocation != null) { - Text medHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.MEDICINE, props.IncludeSpellsInShuffle); + Text medHint = Text.GenerateHelpfulHint(itemLocation, Collectable.MEDICINE, props.IncludeSpellsInShuffle); hints[medicineSpellHintIndex] = medHint; } - itemLocation = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.CHILD))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.CHILD))!; if (itemLocation != null) { - Text kidHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.CHILD, props.IncludeSpellsInShuffle); + Text kidHint = Text.GenerateHelpfulHint(itemLocation, Collectable.CHILD, props.IncludeSpellsInShuffle); hints[childSpellHintIndex] = kidHint; } if (props.IncludeQuestItemsInShuffle) { - itemLocation = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.MIRROR))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.MIRROR))!; if (itemLocation != null) { - Text mirrorHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.MIRROR, props.IncludeSpellsInShuffle); + Text mirrorHint = Text.GenerateHelpfulHint(itemLocation, Collectable.MIRROR, props.IncludeSpellsInShuffle); hints[mirrorSpellHintIndex] = mirrorHint; } - itemLocation = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.WATER))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.WATER))!; if (itemLocation != null) { - Text waterHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.WATER, props.IncludeSpellsInShuffle); + Text waterHint = Text.GenerateHelpfulHint(itemLocation, Collectable.WATER, props.IncludeSpellsInShuffle); hints[waterSpellHintIndex] = waterHint; } } @@ -1027,10 +1027,10 @@ private static void GenerateWizardTexts(List texts, IEnumerable { List vanillaText = new(texts); List usedWizardTexts = []; - List wizardLocations = itemLocs.Where(i => i.ActualTown != 0 && (i?.ActualTown?.IsWizardTown() ?? false)).ToList(); + List wizardLocations = itemLocs.Where(i => i.Town?.GetWizard() != null).ToList(); foreach (Location location in wizardLocations) { - if(location.ActualTown == null) + if(location.Town == null) { throw new Exception("Invalid town that is not a town"); } @@ -1041,7 +1041,7 @@ private static void GenerateWizardTexts(List texts, IEnumerable wizardHint = GenerateWizardText(vanillaText, r, location, useCommunityText); } while (useCommunityText && usedWizardTexts.Contains(wizardHint) && tries++ < 100); usedWizardTexts.Add(wizardHint); - texts[townWizardTextIndexes[(Town)location.ActualTown]] = wizardHint; + texts[townWizardTextIndexes[(TownType)location.Town.Type!]] = wizardHint; } } diff --git a/RandomizerCore/Enemy/Enemies.cs b/RandomizerCore/Enemy/Enemies.cs index 10865afea..949e8dafb 100644 --- a/RandomizerCore/Enemy/Enemies.cs +++ b/RandomizerCore/Enemy/Enemies.cs @@ -281,6 +281,50 @@ public enum EnemiesEast BOULDER_TOSSING_LIZALFOS = 0x1D, } +/// +/// IDs for town "enemies" +/// +public enum EnemiesTown +{ + FAIRY = 0x00, + CANDLE = 0x01, + DOOR = 0x02, + PURPLE_KEESE_03 = 0x03, + PURPLE_BOT = 0x04, + BIT = 0x05, + PURPLE_MOA = 0x06, + PURPLE_KEESE_07 = 0x07, + GOLD_GIRL = 0x08, + TOWNPERSON_09 = 0x09, + TALKING_ACHE = 0x0A, + TALKING_BIT = 0x0B, + INVISIBLE_DIALOG = 0x0C, + RIVER_MAN = 0x0D, + BLUE_LUMBERJACK_0E = 0x0E, + WIZARD = 0x0F, + KID = 0x10, + STATIONARY_BLUE_LADY_WITH_BOWL_11 = 0x11, + STATIONARY_RED_GRANNY = 0x12, + STATIONARY_RED_LUMBERJACK = 0x13, + STATIONARY_BLUE_LADY_WITH_BOWL_14 = 0x14, + STATIONARY_BLUE_GRANNY = 0x15, + STATIONARY_RED_LADY = 0x16, + LIFE_REFILL_LADY = 0x17, + MAGIC_REFILL_GRANNY = 0x18, + BLUE_LUMBERJACK_19 = 0x19, + RED_LADY_WITH_BOWL = 0x1A, + RED_GRANNY = 0x1B, + BLUE_LADY = 0x1C, + BLUE_LUMBERJACK_1D = 0x1D, + RED_GUY_1E = 0x1E, + RED_GUY_1F = 0x1F, + WALKING_LADY = 0x20, + MIRROR = 0x21, + SIGN = 0x22, + BUSY_TOWNSPERSON_GENERATOR = 0x23 + +} + /// /// IDs that are shared in palace 1-6 /// diff --git a/RandomizerCore/Enemy/PalaceEnemyShuffler.cs b/RandomizerCore/Enemy/PalaceEnemyShuffler.cs index 3985776c0..f9d0620be 100644 --- a/RandomizerCore/Enemy/PalaceEnemyShuffler.cs +++ b/RandomizerCore/Enemy/PalaceEnemyShuffler.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore.Enemy; diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index f265edda7..b78dde4ed 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -1,4 +1,9 @@ -using System; +using DynamicData; +using FtRandoLib.Importer; +using js65; +using NLog; +using NLog.Targets; +using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; @@ -8,12 +13,11 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using NLog; -using js65; -using FtRandoLib.Importer; using Z2Randomizer.RandomizerCore.Enemy; using Z2Randomizer.RandomizerCore.Overworld; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -261,7 +265,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC } } ItemGet.Remove(props.ReplaceFireWithDash ? Collectable.FIRE_SPELL : Collectable.DASH_SPELL); - accessibleMagicContainers = 4; + reachableAreas = new HashSet(); //areasByLocation = new SortedDictionary>(); @@ -321,12 +325,9 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC AsmModule sideviewModule = new(); passedValidation = await FillPalaceRooms(sideviewModule); - assembler.Add(sideviewModule); } - //palaces.ForEach(i => Debug.WriteLine(i.GetLayoutDebug(PalaceStyle.TOWER, false))); - ROMData.WriteKasutoJarAmount(props.NewKasutoBasementRequirement); ROMData.DoHackyFixes(); ROMData.AdjustGpProjectileDamage(); @@ -352,6 +353,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC firstProcessOverworldTimestamp = DateTime.Now; await ProcessOverworld(progress, ct); + if (ct.IsCancellationRequested) { return new RandomizerResult(false); } UpdateProgress(progress, 8); @@ -366,43 +368,29 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC { List? customSpellOrder = props.IncludeSpellsInShuffle ? null - : AllLocationsForReal() - .Where(l => l.ActualTown != null && Towns.STRICT_SPELL_LOCATIONS.Contains((Town)l.ActualTown)) - //This makes the assumption that is currently true that each "town" has exactly one item. - //If we later restructure towns to be omni-towns to get rid of fake towns, this will be untrue - .Select(l => l.Collectables[0]).ToList(); + : AllLocations() + .Where(l => l.Town?.GetWizard() != null) + .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); ROMData.CombineFireSpell(assembler, customSpellOrder, r); } - Dictionary spellMap = new() - { - { (Town)westHyrule.locationAtRauru.ActualTown!, westHyrule.locationAtRauru.Collectables[0] }, - { (Town)westHyrule.locationAtMido.ActualTown!, westHyrule.locationAtMido.Collectables[0] }, - { (Town)westHyrule.locationAtSariaNorth.ActualTown!, westHyrule.locationAtSariaNorth.Collectables[0] }, - { (Town)westHyrule.locationAtRuto.ActualTown!, westHyrule.locationAtRuto.Collectables[0] }, - { (Town)eastHyrule.townAtNabooru.ActualTown!, eastHyrule.townAtNabooru.Collectables[0] }, - { (Town)eastHyrule.townAtDarunia.ActualTown!, eastHyrule.townAtDarunia.Collectables[0] }, - { (Town)eastHyrule.townAtNewKasuto.ActualTown!, eastHyrule.townAtNewKasuto.Collectables[0] }, - { (Town)eastHyrule.townAtOldKasuto.ActualTown!, eastHyrule.townAtOldKasuto.Collectables[0] }, - }; - - if (props.StartsWithCollectable(spellMap[Town.RUTO])) + if (props.StartsWithCollectable(westHyrule.ruto.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b14, 0x10); //Trophy } - if (props.StartsWithCollectable(spellMap[Town.SARIA_NORTH])) + if (props.StartsWithCollectable(westHyrule.sariaNorth.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b15, 0x01); //Mirror } - if (props.StartsWithCollectable(spellMap[Town.MIDO_WEST])) + if (props.StartsWithCollectable(westHyrule.mido.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b16, 0x40); //Medicine } - if (props.StartsWithCollectable(spellMap[Town.NABOORU])) + if (props.StartsWithCollectable(eastHyrule.nabooru.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b17, 0x01); //Water } - if (props.StartsWithCollectable(spellMap[Town.DARUNIA_WEST])) + if (props.StartsWithCollectable(eastHyrule.darunia.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b18, 0x20); //Child } @@ -414,7 +402,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC if (ct.IsCancellationRequested) { return new RandomizerResult(false); } UpdateProgress(progress, 9); - List texts = CustomTexts.GenerateTexts(AllLocationsForReal(), itemLocs, ROMData.GetGameText(), props, r); + List texts = CustomTexts.GenerateTexts(AllLocations(), itemLocs, ROMData.GetGameText(), props, r); StatRandomizer randomizedStats = new(ROMData, props); randomizedStats.Randomize(r, skipDifficultyOnly: shareSeedAcrossDifficulty); @@ -657,75 +645,67 @@ private static void SanitizeHashCharacters(byte[] z2Hash) // hand out a duplicate. private void ReplaceDifficultyStartingItemsInWorld(Random r) { - List minorItems = [ - Collectable.BLUE_JAR, Collectable.RED_JAR, Collectable.SMALL_BAG, - Collectable.MEDIUM_BAG, Collectable.LARGE_BAG, Collectable.XL_BAG, - Collectable.ONEUP, Collectable.KEY - ]; Collectable[] difficultyStartingItems = [Collectable.CANDLE, Collectable.CROSS]; - + Collectable[] minorItems = Enum.GetValues().Where(i => i.IsMinorItem()).ToArray(); foreach (Collectable item in difficultyStartingItems) { if (!props.StartsWithCollectable(item)) { continue; } + Collectable replacementItem = minorItems.Sample(r); foreach (Location location in itemLocs) { - for (int i = 0; i < location.Collectables.Count; i++) + if(location.ReplaceCollectable(item, replacementItem)) { - if (location.Collectables[i] == item) - { - location.Collectables[i] = minorItems.Sample(r); - } + return; } } } } - /* - Text Notes: - - Community Text Changes - ---------------------- - Shield Spell 15 43 - Cannot Help 16 35 - Jump Spell 24 34 - Life Spell 35 37 - You know..? 37 42 - Fairy 46 37 - Downstab 47 38 - Bagu 48 44 - Fire 70 43 - You know 71 34 - Reflect 81 37 - Upstab 82 32 - Spell 93 25 - Thunder 96 36 - */ + //Shuffle the palace itself, not just the metadata + private void ShufflePalaces() + { + if (!props.PalacesCanSwapContinent) return; + List palaces = [westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, westHyrule.locationAtPalace3, mazeIsland.locationAtPalace4, eastHyrule.locationAtPalace5, eastHyrule.locationAtPalace6]; + + if (props.P7shuffle) + { + palaces.Add(eastHyrule.locationAtGP); + } + + for (int i = palaces.Count - 1; i > 0; i--) + { + int swap = r.Next(i + 1); + (palaces[i].Palace, palaces[swap].Palace) = (palaces[swap].Palace, palaces[i].Palace); + } + + } private void ShuffleItems() { - List shufflableItems = [ - Collectable.CANDLE, Collectable.GLOVE, Collectable.RAFT, Collectable.BOOTS, - Collectable.FLUTE, Collectable.CROSS, Collectable.HEART_CONTAINER, Collectable.HEART_CONTAINER, - Collectable.MAGIC_CONTAINER, Collectable.MEDICINE, Collectable.TROPHY, Collectable.HEART_CONTAINER, - Collectable.HEART_CONTAINER, Collectable.MAGIC_CONTAINER, Collectable.MAGIC_KEY, Collectable.MAGIC_CONTAINER, - Collectable.HAMMER, Collectable.CHILD, Collectable.MAGIC_CONTAINER]; + List shufflableItems = []; + List minorItems = [Collectable.BLUE_JAR, Collectable.RED_JAR, Collectable.SMALL_BAG, Collectable.MEDIUM_BAG, Collectable.LARGE_BAG, Collectable.XL_BAG, Collectable.ONEUP, Collectable.KEY]; if (props.PbagItemShuffle) { - westHyrule.pbagCave.Collectables = [(Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE)]; - eastHyrule.pbagCave1.Collectables = [(Collectable)ROMData.GetByte(RomMap.EAST_PBAG_CAVE1_COLLECTABLE)]; - eastHyrule.pbagCave2.Collectables = [(Collectable)ROMData.GetByte(RomMap.EAST_PBAG_CAVE2_COLLECTABLE)]; - shufflableItems.Add(westHyrule.pbagCave.Collectables[0]); - shufflableItems.Add(eastHyrule.pbagCave1.Collectables[0]); - shufflableItems.Add(eastHyrule.pbagCave2.Collectables[0]); + Collectable collectable = (Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE); + westHyrule.pbagCave.SetCollectables([collectable]); + shufflableItems.Add(collectable); + + collectable = (Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE); + eastHyrule.pbagCave1.SetCollectables([collectable]); + shufflableItems.Add(collectable); + + collectable = (Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE); + eastHyrule.pbagCave2.SetCollectables([collectable]); + shufflableItems.Add(collectable); } else { - westHyrule.pbagCave.Collectables = [Collectable.LARGE_BAG]; - eastHyrule.pbagCave1.Collectables = [Collectable.LARGE_BAG]; - eastHyrule.pbagCave2.Collectables = [Collectable.XL_BAG]; + westHyrule.pbagCave.SetCollectables([Collectable.LARGE_BAG]); + eastHyrule.pbagCave1.SetCollectables([Collectable.LARGE_BAG]); + eastHyrule.pbagCave2.SetCollectables([Collectable.XL_BAG]); } if(props.IncludeSpellsInShuffle) @@ -797,32 +777,59 @@ private void ShuffleItems() } pbagHearts = []; + List heartContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.HEART_CONTAINER)).ToList(); + //Replace any unused heart containers with small items if (heartContainersInItemPool < 4) { int heartContainersToRemove = 4 - heartContainersInItemPool; while (heartContainersToRemove > 0) { - int remove = r.Next(shufflableItems.Count); - if (shufflableItems[remove] == Collectable.HEART_CONTAINER) + Location hcLocation = heartContainerLocations.Sample(r)!; + List collectables = hcLocation.GetAllCollectables(); + List order = Enumerable.Range(0, collectables.Count).ToList(); + order.FisherYatesShuffle(r); + foreach(int index in order) { - shufflableItems[remove] = minorItems[r.Next(minorItems.Count)]; - heartContainersToRemove--; + if (collectables[index] == Collectable.HEART_CONTAINER) + { + collectables[index] = minorItems.Sample(r); + hcLocation.SetCollectables(collectables); + heartContainersToRemove--; + if (!collectables.Any(i => i == Collectable.HEART_CONTAINER)) + { + heartContainerLocations.Remove(hcLocation); + } + break; + } } } } + List magicContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.MAGIC_CONTAINER)).ToList(); //Replace any unused magic containers with small items if (magicContainersInItemPool < 4) { int magicContainersToRemove = 4 - magicContainersInItemPool; while (magicContainersToRemove > 0) { - int remove = r.Next(shufflableItems.Count); - if (shufflableItems[remove] == Collectable.MAGIC_CONTAINER) + Location mcLocation = magicContainerLocations.Sample(r)!; + List collectables = mcLocation.GetAllCollectables(); + List order = Enumerable.Range(0, collectables.Count).ToList(); + order.FisherYatesShuffle(r); + foreach (int index in order) { - shufflableItems[remove] = minorItems[r.Next(minorItems.Count)]; - magicContainersToRemove--; + if (collectables[index] == Collectable.MAGIC_CONTAINER) + { + collectables[index] = minorItems.Sample(r); + mcLocation.SetCollectables(collectables); + magicContainersToRemove--; + if (!collectables.Any(i => i == Collectable.MAGIC_CONTAINER)) + { + magicContainerLocations.Remove(mcLocation); + } + break; + } } } } @@ -843,25 +850,26 @@ private void ShuffleItems() } else if(!props.IncludeSpellsInShuffle) { - Collectable townCollectable; - townCollectable = westHyrule.AllLocations.First(i => i.ActualTown == Town.RUTO).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + Collectable wizardCollectable; + //wizardCollectable = westHyrule.AllLocations.First(i => i.ActualTown == TownType.RUTO).Collectables[0]; + wizardCollectable = (Collectable)westHyrule.ruto.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[10] == Collectable.TROPHY); shufflableItems[10] = minorItems[r.Next(minorItems.Count)]; ItemGet[Collectable.TROPHY] = true; } - townCollectable = westHyrule.AllLocations.First(i => i.ActualTown == Town.MIDO_WEST).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + wizardCollectable = (Collectable)westHyrule.mido.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[9] == Collectable.MEDICINE); shufflableItems[9] = minorItems[r.Next(minorItems.Count)]; ItemGet[Collectable.MEDICINE] = true; } - townCollectable = eastHyrule.AllLocations.First(i => i.ActualTown == Town.DARUNIA_WEST).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + wizardCollectable = (Collectable)eastHyrule.darunia.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[17] == Collectable.CHILD); shufflableItems[17] = minorItems[r.Next(minorItems.Count)]; @@ -870,16 +878,16 @@ private void ShuffleItems() if (props.IncludeQuestItemsInShuffle) { - townCollectable = westHyrule.AllLocations.First(i => i.ActualTown == Town.SARIA_NORTH).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + wizardCollectable = (Collectable)westHyrule.sariaNorth.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[mirrorIndex] == Collectable.MIRROR); shufflableItems[mirrorIndex] = minorItems[r.Next(minorItems.Count)]; ItemGet[Collectable.MIRROR] = true; } - townCollectable = eastHyrule.AllLocations.First(i => i.ActualTown == Town.NABOORU).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + wizardCollectable = (Collectable)eastHyrule.nabooru.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[waterindex] == Collectable.WATER); shufflableItems[waterindex] = minorItems[r.Next(minorItems.Count)]; @@ -927,7 +935,18 @@ private void ShuffleItems() } } - if(props.IncludeSwordTechsInShuffle) + TownMap? townMap = itemLocs.SelectMany(i => i.Town?.TownMaps ?? []).FirstOrDefault(i => i.Map == (int)VanillaTownMap.MIDO_TRAINER); + if(townMap != null) + { + townMap.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + } + townMap = itemLocs.SelectMany(i => i.Town?.TownMaps ?? []).FirstOrDefault(i => i.Map == (int)VanillaTownMap.DARUNIA_TRAINER); + if (townMap != null) + { + townMap.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + } + + if (props.IncludeSwordTechsInShuffle) { if (props.StartWithDownstab) { @@ -1029,7 +1048,7 @@ private void ShuffleItems() //Do the actual shuffling List duplicateItemPlacementCandidates = []; - palaces.ForEach(i => i.ItemRooms.ForEach(j => j.Collectable = null)); + if (props.MixOverworldPalaceItems) { duplicateItemPlacementCandidates.AddRange(itemLocs); @@ -1044,13 +1063,13 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location palaceLocation in itemLocs.Where(i => i.PalaceNumber != null)) + foreach (Location palaceLocation in itemLocs.Where(i => i.Palace != null)) { shufflableItemLocations.Add(palaceLocation); - Collectable vanillaCollectable = Palace.GetVanillaCollectable(palaceLocation.PalaceNumber); + Collectable vanillaCollectable = palaceLocation.Palace!.GetVanillaCollectable(); itemsToActuallyShuffle.Add(shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)); - for (int i = 1; i < props.PalaceItemRoomCounts[(int)palaceLocation.PalaceNumber! - 1]; i++) + for (int i = 1; i < props.PalaceItemRoomCounts[(int)palaceLocation.Palace.Number! - 1]; i++) { itemsToActuallyShuffle.Add(minorItems.Sample(r)); } @@ -1058,22 +1077,23 @@ private void ShuffleItems() duplicateItemPlacementCandidates.AddRange(shufflableItemLocations); DoShuffle(itemsToActuallyShuffle, shufflableItemLocations); } - else + else //no shuffle palace items { - foreach (Location palaceLocation in itemLocs.Where(i => i.PalaceNumber != null && i.PalaceNumber < 7)) + foreach (Location palaceLocation in itemLocs.Where(i => i.Palace != null && i.Palace.Number < 7)) { - Collectable vanillaCollectable = Palace.GetVanillaCollectable(palaceLocation.PalaceNumber); - palaceLocation.Collectables = [shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)]; - palaces[(int)palaceLocation.PalaceNumber! - 1].ItemRooms.Sample(r)!.Collectable = vanillaCollectable; - for (int i = 0; i < props.PalaceItemRoomCounts[(int)palaceLocation.PalaceNumber! - 1]; i++) + int palaceNumber = palaceLocation.Palace!.Number; + Collectable vanillaCollectable = palaceLocation.Palace!.GetVanillaCollectable(); + List collectables = []; + collectables = [shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)]; + for (int i = 0; i < props.PalaceItemRoomCounts[(int)palaceNumber! - 1]; i++) { - if (palaces[(int)palaceLocation.PalaceNumber! - 1].ItemRooms[i].Collectable == null) + if (palaces[(int)palaceNumber! - 1].ItemRooms[i].Collectable == null) { Collectable smallItem = minorItems.Sample(r); - palaceLocation.Collectables.Add(smallItem); - palaces[(int)palaceLocation.PalaceNumber! - 1].ItemRooms[i].Collectable = smallItem; + collectables.Add(smallItem); } } + palaceLocation.SetCollectables(collectables); } } @@ -1081,91 +1101,90 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location nonPalaceLocation in itemLocs.Where(i => i.PalaceNumber == null)) + foreach (Location nonPalaceLocation in itemLocs.Where(i => i.Palace == null)) { shufflableItemLocations.Add(nonPalaceLocation); - itemsToActuallyShuffle.Add(shufflableItems.Contains(nonPalaceLocation.VanillaCollectable) - ? nonPalaceLocation.VanillaCollectable : minorItems.Sample(r)); + foreach(Collectable collectable in nonPalaceLocation.GetAllCollectables()) + { + itemsToActuallyShuffle.Add(shufflableItems.Contains(collectable) ? collectable : minorItems.Sample(r)); + } } duplicateItemPlacementCandidates.AddRange(shufflableItemLocations); DoShuffle(itemsToActuallyShuffle, shufflableItemLocations); } else { - foreach (Location nonPalaceLocation in itemLocs.Where(i => i.PalaceNumber == null)) + foreach (Location nonPalaceLocation in itemLocs.Where(i => i.Palace == null)) { - Collectable vanillaCollectable = nonPalaceLocation.VanillaCollectable; - nonPalaceLocation.Collectables = [shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)]; + List collectables = nonPalaceLocation.GetAllCollectables(); + for(int i = 0; i < collectables.Count; i++) + { + collectables[i] = shufflableItems.Contains(collectables[i]) ? collectables[i] : minorItems.Sample(r); + } + nonPalaceLocation.SetCollectables(collectables); } } } //Assigning unshuffled locations can make the number of containers wrong, so re-adjust them - List heartContainerLocations = itemLocs.Where(i => i.Collectables.Contains(Collectable.HEART_CONTAINER)).ToList(); - int heartContainerCount = itemLocs.SelectMany(i => i.Collectables).Count(i => i == Collectable.HEART_CONTAINER); + heartContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.HEART_CONTAINER)).ToList(); + int heartContainerCount = heartContainerLocations.SelectMany(i => i.GetAllCollectables()).Count(i => i == Collectable.HEART_CONTAINER); while(heartContainerCount > heartContainersInItemPool) { Location location = heartContainerLocations.Sample(r)!; - int index = r.Next(location.Collectables.Count); - if (location.Collectables[index] == Collectable.HEART_CONTAINER) + Collectable minorItem = minorItems.Sample(r); + if(!location.ReplaceCollectable(Collectable.HEART_CONTAINER, minorItem)) { - location.Collectables[index] = minorItems.Sample(r); - heartContainerCount--; - if (!location.Collectables.Any(i => i == Collectable.HEART_CONTAINER)) - { - heartContainerLocations.Remove(location); - } + heartContainerLocations.Remove(location); } } - while(heartContainerCount < heartContainersInItemPool) - { - List minorItemLocations = itemLocs.Where(i => i.Collectables.Any(j => j.IsMinorItem())).ToList(); + List minorItemLocations = AllLocations().Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); + while (heartContainerCount < heartContainersInItemPool) + { Location location = minorItemLocations.Sample(r)!; - int index = r.Next(location.Collectables.Count); - if (location.Collectables[index].IsMinorItem()) + Collectable[] locationMinorItems = location.GetAllCollectables().Where(i => i.IsMinorItem()).ToArray(); + if(locationMinorItems.Length == 0) { - location.Collectables[index] = Collectable.HEART_CONTAINER; - heartContainerCount++; - if (!location.Collectables.Any(i => i.IsMinorItem())) - { - minorItemLocations.Remove(location); - } + minorItemLocations.Remove(location); + continue; } + if (!location.ReplaceCollectable(locationMinorItems.Sample(r), Collectable.HEART_CONTAINER)) + { + heartContainerLocations.Remove(location); + } + heartContainerCount++; } - List magicContainerLocations = itemLocs.Where(i => i.Collectables.Contains(Collectable.MAGIC_CONTAINER)).ToList(); - int magicContainerCount = itemLocs.SelectMany(i => i.Collectables).Count(i => i == Collectable.MAGIC_CONTAINER); + + magicContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.MAGIC_CONTAINER)).ToList(); + int magicContainerCount = magicContainerLocations.SelectMany(i => i.GetAllCollectables()).Count(i => i == Collectable.MAGIC_CONTAINER); while (magicContainerCount > magicContainersInItemPool) { Location location = magicContainerLocations.Sample(r)!; - int index = r.Next(location.Collectables.Count); - if (location.Collectables[index] == Collectable.MAGIC_CONTAINER) + Collectable minorItem = minorItems.Sample(r); + if (!location.ReplaceCollectable(Collectable.MAGIC_CONTAINER, minorItem)) { - location.Collectables[index] = minorItems.Sample(r); - magicContainerCount--; - if (!location.Collectables.Any(i => i == Collectable.MAGIC_CONTAINER)) - { - magicContainerLocations.Remove(location); - } + magicContainerLocations.Remove(location); } } + minorItemLocations = AllLocations().Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); + while (magicContainerCount < magicContainersInItemPool) { - List minorItemLocations = itemLocs.Where(i => i.Collectables.Any(j => j.IsMinorItem())).ToList(); - Location location = minorItemLocations.Sample(r)!; - int index = r.Next(location.Collectables.Count); - if (location.Collectables[index].IsMinorItem()) + Collectable[] locationMinorItems = location.GetAllCollectables().Where(i => i.IsMinorItem()).ToArray(); + if (locationMinorItems.Length == 0) { - location.Collectables[index] = Collectable.MAGIC_CONTAINER; - magicContainerCount++; - if (!location.Collectables.Any(i => i.IsMinorItem())) - { - minorItemLocations.Remove(location); - } + minorItemLocations.Remove(location); + continue; + } + if (!location.ReplaceCollectable(locationMinorItems.Sample(r), Collectable.MAGIC_CONTAINER)) + { + magicContainerLocations.Remove(location); } + magicContainerCount++; } if (props.AllowImportantItemDuplicates) @@ -1187,8 +1206,8 @@ private void ShuffleItems() importantItemsToDuplicate = importantItemsToDuplicate.Where(shufflableItems.Contains).ToList(); - List minorItemLocations = duplicateItemPlacementCandidates.Where(i => i.Collectables.Any(c => c.IsMinorItem())).ToList(); - int replaceableMinorItemCount = duplicateItemPlacementCandidates.Sum(l => l.Collectables.Count(c => c.IsMinorItem())); + minorItemLocations = duplicateItemPlacementCandidates.Where(i => i.GetAllCollectables().Any(c => c.IsMinorItem())).ToList(); + int replaceableMinorItemCount = duplicateItemPlacementCandidates.Sum(l => l.GetAllCollectables().Count(c => c.IsMinorItem())); importantItemsToDuplicate = importantItemsToDuplicate.GetRange(0, int.Min(replaceableMinorItemCount, importantItemsToDuplicate.Count)); importantItemsToDuplicate.FisherYatesShuffle(r); @@ -1197,13 +1216,16 @@ private void ShuffleItems() for (int itemIndex = 0; itemIndex < importantItemsToDuplicate.Count; itemIndex++) { Location minorItemLocation = minorItemLocations.Sample(r)!; - minorItemLocation.Collectables.FisherYatesShuffle(r); - for(int collectableIndex = 0; collectableIndex < minorItemLocation.Collectables.Count; collectableIndex++) + List locationCollectables = minorItemLocation.GetAllCollectables(); + List collectableIndexes = Enumerable.Range(0, minorItemLocation.GetAllCollectables().Count()).ToList(); + collectableIndexes.FisherYatesShuffle(r); + foreach(int collectableIndex in collectableIndexes) { - if(minorItemLocation.Collectables[collectableIndex].IsMinorItem()) + if(locationCollectables[collectableIndex].IsMinorItem()) { - minorItemLocation.Collectables[collectableIndex] = importantItemsToDuplicate[itemIndex]; - if(!minorItemLocation.Collectables.Any(c => c.IsMinorItem())) + locationCollectables[collectableIndex] = importantItemsToDuplicate[itemIndex]; + minorItemLocation.SetCollectables(locationCollectables); + if(!locationCollectables.Any(c => c.IsMinorItem())) { minorItemLocations.Remove(minorItemLocation); } @@ -1216,48 +1238,24 @@ private void ShuffleItems() private void DoShuffle(List itemsToShuffle, List itemShuffleLocations) { - int itemShuffleLocationsCount = itemShuffleLocations.Count; - if(itemShuffleLocations.Any(i => i.PalaceNumber != null)) - { - itemShuffleLocationsCount = itemShuffleLocationsCount - - itemShuffleLocations.Count(i => i.PalaceNumber != null) - + props.PalaceItemRoomCounts.Sum(i => i); - //- props.PalaceItemRoomCounts.Where(i => i == 0).Count(); - } - if (itemsToShuffle.Count != itemShuffleLocationsCount) + int possibleItemPlacements = itemShuffleLocations.Sum(i => i.GetAllCollectables().Count); + + if (itemsToShuffle.Count != possibleItemPlacements) { throw new Exception("Item locations must match number of items"); } itemsToShuffle.FisherYatesShuffle(r); - //Clear all locations, then set them to the newly shuffled items - itemShuffleLocations.ForEach(i => i.Collectables.Clear()); - using var itemLocsIterator = itemShuffleLocations.GetEnumerator(); - Location? location = null; - int subIndex = 0; - foreach (Collectable item in itemsToShuffle) + int itemIndex = 0; + foreach(Location location in itemShuffleLocations) { - if (location?.PalaceNumber == null) - { - if (!itemLocsIterator.MoveNext()) { throw new InvalidOperationException("Ran out of item locations."); } - location = itemLocsIterator.Current; - } - while (location.PalaceNumber is int palaceNum && ++subIndex > props.PalaceItemRoomCounts[palaceNum - 1]) - { - subIndex = 0; - if (!itemLocsIterator.MoveNext()) { throw new InvalidOperationException("Ran out of item locations."); } - location = itemLocsIterator.Current; - } - - location.Collectables.Add(item); - if (location.PalaceNumber is int palaceNumInIf) - { - palaces[palaceNumInIf - 1].ItemRooms[subIndex - 1].Collectable = item; - } + int itemCount = location.GetAllCollectables().Count; + List collectables = itemsToShuffle.GetRange(itemIndex, itemCount); + itemIndex += itemCount; + location.SetCollectables(collectables); } - Debug.Assert(!itemLocsIterator.MoveNext(), "All item locations were not used. This should not happen."); } private async Task FillPalaceRooms(AsmModule sideviewModule) @@ -1476,7 +1474,7 @@ private bool IsEverythingReachable(Dictionary itemGet) if (ItemGet[item] == false && item.IsItemGetItem()) { itemGetReachableFailures++; -#if UNSAFE_DEBUG +//#if UNSAFE_DEBUG if (reachableLocationsCount >= DEBUG_THRESHOLD) { Debug.WriteLine("Failed on collectables"); @@ -1485,7 +1483,7 @@ private bool IsEverythingReachable(Dictionary itemGet) //Debug.WriteLine(westHyrule.GetMapDebug()); return false; } -#endif +//#endif return false; } } @@ -1549,50 +1547,38 @@ private bool CanGet(Location location) { return location.Reachable; } - - /// - /// Removes the intermediate room between entering the spell house and reaching the old man's basement - /// private void ShortenWizards() { - /* - Spell swap notes: - Shield exit: 0xC7BB, 0xC1; enter: 0xC7EC, 0x90 //change map 48 pointer to map 40 pointer - Jump exit: 0xC7BF, 0xC5; enter: 0xC7F0, 0x94 //change map 49 pointer to map 41 pointer - Life exit: 0xC7C3, 0xC9; enter 0xC7F4, 0x98 //change map 50 pointer to map 42 pointer - Fairy exit: 0xC7C7, 0xCD; enter 0xC7F8, 0x9C //change map 51 pointer to map 43 pointer - Fire exit: 0xC7Cb, 0xD1; enter 0xC7FC, 0xA0 //change map 52 pointer to map 44 pointer - Reflect exit: 0xC7Cf, 0xD5; enter 0xC800, 0xA4 //change map 53 pointer to map 45 pointer - Spell exit: 0xC7D3, 0x6A; enter 0xC795, 0xC796, 0x4D //new kasuto item? - Thunder exit: 0xC7D7, 0xDD; enter 0xC808, 0xAC - Downstab exit: 0xC7DB, 0xE1; enter 0xC80C, 0xB0 - Upstab exit: 0xC7DF, 0xE5; enter 0xC810, 0xB4 - */ - for (int i = 0; i < 16; i = i + 2) - { - ROMData.Put(0xC611 + i, (byte)0x75); - ROMData.Put(0xC611 + i + 1, (byte)0x70); - ROMData.Put(0xC593 + i, (byte)0x48); - ROMData.Put(0xC593 + i + 1, (byte)0x9B); - } - ROMData.Put(0xC7BB, (byte)0x07); - ROMData.Put(0xC7BF, (byte)0x13); - ROMData.Put(0xC7C3, (byte)0x21); - ROMData.Put(0xC7C7, (byte)0x27); - ROMData.Put(0xC7CB, (byte)0x37); - ROMData.Put(0xC7CF, (byte)0x3F); - ROMData.Put(0xC850, (byte)0xB0); - //ROMData.put(0xC7D3, (byte)0x4D); - ROMData.Put(0xC7D7, (byte)0x5E); - ROMData.Put(0xC7DF, (byte)0x43); - ROMData.Put(0xC870, (byte)0xB8); - ROMData.Put(0xC7E3, (byte)0x49); - ROMData.Put(0xC874, (byte)0xA8); - ROMData.Put(0xC7D3, (byte)0x4D); - ROMData.Put(0xC7DB, (byte)0x29); - //ROMData.put(0xC7E3, (byte)0x49); - // ROMData.put(0xC874, (byte)0xA8); - //ROMData.put(0x8560, (byte)0xBC); + //Change the exit leading to the intermedaite wizard room to go directly to the basement + ROMData.Put(0xC827 + 1 * 4 + 2, 36 << 2); + ROMData.Put(0xC827 + 4 * 4 + 3, 37 << 2); + ROMData.Put(0xC827 + 8 * 4 + 1, 38 << 2); + ROMData.Put(0xC827 + 9 * 4 + 3, 39 << 2); + ROMData.Put(0xC827 + 13 * 4 + 3, 40 << 2); + ROMData.Put(0xC827 + 15 * 4 + 3, 41 << 2); + ROMData.Put(0xC827 + 19 * 4 + 1, 42 << 2); + ROMData.Put(0xC827 + 23 * 4 + 2, 43 << 2); + + //Change the exit from the wizard room to point back out to the town + ROMData.Put(0xC827 + 36 * 4, (1 << 2) + 2); + ROMData.Put(0xC827 + 37 * 4, (4 << 2) + 3); + ROMData.Put(0xC827 + 38 * 4, (8 << 2) + 1); + ROMData.Put(0xC827 + 39 * 4, (9 << 2) + 3); + ROMData.Put(0xC827 + 40 * 4, (13 << 2) + 3); + ROMData.Put(0xC827 + 41 * 4, (15 << 2) + 3); + ROMData.Put(0xC827 + 42 * 4, (19 << 2) + 1); + ROMData.Put(0xC827 + 43 * 4, (23 << 2) + 2); + + //downstab guy + ROMData.Put(0xC827 + 10 * 4 + 1, 44 << 2); + ROMData.Put(0xC827 + 44 * 4, (10 << 2) + 1); + + //granny's basement + ROMData.Put(0xC827 + 18 * 4 + 1, 46 << 2); + ROMData.Put(0xC827 + 46 * 4, (18 << 2) + 1); + + //upstab guy entrance is unchanged because you drop into it, but still change the exit to lead directly out + ROMData.Put(0xC827 + 45 * 4, (16 << 2) + 3); } /// @@ -1604,9 +1590,9 @@ private int UpdateItemGets(RandomizerProperties props) List requireables; accessibleMagicContainers = props.StartMagicContainers; accessibleHeartContainers = props.StartHearts; - Location newKasuto = eastHyrule.AllLocations.First(i => i.ActualTown == Town.NEW_KASUTO); + Location newKasuto = eastHyrule.newKasuto; List gottenItems = [], lastIterationGottenItems = []; - List locations = AllLocationsForReal().ToList(); + List locations = AllLocations().ToList(); List heartContainerGetLocations = [], magicContainerGetLocations = []; bool stalled = false; //No more delayed evaluation because too many locations could give you containers or require containers. @@ -1617,41 +1603,20 @@ private int UpdateItemGets(RandomizerProperties props) accessibleMagicContainers = props.StartMagicContainers + magicContainerGetLocations.Count; requireables = GetRequireables(props); gottenItems = []; - foreach (Location location in AllLocationsForReal()) + foreach (Location location in AllLocations()) { - foreach (Collectable collectable in location.Collectables) - { - if (collectable.IsInternalUse()) - { - continue; - } - bool canGet; - //Location is a palace - if (location.PalaceNumber != null && location.PalaceNumber < 7) - { - Debug.Assert(location.CollectableRequirements == World.DEFAULT_PALACE_REQUIREMENTS); - Palace palace = palaces[(int)location.PalaceNumber - 1]; - canGet = CanGet(location) - //All palaces inherently require fairy spell or magic key - && location.CollectableRequirements.AreSatisfiedBy(requireables) - && palace.GetGettableItems(requireables).Contains(collectable); - } - else - { - canGet = CanGet(location) && location.CollectableRequirements.AreSatisfiedBy(requireables); - } + List gettableItems = location.GetGettableItems(requireables); - ItemGet[collectable] = canGet; - if (canGet) - { - gottenItems.Add(collectable); - } + foreach(Collectable item in gettableItems) + { + ItemGet[item] = true; + gottenItems.Add(item); - if (canGet && collectable == Collectable.HEART_CONTAINER && !heartContainerGetLocations.Contains(location)) + if (item == Collectable.HEART_CONTAINER && !heartContainerGetLocations.Contains(location)) { heartContainerGetLocations.Add(location); } - if (canGet && collectable == Collectable.MAGIC_CONTAINER && !magicContainerGetLocations.Contains(location)) + if (item == Collectable.MAGIC_CONTAINER && !magicContainerGetLocations.Contains(location)) { magicContainerGetLocations.Add(location); } @@ -1746,7 +1711,8 @@ private async Task ProcessOverworld(Func progress, CancellationTok worlds.Add(deathMountain); worlds.Add(eastHyrule); worlds.Add(mazeIsland); - ResetTowns(); + + AssignPalaceLocations(); if (props.ContinentConnections == ContinentConnectionType.NORMAL) { @@ -1759,7 +1725,7 @@ private async Task ProcessOverworld(Func progress, CancellationTok } else if (props.ContinentConnections == ContinentConnectionType.TRANSPORTATION_SHUFFLE) { - List chosen = new List(); + List chosen = []; int type = r.Next(4); if (props.WestBiome.UsesVanillaMap() || props.DmBiome.UsesVanillaMap()) @@ -2036,7 +2002,6 @@ private async Task ProcessOverworld(Func progress, CancellationTok timeSpentBuildingMI += (int)DateTime.Now.Subtract(timestamp).TotalMilliseconds; worlds.Where(i => i.biome == Biome.VANILLA_SHUFFLE).ToList().ForEach(i => i.DisableDisallowedPassthroughs()); - worlds.ForEach(i => i.SynchronizeLinkedLocations()); if (ct.IsCancellationRequested) { return; } UpdateProgress(progress, 6); @@ -2064,11 +2029,8 @@ private async Task ProcessOverworld(Func progress, CancellationTok location.Reachable = false; } - eastHyrule.spellTower.Reachable = false; - westHyrule.ResetVisitabilityState(); - eastHyrule.ResetVisitabilityState(); - mazeIsland.ResetVisitabilityState(); - deathMountain.ResetVisitabilityState(); + worlds.ForEach(i => i.ResetVisitabilityState()); + worlds.ForEach(i => i.ResetCollectables(props)); //There was a spooky extra call to LoadItemLocs that used to be here that shouldn't be needed, but be aware. westHyrule.SetStart(); @@ -2191,132 +2153,86 @@ private bool AllContinentsAreConnected((Location, Location)[] connections) return continentsFound.Count == 4; } - private void ResetTowns() - { - westHyrule.locationAtRauru.ActualTown = Town.RAURU; - westHyrule.locationAtRuto.ActualTown = Town.RUTO; - westHyrule.locationAtSariaNorth.ActualTown = Town.SARIA_NORTH; - westHyrule.locationAtSariaSouth.ActualTown = Town.SARIA_SOUTH; - westHyrule.locationAtMido.ActualTown = Town.MIDO_WEST; - eastHyrule.townAtNabooru.ActualTown = Town.NABOORU; - eastHyrule.townAtDarunia.ActualTown = Town.DARUNIA_WEST; - eastHyrule.townAtNewKasuto.ActualTown = Town.NEW_KASUTO; - eastHyrule.townAtOldKasuto.ActualTown = Town.OLD_KASUTO; - } - - private void ShufflePalaces() - { - if (!props.PalacesCanSwapContinent) return; - List pals = [westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, westHyrule.locationAtPalace3, mazeIsland.locationAtPalace4, eastHyrule.locationAtPalace5, eastHyrule.locationAtPalace6]; - - if (props.P7shuffle) - { - pals.Add(eastHyrule.locationAtGP); - } - - for (int i = pals.Count - 1; i > 0; i--) - { - int swap = r.Next(i + 1); - Util.Swap(pals[i], pals[swap]); - } - - } - //ItemLocs is specifically only those locations that contain shufflable items //If you're looking for a global reference for which items are where... too bad it doesn't exist :( private List LoadItemLocs(int[] itemsPerPalaces) { List GetPalacesWithItems(IEnumerable palaces) => - palaces.Where(p => p.PalaceNumber is int n && n != 7 && props.PalaceItemRoomCounts[n - 1] != 0).ToList(); + palaces.Where(p => p.Palace?.Number is int n && n != 7 && props.PalaceItemRoomCounts[n - 1] != 0).ToList(); // Listed somewhat in order of expected progression. This way we can // use the indexes of two locations to determine approx distance. itemLocs = new List(30); + //palaces itemLocs = GetPalacesWithItems([ westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, - westHyrule.locationAtPalace3 + westHyrule.locationAtPalace3, + mazeIsland.locationAtPalace4, + eastHyrule.locationAtPalace5, + eastHyrule.locationAtPalace6, + eastHyrule.locationAtGP ]); + + //always live locations itemLocs.AddRange([ westHyrule.grassTile, westHyrule.heartContainerCave, westHyrule.magicContainerCave, westHyrule.medicineCave, westHyrule.trophyCave, - ]); - if (props.PbagItemShuffle) - { - itemLocs.Add(westHyrule.pbagCave); - } - if (props.IncludeQuestItemsInShuffle) - { - itemLocs.Add(westHyrule.bagu); - } - if (props.IncludeSpellsInShuffle) - { - itemLocs.Add(westHyrule.locationAtRauru); - itemLocs.Add(westHyrule.locationAtRuto); - itemLocs.Add(westHyrule.locationAtSariaNorth); - } - if (props.IncludeQuestItemsInShuffle) - { - itemLocs.Add(westHyrule.mirrorTable); - } - if (props.IncludeSpellsInShuffle) - { - itemLocs.Add(westHyrule.locationAtMido); - } - if (props.IncludeSwordTechsInShuffle) - { - itemLocs.Add(westHyrule.midoChurch); - } - - itemLocs.AddRange([ deathMountain.specRock, deathMountain.hammerCave, - ]); - - itemLocs.AddRange(GetPalacesWithItems([ - eastHyrule.locationAtPalace5, - eastHyrule.locationAtPalace6, - eastHyrule.locationAtGP - ])); - itemLocs.AddRange([ eastHyrule.waterTile, eastHyrule.desertTile, + mazeIsland.childDrop, + mazeIsland.magicContainerDrop ]); + + //pbag shuffle if (props.PbagItemShuffle) { + itemLocs.Add(westHyrule.pbagCave); itemLocs.Add(eastHyrule.pbagCave1); itemLocs.Add(eastHyrule.pbagCave2); } + + + //Quest item shuffle if (props.IncludeQuestItemsInShuffle) { - itemLocs.Add(eastHyrule.fountain); - } - if (props.IncludeSpellsInShuffle) - { - itemLocs.Add(eastHyrule.townAtNabooru); - itemLocs.Add(eastHyrule.townAtDarunia); - } - if (props.IncludeSwordTechsInShuffle) - { - itemLocs.Add(eastHyrule.daruniaRoof); - } - if (props.IncludeSpellsInShuffle) - { - itemLocs.Add(eastHyrule.townAtOldKasuto); - itemLocs.Add(eastHyrule.townAtNewKasuto); + itemLocs.Add(westHyrule.bagu); } - itemLocs.Add(eastHyrule.newKasutoBasement); - itemLocs.Add(eastHyrule.spellTower); + westHyrule.bagu.Town!.GetTownMap(VanillaTownMap.BAGU_INDOORS)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + westHyrule.sariaNorth.Town!.GetTownMap(VanillaTownMap.SARIA_TABLE)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + eastHyrule.nabooru.Town!.GetTownMap(VanillaTownMap.NABOORU_MID)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; - itemLocs.AddRange(GetPalacesWithItems([mazeIsland.locationAtPalace4])); - itemLocs.AddRange([ - mazeIsland.childDrop, - mazeIsland.magicContainerDrop - ]); + + itemLocs.Add(westHyrule.rauru); + itemLocs.Add(westHyrule.ruto); + itemLocs.Add(westHyrule.sariaNorth); + itemLocs.Add(westHyrule.mido); + itemLocs.Add(eastHyrule.nabooru); + itemLocs.Add(eastHyrule.darunia); + itemLocs.Add(eastHyrule.oldKasuto); + itemLocs.Add(eastHyrule.newKasuto); + + westHyrule.rauru.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + westHyrule.ruto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + westHyrule.sariaNorth.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + westHyrule.mido.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + eastHyrule.nabooru.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + eastHyrule.darunia.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + eastHyrule.newKasuto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + eastHyrule.oldKasuto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + + //stab shuffle + westHyrule.mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER)!.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + eastHyrule.darunia.Town!.GetTownMap(VanillaTownMap.DARUNIA_TRAINER)!.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + + //Remove locations that don't have any shufflable items + itemLocs.RemoveAll(i => i.GetShufflableCollectables().Count == 0); return itemLocs; } @@ -2336,8 +2252,8 @@ private void ShuffleSpells() } } - List unallocatedTowns = new List { Town.RAURU, Town.RUTO, Town.SARIA_NORTH, Town.MIDO_WEST, - Town.NABOORU, Town.DARUNIA_WEST, Town.NEW_KASUTO, Town.OLD_KASUTO }; + List unallocatedTowns = new List { westHyrule.rauru, westHyrule.ruto, westHyrule.sariaNorth, westHyrule.mido, + eastHyrule.nabooru, eastHyrule.darunia, eastHyrule.newKasuto, eastHyrule.oldKasuto }; var filteredToJustSpells = Enum.GetValues(typeof(Collectable)).Cast().Where(i => i.IsSpell()); foreach (Collectable spell in filteredToJustSpells) @@ -2349,17 +2265,16 @@ private void ShuffleSpells() { continue; } - Town town = unallocatedTowns[r.Next(unallocatedTowns.Count)]; - Location townLocation = GetTownLocation(town); - townLocation.Collectables = [spell]; - unallocatedTowns.Remove(town); + Location townLocation = unallocatedTowns.Sample(r)!; + townLocation.Town!.GetWizard()!.Collectable = spell; + unallocatedTowns.Remove(townLocation); } } private void SwapUpAndDownstab() { - eastHyrule.daruniaRoof.Collectables = [Collectable.DOWNSTAB]; - westHyrule.midoChurch.Collectables = [Collectable.UPSTAB]; + eastHyrule.darunia.Town!.GetTownMap(VanillaTownMap.DARUNIA_TRAINER)!.Collectable = Collectable.DOWNSTAB; + westHyrule.mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER)!.Collectable = Collectable.UPSTAB; } //Updated to use fisher-yates. Eventually i'll catch all of these. N is small enough here it REALLY makes a difference @@ -2619,13 +2534,13 @@ private void UpdateRom() palace.WriteConnections(ROMData); } - ROMData.Put(0x1CD3A, (byte)palGraphics[(int)westHyrule.locationAtPalace1.PalaceNumber!]); - ROMData.Put(0x1CD3B, (byte)palGraphics[(int)westHyrule.locationAtPalace2.PalaceNumber!]); - ROMData.Put(0x1CD3C, (byte)palGraphics[(int)westHyrule.locationAtPalace3.PalaceNumber!]); - ROMData.Put(0x1CD46, (byte)palGraphics[(int)mazeIsland.locationAtPalace4.PalaceNumber!]); - ROMData.Put(0x1CD42, (byte)palGraphics[(int)eastHyrule.locationAtPalace5.PalaceNumber!]); - ROMData.Put(0x1CD43, (byte)palGraphics[(int)eastHyrule.locationAtPalace6.PalaceNumber!]); - ROMData.Put(0x1CD44, (byte)palGraphics[(int)eastHyrule.locationAtGP.PalaceNumber!]); + ROMData.Put(0x1CD3A, (byte)palGraphics[(int)westHyrule.locationAtPalace1.Palace!.Number!]); + ROMData.Put(0x1CD3B, (byte)palGraphics[(int)westHyrule.locationAtPalace2.Palace!.Number!]); + ROMData.Put(0x1CD3C, (byte)palGraphics[(int)westHyrule.locationAtPalace3.Palace!.Number!]); + ROMData.Put(0x1CD46, (byte)palGraphics[(int)mazeIsland.locationAtPalace4.Palace!.Number!]); + ROMData.Put(0x1CD42, (byte)palGraphics[(int)eastHyrule.locationAtPalace5.Palace!.Number!]); + ROMData.Put(0x1CD43, (byte)palGraphics[(int)eastHyrule.locationAtPalace6.Palace!.Number!]); + ROMData.Put(0x1CD44, (byte)palGraphics[(int)eastHyrule.locationAtGP.Palace!.Number!]); if (props.ShuffleEnemyPalettes) { @@ -2635,98 +2550,37 @@ private void UpdateRom() RerollPaletteTable(RomMap.TOWN_PALETTE_TABLE, customizationRng); RerollPaletteTable(RomMap.PALACE_PALETTE_TABLE_MAJOR, customizationRng); RerollPaletteTable(RomMap.GP_PALETTE_TABLE_MAJOR, customizationRng); - - /* - lets no longer shuffle orange/red/blue bits for enemies, it changes - what is visible in the dark and also doesn't add enough to be worth it imo. - I also prefer if we can have the palette shuffle as a customize (non-flag) option. - - for (int i = RomMap.WEST_ENEMY_STATS_TABLE + 0x3; i < RomMap.WEST_ENEMY_STATS_TABLE + 0x23; i++) - { - if (i != RomMap.WEST_ENEMY_STATS_TABLE + 0x13) // skip elevator - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - for (int i = 0x94e8; i < 0x9508; i++) - { - if (i != 0x94f8) - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - for (int i = 0x114e8; i < 0x11508; i++) - { - if (i != 0x114f8) - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - for (int i = 0x129e8; i < 0x12a09; i++) - { - if (i != 0x129f8) - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - for (int i = 0x154e8; i < 0x15508; i++) - { - if (i != 0x154f8) - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - */ } //WRITE UPDATES TO WIZARD/QUEST COLLECTABLES HERE - ROMData.Put(RomMap.WEST_TROPHY_CAVE_COLLECTABLE, (byte)westHyrule.trophyCave.Collectables[0]); - ROMData.Put(RomMap.WEST_MAGIC_CONTAINER_CAVE_COLLECTABLE, (byte)westHyrule.magicContainerCave.Collectables[0]); - ROMData.Put(RomMap.WEST_HEART_CONTAINER_CAVE_COLLECTABLE, (byte)westHyrule.heartContainerCave.Collectables[0]); - ROMData.Put(RomMap.WEST_MEDICINE_CAVE_COLLECTABLE, (byte)westHyrule.medicineCave.Collectables[0]); - ROMData.Put(RomMap.WEST_GRASS_TILE_COLLECTABLE, (byte)westHyrule.grassTile.Collectables[0]); - ROMData.Put(RomMap.DM_SPECTACLE_ROCK_COLLECTABLE, (byte)deathMountain.specRock.Collectables[0]); - ROMData.Put(RomMap.DM_HAMMER_CAVE_COLLECTABLE, (byte)deathMountain.hammerCave.Collectables[0]); - ROMData.Put(RomMap.EAST_WATER_TILE_COLLECTABLE, (byte)eastHyrule.waterTile.Collectables[0]); - ROMData.Put(RomMap.EAST_DESERT_TILE_COLLECTABLE, (byte)eastHyrule.desertTile.Collectables[0]); + ROMData.Put(RomMap.WEST_TROPHY_CAVE_COLLECTABLE, (byte)westHyrule.trophyCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.WEST_MAGIC_CONTAINER_CAVE_COLLECTABLE, (byte)westHyrule.magicContainerCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.WEST_HEART_CONTAINER_CAVE_COLLECTABLE, (byte)westHyrule.heartContainerCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.WEST_MEDICINE_CAVE_COLLECTABLE, (byte)westHyrule.medicineCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.WEST_GRASS_TILE_COLLECTABLE, (byte)westHyrule.grassTile.GetAllCollectables()[0]); + ROMData.Put(RomMap.DM_SPECTACLE_ROCK_COLLECTABLE, (byte)deathMountain.specRock.GetAllCollectables()[0]); + ROMData.Put(RomMap.DM_HAMMER_CAVE_COLLECTABLE, (byte)deathMountain.hammerCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.EAST_WATER_TILE_COLLECTABLE, (byte)eastHyrule.waterTile.GetAllCollectables()[0]); + ROMData.Put(RomMap.EAST_DESERT_TILE_COLLECTABLE, (byte)eastHyrule.desertTile.GetAllCollectables()[0]); ROMData.ElevatorBossFix(props.BossItem); // Update item rooms and entrances for all palaces - Location[] locations = [ + Location[] palaceLocations = [ westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, westHyrule.locationAtPalace3, eastHyrule.locationAtPalace5, eastHyrule.locationAtPalace6, eastHyrule.locationAtGP, mazeIsland.locationAtPalace4, ]; - foreach (var loc in locations) + foreach (var loc in palaceLocations) { - var idx = (int)loc.PalaceNumber! - 1; - if (loc.PalaceNumber != 7) + var idx = (int)loc.Palace!.Number! - 1; + if (loc.Palace!.Number != 7) { - for(int i = 0; i < palaces[idx].ItemRooms.Count; i++) + foreach(Room itemRoom in loc.Palace.ItemRooms) { - ROMData.UpdateItem(loc.Collectables[i], palaces[idx].ItemRooms[i]); + Debug.Assert(itemRoom.Collectable != null, $"Item room {itemRoom.Name} map {itemRoom.Map} in palace {loc.Palace.Number} is missing its item"); + ROMData.UpdateItem((Collectable)itemRoom.Collectable, itemRoom); } } @@ -2738,33 +2592,33 @@ what is visible in the dark and also doesn't add enough to be worth it imo. ROMData.Put(loc.MemAddress + 0x7e, root.Map); } - ROMData.Put(RomMap.EAST_SPELL_TOWER_COLLECTABLE, (byte)eastHyrule.spellTower.Collectables[0]); //map 47 - ROMData.Put(RomMap.EAST_NEW_KASUTO_BASEMENT_COLLECTABLE, (byte)eastHyrule.newKasutoBasement.Collectables[0]); //map 46 + ROMData.Put(RomMap.EAST_SPELL_TOWER_COLLECTABLE, (byte)eastHyrule.newKasuto.Town!.GetTownMap(VanillaTownMap.SPELL_TOWER_INTERIOR)!.Collectable!); + ROMData.Put(RomMap.EAST_NEW_KASUTO_BASEMENT_COLLECTABLE, (byte)eastHyrule.newKasuto.Town!.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!.Collectable!); - ROMData.Put(RomMap.MI_MAGIC_CONTAINER_DROP_COLLECTABLE, (byte)mazeIsland.magicContainerDrop.Collectables[0]); - ROMData.Put(RomMap.MI_CHILD_DROP_COLLECTABLE, (byte)mazeIsland.childDrop.Collectables[0]); + ROMData.Put(RomMap.MI_MAGIC_CONTAINER_DROP_COLLECTABLE, (byte)mazeIsland.magicContainerDrop.GetAllCollectables()[0]); + ROMData.Put(RomMap.MI_CHILD_DROP_COLLECTABLE, (byte)mazeIsland.childDrop.GetAllCollectables()[0]); if (props.PbagItemShuffle) { - ROMData.Put(RomMap.WEST_PBAG_CAVE_COLLECTABLE, (byte)westHyrule.pbagCave.Collectables[0]); - ROMData.Put(RomMap.EAST_PBAG_CAVE1_COLLECTABLE, (byte)eastHyrule.pbagCave1.Collectables[0]); - ROMData.Put(RomMap.EAST_PBAG_CAVE2_COLLECTABLE, (byte)eastHyrule.pbagCave2.Collectables[0]); + ROMData.Put(RomMap.WEST_PBAG_CAVE_COLLECTABLE, (byte)westHyrule.pbagCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.EAST_PBAG_CAVE1_COLLECTABLE, (byte)eastHyrule.pbagCave1.GetAllCollectables()[0]); + ROMData.Put(RomMap.EAST_PBAG_CAVE2_COLLECTABLE, (byte)eastHyrule.pbagCave2.GetAllCollectables()[0]); } foreach (Location location in pbagHearts) { if (location == westHyrule.pbagCave) { - ROMData.Put(RomMap.WEST_PBAG_CAVE_COLLECTABLE, (byte)westHyrule.pbagCave.Collectables[0]); + ROMData.Put(RomMap.WEST_PBAG_CAVE_COLLECTABLE, (byte)westHyrule.pbagCave.GetAllCollectables()[0]); } if (location == eastHyrule.pbagCave1) { - ROMData.Put(RomMap.EAST_PBAG_CAVE1_COLLECTABLE, (byte)eastHyrule.pbagCave1.Collectables[0]); + ROMData.Put(RomMap.EAST_PBAG_CAVE1_COLLECTABLE, (byte)eastHyrule.pbagCave1.GetAllCollectables()[0]); } if (location == eastHyrule.pbagCave2) { - ROMData.Put(RomMap.EAST_PBAG_CAVE2_COLLECTABLE, (byte)eastHyrule.pbagCave2.Collectables[0]); + ROMData.Put(RomMap.EAST_PBAG_CAVE2_COLLECTABLE, (byte)eastHyrule.pbagCave2.GetAllCollectables()[0]); } } @@ -2809,12 +2663,12 @@ what is visible in the dark and also doesn't add enough to be worth it imo. } //Update world check for p7 - if (westHyrule.locationAtPalace1.PalaceNumber == 7 || westHyrule.locationAtPalace2.PalaceNumber == 7 || westHyrule.locationAtPalace3.PalaceNumber == 7) + if (westHyrule.locationAtPalace1.Palace!.Number == 7 || westHyrule.locationAtPalace2.Palace!.Number == 7 || westHyrule.locationAtPalace3.Palace!.Number == 7) { ROMData.Put(0x1dd3b, 0x05); } - if (mazeIsland.locationAtPalace4.PalaceNumber == 7) + if (mazeIsland.locationAtPalace4.Palace!.Number == 7) { ROMData.Put(0x1dd3b, 0x14); } @@ -2850,9 +2704,9 @@ what is visible in the dark and also doesn't add enough to be worth it imo. else { // Use the old spell menu behavior where the spells are placed in the menu in the location it came from - var wizardCollectables = AllLocationsForReal() - .Where(l => l.ActualTown != null && Towns.STRICT_SPELL_LOCATIONS.Contains((Town)l.ActualTown)) - .Select(l => l.Collectables[0]).ToList(); + var wizardCollectables = AllLocations() + .Where(l => l.Town?.GetWizard() != null) + .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); for (int i = 0; i < magFunction.Length; i++) { @@ -3070,6 +2924,9 @@ public void RandomizeSmallItems(int world, bool first) public void PrintDebugSpoiler(LogLevel logLevel) { + //I didn't want to port this and we mostly use spoiler log for this now anyway so maybe this just dies. + throw new NotImplementedException(); + /* logger.Log(logLevel, "ITEMS:"); List magicContainerLocations = []; List heartContainerLocations = []; @@ -3118,6 +2975,7 @@ public void PrintDebugSpoiler(LogLevel logLevel) sb.AppendLine(locationName); } logger.Log(logLevel, sb.ToString()); + */ } public string GenerateSpoiler() @@ -3225,7 +3083,8 @@ .define blueTile $fe """, "crop_guides.s"); } - public IEnumerable AllLocationsForReal() + //No longer AllLocationsForReal since all fakeness has been removed. + public IEnumerable AllLocations() { List? locations = westHyrule?.AllLocations .Union(eastHyrule?.AllLocations ?? []) @@ -3234,14 +3093,6 @@ public IEnumerable AllLocationsForReal() return locations ?? []; } - public IEnumerable GetNonSideviewItemLocations() - { - return AllLocationsForReal().Where( - i => i.VanillaCollectable.IsSpell() - || i.VanillaCollectable.IsQuestItem() - || i.VanillaCollectable.IsSwordTech()); - } - public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) { StringBuilder sb = new StringBuilder(); @@ -3254,15 +3105,11 @@ public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) sb.AppendLine(""); //Where(i => !i.ItemGet && !i.Collectable.IsInternalUse() - foreach (Location location in AllLocationsForReal()) + foreach (Location location in AllLocations()) { - foreach (Collectable collectable in location.Collectables) + foreach (Collectable collectable in location.GetAllCollectables().Where(i => !i.IsMinorItem() && !ItemGet[i])) { - if (!collectable.IsMinorItem() && !ItemGet[collectable]) - { - sb.AppendLine(location.Name + " / " + Enum.GetName(typeof(Collectable), collectable)); - } - //TODO: figure out how to display individual heart/magic containers + sb.AppendLine($"{location.Name} / [{String.Join(", ", location.GetAllCollectables().Select(i => Enum.GetName(i)!))}]"); } } @@ -3292,17 +3139,19 @@ private bool IsRaftAlwaysRequired(RandomizerProperties props) return false; } - private void FullItemShuffle(Assembler asm, IEnumerable nonSideviewLocations) + private void FullItemShuffle(Assembler asm) { var a = asm.Module(); foreach (var collect in Enum.GetValues()) { a.Set($"{collect.ToString().ToUpper()}_ITEMLOC", (int)collect); } + /* foreach (var loc in nonSideviewLocations) { - a.Set($"{loc.VanillaCollectable.ToString().ToUpper()}_ITEMLOC", (int)loc.Collectables[0]); + //a.Set($"{loc.VanillaCollectable.ToString().ToUpper()}_ITEMLOC", (int)loc.Collectables[0]); } + */ foreach (var val in Enum.GetValues()) { a.Set($"{val.ToString()}_DIALOG_WEST_INDEX", (int)val); @@ -3486,13 +3335,13 @@ beq @EndOfData public void SetPalacePalettes(Assembler asm) { var a = asm.Module(); - a.Assign("P1Palette", (byte)palPalettes[westHyrule?.locationAtPalace1.PalaceNumber ?? 0]); - a.Assign("P2Palette", (byte)palPalettes[westHyrule?.locationAtPalace2.PalaceNumber ?? 1]); - a.Assign("P3Palette", (byte)palPalettes[westHyrule?.locationAtPalace3.PalaceNumber ?? 2]); - a.Assign("P4Palette", (byte)palPalettes[mazeIsland?.locationAtPalace4.PalaceNumber ?? 3]); - a.Assign("P5Palette", (byte)palPalettes[eastHyrule?.locationAtPalace5.PalaceNumber ?? 4]); - a.Assign("P6Palette", (byte)palPalettes[eastHyrule?.locationAtPalace6.PalaceNumber ?? 5]); - a.Assign("PGreatPalette", (byte)palPalettes[eastHyrule?.locationAtGP.PalaceNumber ?? 6]); + a.Assign("P1Palette", (byte)palPalettes[westHyrule?.locationAtPalace1.Palace!.Number ?? 0]); + a.Assign("P2Palette", (byte)palPalettes[westHyrule?.locationAtPalace2.Palace!.Number ?? 1]); + a.Assign("P3Palette", (byte)palPalettes[westHyrule?.locationAtPalace3.Palace!.Number ?? 2]); + a.Assign("P4Palette", (byte)palPalettes[mazeIsland?.locationAtPalace4.Palace!.Number ?? 3]); + a.Assign("P5Palette", (byte)palPalettes[eastHyrule?.locationAtPalace5.Palace!.Number ?? 4]); + a.Assign("P6Palette", (byte)palPalettes[eastHyrule?.locationAtPalace6.Palace!.Number ?? 5]); + a.Assign("PGreatPalette", (byte)palPalettes[eastHyrule?.locationAtGP.Palace!.Number ?? 6]); a.Code(""" .include "z2r.inc" @@ -3680,13 +3529,13 @@ private static void UpdateTexts(Assembler asm, List hints) private void AssignRealPalaceLocations(AsmModule a) { - a.Assign("RealPalaceAtLocation1", (westHyrule?.locationAtPalace1.PalaceNumber ?? 1) - 1); - a.Assign("RealPalaceAtLocation2", (westHyrule?.locationAtPalace2.PalaceNumber ?? 2) - 1); - a.Assign("RealPalaceAtLocation3", (westHyrule?.locationAtPalace3.PalaceNumber ?? 3) - 1); - a.Assign("RealPalaceAtLocation4", (mazeIsland?.locationAtPalace4.PalaceNumber ?? 4) - 1); - a.Assign("RealPalaceAtLocation5", (eastHyrule?.locationAtPalace5.PalaceNumber ?? 5) - 1); - a.Assign("RealPalaceAtLocation6", (eastHyrule?.locationAtPalace6.PalaceNumber ?? 6) - 1); - a.Assign("RealPalaceAtLocationGP", (eastHyrule?.locationAtGP.PalaceNumber ?? 7) - 1); + a.Assign("RealPalaceAtLocation1", (westHyrule?.locationAtPalace1.Palace!.Number ?? 1) - 1); + a.Assign("RealPalaceAtLocation2", (westHyrule?.locationAtPalace2.Palace!.Number ?? 2) - 1); + a.Assign("RealPalaceAtLocation3", (westHyrule?.locationAtPalace3.Palace!.Number ?? 3) - 1); + a.Assign("RealPalaceAtLocation4", (mazeIsland?.locationAtPalace4.Palace!.Number ?? 4) - 1); + a.Assign("RealPalaceAtLocation5", (eastHyrule?.locationAtPalace5.Palace!.Number ?? 5) - 1); + a.Assign("RealPalaceAtLocation6", (eastHyrule?.locationAtPalace6.Palace!.Number ?? 6) - 1); + a.Assign("RealPalaceAtLocationGP", (eastHyrule?.locationAtGP.Palace!.Number ?? 7) - 1); } public void StatTracking(RandomizerProperties props, Assembler asm) @@ -3795,7 +3644,7 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando rom.AddRandomizerToTitle(engine); AddCropGuideBoxesToFileSelect(engine); FixHelmetheadBossRoom(engine); - FullItemShuffle(engine, GetNonSideviewItemLocations()); + FullItemShuffle(engine); rom.DontCountExpDuringTalking(engine); rom.FixElevatorPositionInFallRooms(engine); rom.AllowForChangingDoorYPosition(engine); @@ -3928,41 +3777,31 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando UpdateTexts(engine, texts); } - //This entire town location shuffle structure is awful if this method needs to exist. - private Location GetTownLocation(Town town) + private void AssignPalaceLocations() { - if(westHyrule.locationAtMido.ActualTown == town) - { - return westHyrule.locationAtMido; - } - if (westHyrule.locationAtRauru.ActualTown == town) + List palaceLocations = [westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, westHyrule.locationAtPalace3, + mazeIsland.locationAtPalace4, eastHyrule.locationAtPalace5, eastHyrule.locationAtPalace6]; + if (props.PalacesCanSwapContinent) { - return westHyrule.locationAtRauru; - } - if (westHyrule.locationAtRuto.ActualTown == town) - { - return westHyrule.locationAtRuto; - } - if (westHyrule.locationAtSariaNorth.ActualTown == town) - { - return westHyrule.locationAtSariaNorth; - } - if (eastHyrule.townAtDarunia.ActualTown == town) - { - return eastHyrule.townAtDarunia; - } - if (eastHyrule.townAtNabooru.ActualTown == town) - { - return eastHyrule.townAtNabooru; + if (props.P7shuffle) + { + palaceLocations.Add(eastHyrule.locationAtGP); + palaceLocations.FisherYatesShuffle(r); + } + else + { + palaceLocations.FisherYatesShuffle(r); + palaceLocations.Add(eastHyrule.locationAtGP); + } } - if (eastHyrule.townAtNewKasuto.ActualTown == town) + else { - return eastHyrule.townAtNewKasuto; + palaceLocations.Add(eastHyrule.locationAtGP); } - if (eastHyrule.townAtOldKasuto.ActualTown == town) + for (int i = 0; i < 7; i++) { - return eastHyrule.townAtOldKasuto; + palaceLocations[i].Palace = palaces[i]; } - throw new Exception("Unrecognized town in GetTownLocation, was this extended to non-standard wizards?"); + palaceLocations.ForEach(i => i.AccessRequirements = Requirements.DEFAULT_PALACE_REQUIREMENTS); } } diff --git a/RandomizerCore/Music.cs b/RandomizerCore/Music.cs index 7dd572ba1..7c6e2d37c 100644 --- a/RandomizerCore/Music.cs +++ b/RandomizerCore/Music.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Text; using Z2Randomizer.RandomizerCore.Overworld; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -348,7 +349,7 @@ SongMap CreateAreaSongMap(UsesSongs usesSongs) usage = Usage.Town; else if (loc.TerrainType == Terrain.TOWN) { - if (loc.ActualTown == Town.OLD_KASUTO // ?? + if (loc.Town?.Type == TownType.OLD_KASUTO // ?? /*|| loc.ActualTown == Town.SARIA_SOUTH*/) continue; @@ -356,10 +357,10 @@ SongMap CreateAreaSongMap(UsesSongs usesSongs) } else if (loc.TerrainType == Terrain.PALACE) { - if (loc.PalaceNumber == null) + if (loc.Palace?.Number == null) continue; // North palace - usage = (loc.PalaceNumber < 7) + usage = (loc.Palace.Number < 7) ? Usage.Palace : Usage.GreatPalace; } @@ -383,7 +384,7 @@ SongMap CreateAreaSongMap(UsesSongs usesSongs) Func GetSongIdx = usage switch { // One song per palace - Usage.Palace => (loc => (int)loc.PalaceNumber! - 1), + Usage.Palace => (loc => (int)loc.Palace!.Number! - 1), Usage.GreatPalace => (loc => 0), // One song per continent _ => (loc => (int)(loc.VanillaContinent ?? loc.Continent)), diff --git a/RandomizerCore/Overworld/DeathMountain.cs b/RandomizerCore/Overworld/DeathMountain.cs index c2bcbdeb2..d00f67537 100644 --- a/RandomizerCore/Overworld/DeathMountain.cs +++ b/RandomizerCore/Overworld/DeathMountain.cs @@ -193,7 +193,7 @@ .. rom.LoadLocations(LocationID.DM_SPEC_ROCK, 1, terrains), climate = Climates.Create(props.DmClimate); climate.SeedTerrainCount = Math.Min(climate.SeedTerrainCount, biome.SeedTerrainLimit()); - SetVanillaCollectables(props.ReplaceFireWithDash); + //SetVanillaCollectables(props.ReplaceFireWithDash); } private void RemoveLocationsDM(ICollection locations) @@ -1114,7 +1114,7 @@ public override void UpdateVisit(List requireables) if (location.AccessRequirements.AreSatisfiedBy(requireables)) { location.Reachable = true; - if (connectionsDM.ContainsKey(location) && location.ConnectionRequirements.AreSatisfiedBy(requireables)) + if (connectionsDM.ContainsKey(location) && (location.Town == null || location.Town.CanBeTraversed(requireables))) { foreach (Location connectedLocation in connectionsDM[location]) { @@ -1215,18 +1215,20 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool return requiredLocations.Where(i => i != null); } + /* protected override void SetVanillaCollectables(bool useDash) { hammerCave.VanillaCollectable = Collectable.HAMMER; specRock.VanillaCollectable = Collectable.MAGIC_CONTAINER; } + */ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("DEATH MOUNTAIN: "); - sb.AppendLine("\tHammer Cave: " + hammerCave.Collectables[0].EnglishText()); - sb.AppendLine("\tSpec Rock: " + specRock.Collectables[0].EnglishText()); + sb.AppendLine("\tHammer Cave: " + hammerCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tSpec Rock: " + specRock.GetAllCollectables()[0].EnglishText()); sb.AppendLine(); return sb.ToString(); @@ -1234,17 +1236,25 @@ public override string GenerateSpoiler() public override void DisableDisallowedPassthroughs() { - foreach (Location location in Locations[Terrain.CAVE]) - { - location.IsPassthrough = false; - } - foreach (Location location in Locations[Terrain.TOWN]) - { - location.IsPassthrough = false; - } - foreach (Location location in Locations[Terrain.PALACE]) - { - location.IsPassthrough = false; - } + foreach (Location location in Locations[Terrain.CAVE]) + { + location.IsPassthrough = false; + } + foreach (Location location in Locations[Terrain.TOWN]) + { + location.IsPassthrough = false; + } + foreach (Location location in Locations[Terrain.PALACE]) + { + location.IsPassthrough = false; + } + } + + public override void ResetCollectables(RandomizerProperties props) + { + hammerCave.SetCollectables([Collectable.HAMMER]); + hammerCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + specRock.SetCollectables([Collectable.MAGIC_CONTAINER]); + specRock.CollectablesAreShufflable = props.ShuffleOverworldItems; } } diff --git a/RandomizerCore/Overworld/EastHyrule.cs b/RandomizerCore/Overworld/EastHyrule.cs index 5a8b46a66..a0c74b13d 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -5,6 +5,7 @@ using System.Text; using NLog; using Z2Randomizer.RandomizerCore.Enemy; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore.Overworld; @@ -12,7 +13,6 @@ namespace Z2Randomizer.RandomizerCore.Overworld; //6A35 - address in memory of palace 6 y coord public sealed class EastHyrule : World { - //private int bridgeCount; int debug = 0; private static readonly new Logger logger = LogManager.GetCurrentClassLogger(); @@ -65,16 +65,12 @@ public sealed class EastHyrule : World public Location locationAtPalace5; public Location locationAtPalace6; - public Location fountain; public Location waterTile; public Location desertTile; - public Location townAtDarunia; - public Location daruniaRoof; - public Location townAtNewKasuto; - public Location spellTower; - public Location newKasutoBasement; - public Location townAtNabooru; - public Location townAtOldKasuto; + public Location darunia; + public Location newKasuto; + public Location nabooru; + public Location oldKasuto; public Location locationAtGP; public Location pbagCave1; public Location pbagCave2; @@ -140,43 +136,28 @@ .. rom.LoadLocations(LocationID.EAST_TOWN_OLD_KASUTO, 4, terrains), //Palaces locationAtPalace5 = GetLocation(LocationID.EAST_PALACE5); - locationAtPalace5.PalaceNumber = 5; - locationAtPalace5.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; - locationAtPalace6 = GetLocation(LocationID.EAST_PALACE6); - locationAtPalace6.PalaceNumber = 6; - locationAtPalace6.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; - locationAtGP = GetLocation(LocationID.EAST_GREAT_PALACE); - locationAtGP.PalaceNumber = 7; - locationAtGP.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; // the same as this gets shuffled with regular palaces - locationAtGP.VanillaCollectable = Collectable.DO_NOT_USE; - locationAtGP.Collectables = []; //Towns - townAtNabooru = GetLocation(LocationID.EAST_TOWN_NABOORU); - townAtNabooru.VanillaCollectable = props.ReplaceFireWithDash ? Collectable.DASH_SPELL : Collectable.FIRE_SPELL; - townAtNabooru.Collectables = props.ReplaceFireWithDash ? [Collectable.DASH_SPELL] : [Collectable.FIRE_SPELL]; - townAtNabooru.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.WATER]) - : new Requirements([], [[RequirementType.WATER, RequirementType.FIVE_CONTAINERS]]); - - townAtDarunia = GetLocation(LocationID.EAST_TOWN_DARUNIA); - townAtDarunia.VanillaCollectable = Collectable.REFLECT_SPELL; - townAtDarunia.Collectables = [Collectable.REFLECT_SPELL]; - townAtDarunia.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.CHILD]) - : new Requirements([], [[RequirementType.CHILD, RequirementType.SIX_CONTAINERS]]); - - townAtNewKasuto = GetLocation(LocationID.EAST_TOWN_NEW_KASUTO); - townAtNewKasuto.VanillaCollectable = Collectable.SPELL_SPELL; - townAtNewKasuto.Collectables = [Collectable.SPELL_SPELL]; - townAtNewKasuto.CollectableRequirements = props.DisableMagicRecs ? Requirements.NONE : new Requirements([RequirementType.SEVEN_CONTAINERS]); - - townAtOldKasuto = GetLocation(LocationID.EAST_TOWN_OLD_KASUTO); - townAtOldKasuto.VanillaCollectable = Collectable.THUNDER_SPELL; - townAtOldKasuto.Collectables = [Collectable.THUNDER_SPELL]; - townAtOldKasuto.CollectableRequirements = props.DisableMagicRecs ? Requirements.NONE : new Requirements([RequirementType.EIGHT_CONTAINERS]); + nabooru = GetLocation(LocationID.EAST_TOWN_NABOORU); + nabooru.Town = Towns.LoadVanillaTown(rom, TownType.NABOORU, props.DisableMagicRecs); + + darunia = GetLocation(LocationID.EAST_TOWN_DARUNIA); + darunia.Town = Towns.LoadVanillaTown(rom, TownType.DARUNIA, props.DisableMagicRecs); + + newKasuto = GetLocation(LocationID.EAST_TOWN_NEW_KASUTO); + newKasuto.Town = Towns.LoadVanillaTown(rom, TownType.NEW_KASUTO, props.DisableMagicRecs); + newKasuto.Town.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!.AccessRequirements = props.NewKasutoBasementRequirement switch + { + 5 => new Requirements([RequirementType.FIVE_CONTAINERS]), + 6 => new Requirements([RequirementType.SIX_CONTAINERS]), + 7 => new Requirements([RequirementType.SEVEN_CONTAINERS]), + _ => throw new Exception($"Unsupported New Kasuto basement container count: {props.NewKasutoBasementRequirement}") + }; + + oldKasuto = GetLocation(LocationID.EAST_TOWN_OLD_KASUTO); + oldKasuto.Town = Towns.LoadVanillaTown(rom, TownType.OLD_KASUTO, props.DisableMagicRecs); waterTile = GetLocation(LocationID.EAST_WATER); waterTile.AccessRequirements = new Requirements([RequirementType.BOOTS]); @@ -228,52 +209,6 @@ .. rom.LoadLocations(LocationID.EAST_TOWN_OLD_KASUTO, 4, terrains), pbagCave2 = GetLocation(LocationID.EAST_CAVE_PBAG2); VANILLA_MAP_ADDR = 0x9056; - //Fake locations that dont correspond to anywhere on the map, but still hold logic and items - spellTower = new Location(townAtNewKasuto); - spellTower.VanillaCollectable = Collectable.MAGIC_KEY; - spellTower.Collectables = [Collectable.MAGIC_KEY]; - spellTower.CollectableRequirements = new Requirements([RequirementType.SPELL]); - spellTower.Name = "Spell Tower"; - spellTower.CanShuffle = false; - spellTower.ActualTown = null; - townAtNewKasuto.Children.Add(spellTower); - AddLocation(spellTower); - - newKasutoBasement = new Location(townAtNewKasuto); - newKasutoBasement.VanillaCollectable = Collectable.MAGIC_CONTAINER; - newKasutoBasement.Collectables = [Collectable.MAGIC_CONTAINER]; - newKasutoBasement.CollectableRequirements = props.NewKasutoBasementRequirement switch - { - 5 => new Requirements([RequirementType.FIVE_CONTAINERS]), - 6 => new Requirements([RequirementType.SIX_CONTAINERS]), - 7 => new Requirements([RequirementType.SEVEN_CONTAINERS]), - _ => throw new Exception($"Unsupported New Kasuto basement container count: {props.NewKasutoBasementRequirement}") - }; - newKasutoBasement.Name = "Granny's basement"; - newKasutoBasement.CanShuffle = false; - newKasutoBasement.ActualTown = null; - townAtNewKasuto.Children.Add(newKasutoBasement); - AddLocation(newKasutoBasement); - - fountain = new Location(townAtNabooru); - fountain.VanillaCollectable = Collectable.WATER; - fountain.Collectables = [Collectable.WATER]; - fountain.Name = "Water Fountain"; - fountain.ActualTown = Town.NABOORU_FOUNTAIN; - fountain.CanShuffle = false; - townAtNabooru.Children.Add(fountain); - AddLocation(fountain); - - daruniaRoof = new Location(townAtDarunia); - daruniaRoof.VanillaCollectable = Collectable.UPSTAB; - daruniaRoof.Collectables = [Collectable.UPSTAB]; - daruniaRoof.CollectableRequirements = new Requirements([RequirementType.FAIRY, RequirementType.JUMP]); - daruniaRoof.Name = "Darunia Roof"; - daruniaRoof.ActualTown = Town.DARUNIA_ROOF; - daruniaRoof.CanShuffle = false; - townAtDarunia.Children.Add(daruniaRoof); - AddLocation(daruniaRoof); - walkableTerrains = [Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, Terrain.SWAMP, Terrain.GRAVE]; randomTerrainFilter = [Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, Terrain.SWAMP, Terrain.GRAVE, Terrain.MOUNTAIN, Terrain.WALKABLEWATER]; @@ -357,17 +292,15 @@ .. rom.LoadLocations(LocationID.EAST_TOWN_OLD_KASUTO, 4, terrains), { (0x66, 0x2D), "south" }, { (0x49, 0x04), "gp" } }; - townAtNewKasuto.IsExternalWorld = true; + newKasuto.IsExternalWorld = true; locationAtPalace6.IsExternalWorld = true; hiddenPalaceLocation = locationAtPalace6; - hiddenKasutoLocation = townAtNewKasuto; + hiddenKasutoLocation = newKasuto; //Climate filtering climate = Climates.Create(props.EastClimate); climate.SeedTerrainCount = Math.Min(climate.SeedTerrainCount, biome.SeedTerrainLimit()); climate.DisallowTerrain(props.CanWalkOnWaterWithBoots ? Terrain.WATER : Terrain.WALKABLEWATER); - //climate.DisallowTerrain(Terrain.LAVA); - SetVanillaCollectables(props.ReplaceFireWithDash); } public override bool Terraform(RandomizerProperties props, ROM rom) @@ -435,7 +368,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) if (!props.ShuffleHidden) { - townAtNewKasuto.CanShuffle = false; + newKasuto.CanShuffle = false; locationAtPalace6.CanShuffle = false; } ShuffleLocations(AllLocations); @@ -806,7 +739,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) if(props.RiverDevilBlockerOption == RiverDevilBlockerOption.SIEGE) { //Iterate the towns in a random order - Location[] towns = [townAtNabooru, townAtDarunia, townAtNewKasuto, townAtOldKasuto]; + Location[] towns = [nabooru, darunia, newKasuto, oldKasuto]; RNG.Shuffle(towns); bool placed = false; foreach (Location location in towns) @@ -863,17 +796,13 @@ public override bool Terraform(RandomizerProperties props, ROM rom) if (props.HiddenPalace) { - rom.UpdateHiddenPalaceSpot(biome, hiddenPalaceCoords, hiddenPalaceLocation, - townAtNewKasuto, spellTower, !props.LegacyVanillaShuffledLocations); - hiddenPalaceLocation.AccessRequirements = hiddenPalaceLocation.AccessRequirements.WithHardRequirement(RequirementType.FLUTE); - hiddenPalaceLocation.Children.ForEach(i => i.AccessRequirements = i.AccessRequirements.WithHardRequirement(RequirementType.FLUTE)); - } + rom.UpdateHiddenPalaceSpot(biome, hiddenPalaceCoords, hiddenPalaceLocation, !props.LegacyVanillaShuffledLocations); + hiddenPalaceLocation.AccessRequirements = hiddenPalaceLocation.AccessRequirements.WithHardRequirement(RequirementType.FLUTE); } if (props.HiddenKasuto) { - rom.UpdateKasuto(hiddenKasutoLocation, townAtNewKasuto, spellTower, biome, + rom.UpdateKasuto(hiddenKasutoLocation, biome, baseAddr, terrains[hiddenKasutoLocation.ID], !props.LegacyVanillaShuffledLocations); hiddenKasutoLocation.AccessRequirements = hiddenPalaceLocation.AccessRequirements.WithHardRequirement(RequirementType.HAMMER); - hiddenKasutoLocation.Children.ForEach(i => i.AccessRequirements = i.AccessRequirements.WithHardRequirement(RequirementType.HAMMER)); } WriteMapToRom(rom, true, MAP_ADDR, MAP_SIZE_BYTES, hiddenPalaceLocation.Y, hiddenPalaceLocation.Xpos, props.HiddenPalace, props.HiddenKasuto); @@ -1558,7 +1487,7 @@ private void RandomizeHiddenKasuto(bool shuffleHidden) } else { - hiddenKasutoLocation = townAtNewKasuto; + hiddenKasutoLocation = newKasuto; } hiddenKasutoLocation.TerrainType = Terrain.FOREST; hiddenKasutoLocation.AccessRequirements = hiddenKasutoLocation.AccessRequirements.WithHardRequirement(RequirementType.HAMMER); @@ -1662,10 +1591,6 @@ private bool RandomizeHiddenPalace(ROM rom, bool shuffleHidden, bool hiddenKasut if (!AllReached) { base.UpdateAllReached(); - if (!hiddenPalaceLocation.Reachable || !hiddenKasutoLocation.Reachable || !spellTower.Reachable) - { - AllReached = false; - } } } @@ -1680,7 +1605,7 @@ public override void UpdateVisit(List requireables) if(location.AccessRequirements.AreSatisfiedBy(requireables)) { location.Reachable = true; - if (connections.ContainsKey(location) && location.ConnectionRequirements.AreSatisfiedBy(requireables)) + if (connections.ContainsKey(location) && (location.Town == null || location.Town.CanBeTraversed(requireables))) { Location connectedLocation = connections[location]; connectedLocation.Reachable = true; @@ -1947,8 +1872,6 @@ protected override List GetPathingStarts() public bool ValidateBasicRouting() { List unreachedLocations = RequiredLocations(false, false).ToList(); - //SpellTower's connection logic isn't implemented here, nor do we care since we're assuming you have everything. - unreachedLocations.Remove(spellTower); bool[,] visitedCoordinates = new bool[MapRows, MapColumns]; List<(int, int)> pendingCoordinates = new(); @@ -2000,15 +1923,11 @@ public bool ValidateBasicRouting() } } while (pendingCoordinates.Count > 0); - return !unreachedLocations.Any(); + return unreachedLocations.Count == 0; } protected override void OnUpdateReachableTrigger() { - foreach(Location parentLocation in AllLocations.Where(i => i.Children != null && i.ActualTown != null)) - { - parentLocation.Children.ForEach(child => child.Reachable = parentLocation.Reachable); - } } public override string GetName() @@ -2024,11 +1943,10 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool locationAtPalace6, waterTile, desertTile, - townAtDarunia, - townAtNewKasuto, - spellTower, - townAtNabooru, - townAtOldKasuto, + darunia, + newKasuto, + nabooru, + oldKasuto, locationAtGP, pbagCave1, pbagCave2, @@ -2059,52 +1977,50 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool return requiredLocations.Where(i => i != null); } + /* protected override void SetVanillaCollectables(bool useDash) { locationAtPalace5.VanillaCollectable = Collectable.FLUTE; locationAtPalace6.VanillaCollectable = Collectable.CROSS; - townAtNabooru.VanillaCollectable = useDash ? Collectable.DASH_SPELL : Collectable.FIRE_SPELL; + nabooru.VanillaCollectable = useDash ? Collectable.DASH_SPELL : Collectable.FIRE_SPELL; fountain.VanillaCollectable = Collectable.WATER; - townAtDarunia.VanillaCollectable = Collectable.REFLECT_SPELL; + darunia.VanillaCollectable = Collectable.REFLECT_SPELL; daruniaRoof.VanillaCollectable = Collectable.UPSTAB; newKasutoBasement.VanillaCollectable = Collectable.MAGIC_CONTAINER; - townAtNewKasuto.VanillaCollectable = Collectable.SPELL_SPELL; + newKasuto.VanillaCollectable = Collectable.SPELL_SPELL; spellTower.VanillaCollectable = Collectable.MAGIC_KEY; - townAtOldKasuto.VanillaCollectable = Collectable.THUNDER_SPELL; + oldKasuto.VanillaCollectable = Collectable.THUNDER_SPELL; waterTile.VanillaCollectable = Collectable.HEART_CONTAINER; desertTile.VanillaCollectable = Collectable.HEART_CONTAINER; pbagCave1.VanillaCollectable = Collectable.XL_BAG; pbagCave2.VanillaCollectable = Collectable.XL_BAG; } + */ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("EAST: "); - sb.AppendLine("\tNabooru: " + AllLocations.First(i => i.ActualTown == Town.NABOORU).Collectables[0].EnglishText()); - sb.AppendLine("\tFountain: " + fountain.Collectables[0].EnglishText()); - sb.AppendLine("\tDarunia: " + AllLocations.First(i => i.ActualTown == Town.DARUNIA_WEST).Collectables[0].EnglishText()); - sb.AppendLine("\tUpstab Guy: " + daruniaRoof.Collectables[0].EnglishText()); - sb.AppendLine("\tNew Kasuto: " + AllLocations.First(i => i.ActualTown == Town.NEW_KASUTO).Collectables[0].EnglishText()); - sb.AppendLine("\tGranny's Basement: " + newKasutoBasement.Collectables[0].EnglishText()); - sb.AppendLine("\tSpell Tower: " + spellTower.Collectables[0].EnglishText()); - sb.AppendLine("\tOld Kasuto: " + AllLocations.First(i => i.ActualTown == Town.OLD_KASUTO).Collectables[0].EnglishText()); + sb.AppendLine("Nabooru: \n" + nabooru.Town!.GenerateSpoiler()); + sb.AppendLine("Darunia: \n" + nabooru.Town!.GenerateSpoiler()); + sb.AppendLine("New Kasuto: \n" + nabooru.Town!.GenerateSpoiler()); + sb.AppendLine("Old Kasuto: \n" + nabooru.Town!.GenerateSpoiler()); - sb.AppendLine("\tRisen Pbag Cave: " + pbagCave2.Collectables[0].EnglishText()); - sb.AppendLine("\tSunken Pbag Cave: " + pbagCave1.Collectables[0].EnglishText()); - sb.AppendLine("\tWater Tile: " + waterTile.Collectables[0].EnglishText()); - sb.AppendLine("\tDesert tile: " + desertTile.Collectables[0].EnglishText()); + sb.AppendLine("\tRisen Pbag Cave: " + pbagCave2.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tSunken Pbag Cave: " + pbagCave1.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tWater Tile: " + waterTile.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tDesert tile: " + desertTile.GetAllCollectables()[0].EnglishText()); - sb.Append("\tPalace 5 (" + locationAtPalace5.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace5.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace5.Collectables.Select(c => c.EnglishText()))); + sb.Append("\tPalace 5 (" + locationAtPalace5.Palace!.Number + "): "); + sb.AppendLine(locationAtPalace5.GetAllCollectables().Count == 0 ? "No Items" : string.Join(", ", locationAtPalace5.GetAllCollectables().Select(c => c.EnglishText()))); - sb.Append("\tPalace 6 (" + locationAtPalace6.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace6.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace6.Collectables.Select(c => c.EnglishText()))); + sb.Append("\tPalace 6 (" + locationAtPalace6.Palace!.Number + "): "); + sb.AppendLine(locationAtPalace6.GetAllCollectables().Count == 0 ? "No Items" : string.Join(", ", locationAtPalace6.GetAllCollectables().Select(c => c.EnglishText()))); - sb.Append("\tPalace 7 (" + locationAtGP.PalaceNumber + "): "); - sb.AppendLine(locationAtGP.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtGP.Collectables.Select(c => c.EnglishText()))); + sb.Append("\tPalace 7 (" + locationAtGP.Palace!.Number + "): "); + sb.AppendLine(locationAtGP.GetAllCollectables().Count == 0 ? "No Items" : string.Join(", ", locationAtGP.GetAllCollectables().Select(c => c.EnglishText()))); sb.AppendLine(); return sb.ToString(); @@ -2161,4 +2077,41 @@ public override void DisableDisallowedPassthroughs() swamp.AccessRequirements = swamp.AccessRequirements.Without(RequirementType.FAIRY); } } + + public override void ResetCollectables(RandomizerProperties props) + { + nabooru.Town!.GetWizard()!.Collectable = props.ReplaceFireWithDash ? Collectable.DASH_SPELL : Collectable.FAIRY_SPELL; + nabooru.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + nabooru.Town!.GetTownMap(VanillaTownMap.NABOORU_MID)!.Collectable = Collectable.WATER; + nabooru.Town!.GetTownMap(VanillaTownMap.NABOORU_MID)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + + darunia.Town!.GetWizard()!.Collectable = Collectable.REFLECT_SPELL; + darunia.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + darunia.Town!.GetTownMap(VanillaTownMap.DARUNIA_TRAINER)!.Collectable = Collectable.UPSTAB; + darunia.Town!.GetTownMap(VanillaTownMap.DARUNIA_TRAINER)!.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + + newKasuto.Town!.GetWizard()!.Collectable = Collectable.SPELL_SPELL; + newKasuto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + newKasuto.Town!.GetTownMap(VanillaTownMap.SPELL_TOWER_INTERIOR)!.Collectable = Collectable.MAGIC_KEY; + newKasuto.Town!.GetTownMap(VanillaTownMap.SPELL_TOWER_INTERIOR)!.CollectableIsShufflable = props.ShuffleOverworldItems; + newKasuto.Town!.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!.Collectable = Collectable.MAGIC_CONTAINER; + newKasuto.Town!.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!.CollectableIsShufflable = props.ShuffleOverworldItems; + + oldKasuto.Town!.GetWizard()!.Collectable = Collectable.THUNDER_SPELL; + oldKasuto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + + desertTile.SetCollectables([Collectable.HEART_CONTAINER]); + desertTile.CollectablesAreShufflable = props.ShuffleOverworldItems; + waterTile.SetCollectables([Collectable.HEART_CONTAINER]); + waterTile.CollectablesAreShufflable = props.ShuffleOverworldItems; + pbagCave1.SetCollectables([Collectable.XL_BAG]); + pbagCave1.CollectablesAreShufflable = props.ShuffleOverworldItems && props.PbagItemShuffle; + pbagCave2.SetCollectables([Collectable.XL_BAG]); + pbagCave2.CollectablesAreShufflable = props.ShuffleOverworldItems && props.PbagItemShuffle; + + + locationAtPalace5.SetCollectables(locationAtPalace5.Palace!.GetVanillaCollectables()); + locationAtPalace6.SetCollectables(locationAtPalace6.Palace!.GetVanillaCollectables()); + locationAtGP.SetCollectables(locationAtGP.Palace!.GetVanillaCollectables()); + } } \ No newline at end of file diff --git a/RandomizerCore/Overworld/Location.cs b/RandomizerCore/Overworld/Location.cs index 23536853e..142bd9b74 100644 --- a/RandomizerCore/Overworld/Location.cs +++ b/RandomizerCore/Overworld/Location.cs @@ -3,6 +3,9 @@ using System.Diagnostics; using System.Collections.Generic; using System.Linq; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; +using DynamicData; namespace Z2Randomizer.RandomizerCore.Overworld; @@ -13,13 +16,14 @@ public class Location public LocationID ID { get; set; } - public List Collectables { get; set; } - public Collectable VanillaCollectable { get; set; } + private List NonPalaceTownCollectables { get; set; } + public bool CollectablesAreShufflable { get; set; } public bool AppearsOnMap { get; set; } public bool Reachable { get; set; } - public int? PalaceNumber { get; set; } - public Town? ActualTown { get; set; } + public Town? Town { get; set; } + public Palace? Palace { get; set; } + //public TownEnum? ActualTown { get; set; } public Continent Continent { get; set; } public Continent? VanillaContinent { get; set; } public Continent? ConnectedContinent { get; set; } @@ -27,7 +31,6 @@ public class Location public Terrain TerrainType { get; set; } //public byte[] LocationBytes { get; set; } - public List Children { get; set; } = []; public int MemAddress { get; set; } @@ -81,11 +84,6 @@ public int Xpos //List of requirements to access this location. Required before the other requirements are evaluated, //and to pass through the location on the map, even if the collectable is not gettable. public Requirements AccessRequirements; - //Requirements to get the thing(s) here. For palaces, this does not (yet) consider the layout of the palace or the rooms in it - //only the general "need fairy or key" requirements for palaces generally - public Requirements CollectableRequirements; - //If this location is a connector, these are the requirements for the other side of the connection to be reached - public Requirements ConnectionRequirements; //This does 2 things and should only do 1. It both tracks whether the location is a location that should be possible to shuffle, //and tracks whether this location has actually been shuffled already. This persistence doesn't always get reset properly, @@ -151,20 +149,21 @@ public Location(LocationID lid, int yPos, int xPos, int map, Continent continent MemAddress = lid.GetRomOffset(); EntranceNumber = 0; CanShuffle = true; - Collectables = []; + NonPalaceTownCollectables = []; Reachable = false; - PalaceNumber = null; + Palace = null; Continent = continent; VanillaContinent = lid.GetContinent(); ConnectedContinent = null; AccessRequirements = Requirements.NONE; - CollectableRequirements = Requirements.NONE; - ConnectionRequirements = Requirements.NONE; IsPassthrough = isPassthrough; WasPassthrough = isPassthrough; Name = ID.GetName() ?? "Unknown (" + Continent.GetName(Continent) + ")"; - ActualTown = ID.GetTown(); + if (Town != null) + { + Town.Type = ID.GetTown(); + } if (Name.StartsWith("Unknown") && Xpos != 0 && YRaw != 0) { @@ -212,7 +211,7 @@ public string GetDebuggerDisplay() + " " + Name + " (" + (Y) + "," + (Xpos) + ") _" + (Reachable ? "Reachable " : "Unreachable ") - + '[' + string.Join(", ", Collectables.Select(i => i.ToString())) + ']'; + + '[' + string.Join(", ", GetAllCollectables().Select(i => i.ToString())) + ']'; } public void ResetCoords() @@ -223,7 +222,7 @@ public void ResetCoords() public int GetWorld() { //Towns reference their banks - if (ActualTown != null) + if (Town?.Type != null) { return Continent == Continent.WEST ? 4 : 10; } @@ -246,12 +245,10 @@ public int GetWorld() } //Palaces... have world numbers that kind... of... make sense? //This is what they are. - if (PalaceNumber != null) + if (Palace != null) { - return PalaceNumber switch + return Palace.Number switch { - //North palace - null => 0, 1 => 0b00001100 + (int)Continent, //12, 2 => 0b00001100 + (int)Continent, //12, 3 => 0b00010000 + (int)Continent, //16, @@ -277,4 +274,75 @@ public void Clear() EntranceNumber = 0; CanShuffle = false; } + + public List GetAllCollectables() + { + return GetGettableItems(RequirementTypeExtensions.ALL); + } + + public List GetShufflableCollectables() + { + return GetGettableItems(RequirementTypeExtensions.ALL, ForceEnterRight ? Direction.EAST : Direction.WEST, true); + } + + public List GetGettableItems(List requireables) + { + return GetGettableItems(requireables, ForceEnterRight ? Direction.EAST : Direction.WEST); + } + + public List GetGettableItems(List requireables, Direction entranceDirection, bool shufflableItemsOnly = false) + { + if (Town != null) + { + return Town.GetGettableItems(entranceDirection, requireables, shufflableItemsOnly); + } + if (Palace != null) + { + return Palace.GetGettableItems(requireables, shufflableItemsOnly); + } + if(shufflableItemsOnly && !CollectablesAreShufflable) + { + return []; + } + return AccessRequirements.AreSatisfiedBy(requireables) ? NonPalaceTownCollectables : []; + } + + public void SetCollectables(IEnumerable collectables) + { + Debug.Assert(Town == null || Palace == null); + if (Town != null) + { + Town.SetCollectables(collectables); + } + else if (Palace != null) + { + Palace.SetCollectables(collectables); + } + else NonPalaceTownCollectables = [.. collectables]; + } + + /// + /// Replaces the first instance of a given collectable at a location with the indicated collectable. + /// Intended to be used to replace unique items, and "first" is currently undefined in order so be careful (or fix this) + /// + /// + /// + public bool ReplaceCollectable(Collectable toReplace, Collectable replacement) + { + Debug.Assert(Town == null || Palace == null); + if (Town != null) + { + return Town.ReplaceCollectable(toReplace, replacement); + } + else if (Palace != null) + { + return Palace.ReplaceCollectable(toReplace, replacement); + } + else if (NonPalaceTownCollectables.Contains(toReplace)) + { + NonPalaceTownCollectables.Replace(toReplace, replacement); + return true; + } + return false; + } } diff --git a/RandomizerCore/Overworld/LocationID.cs b/RandomizerCore/Overworld/LocationID.cs index 2216f094a..7636772ea 100644 --- a/RandomizerCore/Overworld/LocationID.cs +++ b/RandomizerCore/Overworld/LocationID.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore.Overworld; @@ -276,18 +277,18 @@ public static IEnumerable EnumerateNamed(LocationID startLid, int li public static class LocationIDExtensions { - static readonly Dictionary townMap = new() + static readonly Dictionary townMap = new() { - [LocationID.WEST_TOWN_RAURO] = Town.RAURU, - [LocationID.WEST_TOWN_RUTO] = Town.RUTO, - [LocationID.WEST_TOWN_SARIA_NORTH] = Town.SARIA_NORTH, - [LocationID.WEST_TOWN_SARIA_SOUTH] = Town.SARIA_SOUTH, - [LocationID.WEST_BAGU_HOUSE] = Town.BAGU, - [LocationID.WEST_TOWN_MIDO] = Town.MIDO_WEST, - [LocationID.EAST_TOWN_NABOORU] = Town.NABOORU, - [LocationID.EAST_TOWN_DARUNIA] = Town.DARUNIA_WEST, - [LocationID.EAST_TOWN_OLD_KASUTO] = Town.OLD_KASUTO, - [LocationID.EAST_TOWN_NEW_KASUTO] = Town.NEW_KASUTO, + [LocationID.WEST_TOWN_RAURO] = TownType.RAURU, + [LocationID.WEST_TOWN_RUTO] = TownType.RUTO, + [LocationID.WEST_TOWN_SARIA_NORTH] = TownType.SARIA, + [LocationID.WEST_TOWN_SARIA_SOUTH] = TownType.SARIA, + [LocationID.WEST_BAGU_HOUSE] = TownType.BAGU, + [LocationID.WEST_TOWN_MIDO] = TownType.MIDO, + [LocationID.EAST_TOWN_NABOORU] = TownType.NABOORU, + [LocationID.EAST_TOWN_DARUNIA] = TownType.DARUNIA, + [LocationID.EAST_TOWN_OLD_KASUTO] = TownType.OLD_KASUTO, + [LocationID.EAST_TOWN_NEW_KASUTO] = TownType.NEW_KASUTO, }; public static bool IsNamed(this LocationID lid) @@ -338,9 +339,9 @@ public static int GetRomOffset(this LocationID lid, int byteIdx = 0) /// /// /// - public static Town? GetTown(this LocationID lid) + public static TownType? GetTown(this LocationID lid) { - if (townMap.TryGetValue(lid, out Town town)) + if (townMap.TryGetValue(lid, out TownType town)) return town; return null; diff --git a/RandomizerCore/Overworld/MazeIsland.cs b/RandomizerCore/Overworld/MazeIsland.cs index a55458a1d..8f9934706 100644 --- a/RandomizerCore/Overworld/MazeIsland.cs +++ b/RandomizerCore/Overworld/MazeIsland.cs @@ -75,8 +75,6 @@ .. rom.LoadLocations(LocationID.MI_CHILD_DROP, 1, terrains), childDrop = GetLocation(LocationID.MI_CHILD_DROP); magicContainerDrop = GetLocation(LocationID.MI_MAGIC_CONTAINER_DROP); locationAtPalace4 = GetLocation(LocationID.MI_PALACE4); - locationAtPalace4.PalaceNumber = 4; - locationAtPalace4.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; continentId = Continent.MAZE; baseAddr = RomMap.ContinentLocationBases[continentId]; @@ -111,7 +109,7 @@ .. rom.LoadLocations(LocationID.MI_CHILD_DROP, 1, terrains), trapLocations.Remove(removeLoc); } } - SetVanillaCollectables(props.ReplaceFireWithDash); + //SetVanillaCollectables(props.ReplaceFireWithDash); } public override bool Terraform(RandomizerProperties props, ROM rom) @@ -961,6 +959,7 @@ public override void UpdateVisit(List requireables) } } + //DebugVisitation(); foreach (Location location in AllLocations) { if (visitation[location.Y, location.Xpos]) @@ -1004,22 +1003,25 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool return requiredLocations.Where(i => i != null); } + /* protected override void SetVanillaCollectables(bool useDash) { locationAtPalace4.VanillaCollectable = Collectable.BOOTS; childDrop.VanillaCollectable = Collectable.CHILD; magicContainerDrop.VanillaCollectable = Collectable.MAGIC_CONTAINER; } + */ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("MAZE ISLAND: "); - sb.AppendLine("\tMagic Container Drop: " + magicContainerDrop.Collectables[0].EnglishText()); - sb.AppendLine("\tChild Drop: " + childDrop.Collectables[0].EnglishText()); + sb.AppendLine("\tMagic Container Drop: " + magicContainerDrop.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tChild Drop: " + childDrop.GetAllCollectables()[0].EnglishText()); - sb.Append("\tPalace 4 (" + locationAtPalace4.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace4.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace4.Collectables.Select(c => c.EnglishText()))); + sb.Append("\tPalace 4 (" + locationAtPalace4.Palace!.Number + "): "); + List palaceCollectables = locationAtPalace4.GetAllCollectables(); + sb.AppendLine(palaceCollectables.Count == 0 ? "No Items" : string.Join(", ", palaceCollectables.Select(c => c.EnglishText()))); sb.AppendLine(); return sb.ToString(); @@ -1046,4 +1048,14 @@ public override void DisableDisallowedPassthroughs() magicContainerDrop.IsPassthrough = false; childDrop.IsPassthrough = false; } + + public override void ResetCollectables(RandomizerProperties props) + { + childDrop.SetCollectables([Collectable.CHILD]); + childDrop.CollectablesAreShufflable = props.ShuffleOverworldItems; + magicContainerDrop.SetCollectables([Collectable.MAGIC_CONTAINER]); + magicContainerDrop.CollectablesAreShufflable = props.ShuffleOverworldItems; + + locationAtPalace4.SetCollectables(locationAtPalace4.Palace!.GetVanillaCollectables()); + } } diff --git a/RandomizerCore/Overworld/WestHyrule.cs b/RandomizerCore/Overworld/WestHyrule.cs index 2d206da32..56c5380d3 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -5,6 +5,7 @@ using System.Text; using NLog; using Z2Randomizer.RandomizerCore.Enemy; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore.Overworld; @@ -13,11 +14,12 @@ public sealed class WestHyrule : World private readonly new Logger logger = LogManager.GetCurrentClassLogger(); public Location northPalace; - public Location locationAtMido; + public Location rauru; + public Location ruto; + public Location sariaNorth; + public Location sariaSouth; + public Location mido; public Location bagu; - public Location mirrorTable; - public Location midoChurch; - public Location locationAtRuto; public Location medicineCave; public Location trophyCave; public Location locationAtPalace1; @@ -26,9 +28,6 @@ public sealed class WestHyrule : World public Location magicContainerCave; public Location grassTile; public Location heartContainerCave; - public Location locationAtSariaNorth; - public Location locationAtSariaSouth; - public Location locationAtRauru; private Location bridge1; private Location bridge2; @@ -121,9 +120,6 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), northPalace = GetLocation(LocationID.WEST_NORTH_PALACE); //0x462f medicineCave = GetLocation(LocationID.WEST_CAVE_MEDICINE); //0x463e - bagu = GetLocation(LocationID.WEST_BAGU_HOUSE); //0x4661 - bagu.ActualTown = Town.BAGU; - bagu.Collectables = [Collectable.BAGUS_NOTE]; trophyCave = GetLocation(LocationID.WEST_CAVE_TROPHY); //0x00004630 magicContainerCave = GetLocation(LocationID.WEST_CAVE_MAGIC_CONTAINER); grassTile = GetLocation(LocationID.WEST_GRASS); @@ -131,46 +127,33 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), pbagCave = GetLocation(LocationID.WEST_CAVE_PBAG); //Towns - locationAtRauru = GetLocation(LocationID.WEST_TOWN_RAURO); - locationAtRauru.Collectables = [Collectable.SHIELD_SPELL]; - locationAtRauru.CollectableRequirements = props.DisableMagicRecs ? Requirements.NONE : new Requirements([RequirementType.ONE_CONTAINER]); - - locationAtRuto = GetLocation(LocationID.WEST_TOWN_RUTO); //0x465e - locationAtRuto.Collectables = [Collectable.JUMP_SPELL]; - locationAtRuto.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.TROPHY]) - : new Requirements([], [[RequirementType.TROPHY, RequirementType.TWO_CONTAINERS]]); - - locationAtSariaNorth = GetLocation(LocationID.WEST_TOWN_SARIA_NORTH); //0x00004660 - locationAtSariaNorth.Collectables = [Collectable.LIFE_SPELL]; - locationAtSariaNorth.ConnectionRequirements = new Requirements([RequirementType.FAIRY, RequirementType.BAGU_LETTER], [[RequirementType.JUMP, RequirementType.DASH]]); - locationAtSariaNorth.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.MIRROR]) - : new Requirements([], [[RequirementType.MIRROR, RequirementType.THREE_CONTAINERS]]); - - locationAtSariaSouth = GetLocation(LocationID.WEST_TOWN_SARIA_SOUTH); //0x0000465f - locationAtSariaSouth.ConnectionRequirements = locationAtSariaNorth.ConnectionRequirements; - - locationAtMido = GetLocation(LocationID.WEST_TOWN_MIDO); //0x00004662 - locationAtMido.Collectables = [Collectable.FAIRY_SPELL]; - locationAtMido.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.MEDICINE]) - : new Requirements([], [[RequirementType.MEDICINE, RequirementType.FOUR_CONTAINERS]]); + bagu = GetLocation(LocationID.WEST_BAGU_HOUSE); //0x4661 + bagu.Town = Towns.LoadVanillaTown(rom, TownType.BAGU, props.DisableMagicRecs); + + rauru = GetLocation(LocationID.WEST_TOWN_RAURO); + rauru.Town = Towns.LoadVanillaTown(rom, TownType.RAURU, props.DisableMagicRecs); + + ruto = GetLocation(LocationID.WEST_TOWN_RUTO); //0x465e + ruto.Town = Towns.LoadVanillaTown(rom, TownType.RUTO, props.DisableMagicRecs); + + sariaNorth = GetLocation(LocationID.WEST_TOWN_SARIA_NORTH); //0x00004660 + Town sariaTown = Towns.LoadVanillaTown(rom, TownType.SARIA, props.DisableMagicRecs); + sariaNorth.Town = sariaTown; + + sariaSouth = GetLocation(LocationID.WEST_TOWN_SARIA_SOUTH); //0x0000465f + sariaSouth.Town = sariaTown; + + mido = GetLocation(LocationID.WEST_TOWN_MIDO); //0x00004662 + mido.Town = Towns.LoadVanillaTown(rom, TownType.MIDO, props.DisableMagicRecs); //Palaces locationAtPalace1 = GetLocation(LocationID.WEST_PALACE1); - locationAtPalace1.PalaceNumber = 1; - locationAtPalace1.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; locationAtPalace2 = GetLocation(LocationID.WEST_PALACE2); - locationAtPalace2.PalaceNumber = 2; - locationAtPalace2.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; locationAtPalace3 = GetLocation(LocationID.WEST_PALACE3); - locationAtPalace3.PalaceNumber = 3; - locationAtPalace3.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; //Connectors fairyCave = GetLocation(LocationID.WEST_FAIRY_CAVE_DROP); //0x4640 - fairyCave.ConnectionRequirements = new Requirements([RequirementType.FAIRY]); + fairyCave.AccessRequirements = new Requirements([RequirementType.FAIRY]); jumpCave = GetLocation(LocationID.WEST_CAVE_JUMP_NORTH); //0x463b jumpCave.AccessRequirements = new Requirements([RequirementType.JUMP, RequirementType.FAIRY]); @@ -184,24 +167,6 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), bridge1 = GetLocation(LocationID.WEST_BRIDGE_AFTER_DM_WEST); //0x4644 bridge2 = GetLocation(LocationID.WEST_BRIDGE_AFTER_DM_EAST); //0x4645 - //Fake locations that dont correspond to anywhere on the map, but still hold logic and items - mirrorTable = new Location(locationAtSariaNorth); - mirrorTable.Collectables = [Collectable.MIRROR]; - mirrorTable.ActualTown = Town.SARIA_TABLE; - mirrorTable.Name = "Saria Mirror Table"; - mirrorTable.CanShuffle = false; - locationAtSariaNorth.Children.Add(mirrorTable); - AddLocation(mirrorTable); - - midoChurch = new Location(locationAtMido); - midoChurch.Collectables = [Collectable.DOWNSTAB]; - midoChurch.CollectableRequirements = new Requirements([RequirementType.JUMP, RequirementType.FAIRY]); - midoChurch.ActualTown = Town.MIDO_CHURCH; - midoChurch.Name = "Mido Church"; - midoChurch.CanShuffle = false; - locationAtMido.Children.Add(midoChurch); - AddLocation(midoChurch); - caveConnections = []; // (left,right) caveConnections.Add((parapaCave1, parapaCave2)); caveConnections.Add((jumpCave, jumpCave2)); @@ -360,7 +325,7 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), GetLocation(LocationID.WEST_BAGU_WOODS5), GetLocation(LocationID.WEST_BAGU_WOODS1) ]; - SetVanillaCollectables(props.ReplaceFireWithDash); + //SetVanillaCollectables(props.ReplaceFireWithDash); } public override bool Terraform(RandomizerProperties props, ROM rom) @@ -430,8 +395,8 @@ public override bool Terraform(RandomizerProperties props, ROM rom) { AllLocations.ForEach(i => i.CanShuffle = true); Terrain riverTerrain = Terrain.MOUNTAIN; - locationAtSariaSouth.CanShuffle = false; - locationAtSariaNorth.CanShuffle = false; + sariaSouth.CanShuffle = false; + sariaNorth.CanShuffle = false; map = new Terrain[MapRows, MapColumns]; @@ -501,8 +466,8 @@ public override bool Terraform(RandomizerProperties props, ROM rom) rows--; } } - locationAtSariaSouth.CanShuffle = false; - locationAtSariaNorth.CanShuffle = false; + sariaSouth.CanShuffle = false; + sariaNorth.CanShuffle = false; walkableTerrains = [Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, Terrain.SWAMP, Terrain.GRAVE]; //Islands specifically seeds preplaced water instead of normally expandable water to make wetlands less insane randomTerrainFilter = new List { Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, @@ -591,8 +556,8 @@ public override bool Terraform(RandomizerProperties props, ROM rom) rows--; } } - locationAtSariaSouth.CanShuffle = false; - locationAtSariaNorth.CanShuffle = false; + sariaSouth.CanShuffle = false; + sariaNorth.CanShuffle = false; break; default: walkableTerrains = [Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, Terrain.SWAMP, Terrain.GRAVE]; @@ -1397,7 +1362,7 @@ public override void UpdateVisit(List requireables) if (location.AccessRequirements.AreSatisfiedBy(requireables)) { location.Reachable = true; - if (connections.ContainsKey(location) && location.ConnectionRequirements.AreSatisfiedBy(requireables)) + if (connections.ContainsKey(location) && (location.Town == null || location.Town.CanBeTraversed(requireables))) { Location connectedLocation = connections[location]; connectedLocation.Reachable = true; @@ -1488,11 +1453,11 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool HashSet requiredLocations = [ northPalace, - locationAtRauru, - locationAtMido, - locationAtRuto, - locationAtSariaNorth, - locationAtSariaSouth, + rauru, + mido, + ruto, + sariaNorth, + sariaSouth, bagu, medicineCave, @@ -1517,16 +1482,17 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool return requiredLocations.Where(i => i != null); } + /* protected override void SetVanillaCollectables(bool useFire) { locationAtPalace1.VanillaCollectable = Collectable.CANDLE; locationAtPalace2.VanillaCollectable = Collectable.GLOVE; locationAtPalace3.VanillaCollectable = Collectable.RAFT; - locationAtRauru.VanillaCollectable = Collectable.SHIELD_SPELL; - locationAtRuto.VanillaCollectable = Collectable.JUMP_SPELL; - locationAtSariaNorth.VanillaCollectable = Collectable.LIFE_SPELL; - locationAtMido.VanillaCollectable = Collectable.FAIRY_SPELL; + rauru.VanillaCollectable = Collectable.SHIELD_SPELL; + ruto.VanillaCollectable = Collectable.JUMP_SPELL; + sariaNorth.VanillaCollectable = Collectable.LIFE_SPELL; + mido.VanillaCollectable = Collectable.FAIRY_SPELL; midoChurch.VanillaCollectable = Collectable.DOWNSTAB; bagu.VanillaCollectable = Collectable.BAGUS_NOTE; mirrorTable.VanillaCollectable = Collectable.MIRROR; @@ -1538,34 +1504,36 @@ protected override void SetVanillaCollectables(bool useFire) trophyCave.VanillaCollectable = Collectable.TROPHY; medicineCave.VanillaCollectable = Collectable.MEDICINE; } + */ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("WEST: "); - sb.AppendLine("\tRauru: " + AllLocations.First(i => i.ActualTown == Town.RAURU).Collectables[0].EnglishText()); - sb.AppendLine("\tRuto: " + AllLocations.First(i => i.ActualTown == Town.RUTO).Collectables[0].EnglishText()); - sb.AppendLine("\tMirror Table: " + mirrorTable.Collectables[0].EnglishText()); - sb.AppendLine("\tSaria: " + AllLocations.First(i => i.ActualTown == Town.SARIA_NORTH).Collectables[0].EnglishText()); - sb.AppendLine("\tDownstab Guy: " + midoChurch.Collectables[0].EnglishText()); - sb.AppendLine("\tMido: " + AllLocations.First(i => i.ActualTown == Town.MIDO_WEST).Collectables[0].EnglishText()); - sb.AppendLine("\tBagu: " + bagu.Collectables[0].EnglishText()); - - sb.AppendLine("\tMagic Container Cave: " + magicContainerCave.Collectables[0].EnglishText()); - sb.AppendLine("\tTrophy Cave: " + trophyCave.Collectables[0].EnglishText()); - sb.AppendLine("\tGrass Tile: " + grassTile.Collectables[0].EnglishText()); - sb.AppendLine("\tHeart Container Cave: " + heartContainerCave.Collectables[0].EnglishText()); - sb.AppendLine("\tPillar Pbag Cave: " + pbagCave.Collectables[0].EnglishText()); - sb.AppendLine("\tMedicine Cave: " + medicineCave.Collectables[0].EnglishText()); - - sb.Append("\tPalace 1 (" + locationAtPalace1.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace1.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace1.Collectables.Select(c => c.EnglishText()))); - - sb.Append("\tPalace 2 (" + locationAtPalace2.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace2.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace2.Collectables.Select(c => c.EnglishText()))); - - sb.Append("\tPalace 3 (" + locationAtPalace3.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace3.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace3.Collectables.Select(c => c.EnglishText()))); + sb.AppendLine("Rauru: " + rauru.Town!.GenerateSpoiler()); + sb.AppendLine("Ruto: " + ruto.Town!.GenerateSpoiler()); + sb.AppendLine("Saria: " + sariaNorth.Town!.GenerateSpoiler()); + sb.AppendLine("Mido: " + mido.Town!.GenerateSpoiler()); + sb.AppendLine("Bagu: " + bagu.Town!.GenerateSpoiler()); + + sb.AppendLine("\tMagic Container Cave: " + magicContainerCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tTrophy Cave: " + trophyCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tGrass Tile: " + grassTile.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tHeart Container Cave: " + heartContainerCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tPillar Pbag Cave: " + pbagCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tMedicine Cave: " + medicineCave.GetAllCollectables()[0].EnglishText()); + + List palaceCollectables = locationAtPalace1.GetAllCollectables(); + sb.Append("\tPalace 1 (" + locationAtPalace1.Palace!.Number + "): "); + sb.AppendLine(palaceCollectables.Count == 0 ? "No Items" : string.Join(", ", palaceCollectables.Select(c => c.EnglishText()))); + + palaceCollectables = locationAtPalace2.GetAllCollectables(); + sb.Append("\tPalace 2 (" + locationAtPalace2.Palace!.Number + "): "); + sb.AppendLine(palaceCollectables.Count == 0 ? "No Items" : string.Join(", ", palaceCollectables.Select(c => c.EnglishText()))); + + palaceCollectables = locationAtPalace3.GetAllCollectables(); + sb.Append("\tPalace 3 (" + locationAtPalace3.Palace!.Number + "): "); + sb.AppendLine(palaceCollectables.Count == 0 ? "No Items" : string.Join(", ", palaceCollectables.Select(c => c.EnglishText()))); sb.AppendLine(); return sb.ToString(); @@ -1573,10 +1541,6 @@ public override string GenerateSpoiler() protected override void OnUpdateReachableTrigger() { - if (AllLocations.Where(i => i.ActualTown == Town.SARIA_NORTH).FirstOrDefault()?.Reachable ?? false) - { - mirrorTable.Reachable = true; - } } public override void DisableDisallowedPassthroughs() @@ -1602,4 +1566,43 @@ public override void DisableDisallowedPassthroughs() fairyCave.IsPassthrough = false; //fairy cave } + + public override void ResetCollectables(RandomizerProperties props) + { + rauru.Town!.GetWizard()!.Collectable = Collectable.SHIELD_SPELL; + rauru.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + + ruto.Town!.GetWizard()!.Collectable = Collectable.JUMP_SPELL; + ruto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + + sariaNorth.Town!.GetWizard()!.Collectable = Collectable.LIFE_SPELL; + sariaNorth.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + sariaNorth.Town!.GetTownMap(VanillaTownMap.SARIA_TABLE)!.Collectable = Collectable.MIRROR; + sariaNorth.Town!.GetTownMap(VanillaTownMap.SARIA_TABLE)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + + mido.Town!.GetWizard()!.Collectable = Collectable.FAIRY_SPELL; + mido.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER)!.Collectable = Collectable.DOWNSTAB; + mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER)!.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + + bagu.Town!.GetTownMap(VanillaTownMap.BAGU_INDOORS)!.Collectable = Collectable.BAGUS_NOTE; + bagu.Town!.GetTownMap(VanillaTownMap.BAGU_INDOORS)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + + trophyCave.SetCollectables([Collectable.TROPHY]); + trophyCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + medicineCave.SetCollectables([Collectable.MEDICINE]); + medicineCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + heartContainerCave.SetCollectables([Collectable.HEART_CONTAINER]); + heartContainerCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + magicContainerCave.SetCollectables([Collectable.MAGIC_CONTAINER]); + magicContainerCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + pbagCave.SetCollectables([Collectable.LARGE_BAG]); + pbagCave.CollectablesAreShufflable = props.ShuffleOverworldItems && props.PbagItemShuffle; + grassTile.SetCollectables([Collectable.HEART_CONTAINER]); + grassTile.CollectablesAreShufflable = props.ShuffleOverworldItems; + + locationAtPalace1.SetCollectables(locationAtPalace1.Palace!.GetVanillaCollectables()); + locationAtPalace2.SetCollectables(locationAtPalace2.Palace!.GetVanillaCollectables()); + locationAtPalace3.SetCollectables(locationAtPalace3.Palace!.GetVanillaCollectables()); + } } diff --git a/RandomizerCore/Overworld/World.cs b/RandomizerCore/Overworld/World.cs index 853d35cc4..d9ef05d07 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -7,6 +7,7 @@ using System.Text; using NLog; using Z2Randomizer.RandomizerCore.Enemy; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; //using System.Runtime.InteropServices.WindowsRuntime; @@ -56,8 +57,6 @@ public abstract class World private const int MINIMUM_BRIDGE_LENGTH = 2; - public static readonly Requirements DEFAULT_PALACE_REQUIREMENTS = new Requirements([RequirementType.FAIRY, RequirementType.KEY]); - private static readonly Dictionary MAXIMUM_BRIDGE_LENGTH = new() { { Biome.ISLANDS, 10 }, @@ -195,20 +194,6 @@ protected void Swap(Location l1, Location l2) (l2.Xpos, l1.Xpos) = (l1.Xpos, l2.Xpos); (l2.Y, l1.Y) = (l1.Y, l2.Y); (l2.IsPassthrough, l1.IsPassthrough) = (l1.IsPassthrough, l2.IsPassthrough); - - foreach (Location child in l1.Children) - { - child.Xpos = l1.Xpos; - child.Y = l1.Y; - child.IsPassthrough = l1.IsPassthrough; - } - - foreach (Location child in l2.Children) - { - child.Xpos = l2.Xpos; - child.Y = l2.Y; - child.IsPassthrough = l2.IsPassthrough; - } } protected void RemoveLocations(ICollection locationsToRemove) @@ -307,7 +292,7 @@ public void UpdateAllReached() //TODO: Doing this off terrain type is a very bad idea for stuff like vanilla shuffle no actual terrain. if (location.TerrainType == Terrain.PALACE || location.TerrainType == Terrain.TOWN - || location.Collectables.Any(i => !i.IsInternalUse())) + || location.GetAllCollectables().Any(i => !i.IsInternalUse())) { if (!location.Reachable) { @@ -440,8 +425,7 @@ protected bool PlaceLocations(Terrain riverTerrain, bool saneCaves, Location? hi location.Y = y; location.CanShuffle = false; } - else if (location.TerrainType != Terrain.TOWN || - (location.ActualTown > 0 && (location?.ActualTown?.AppearsOnMap() ?? false))) + else if (location.Town == null) { Terrain t; do @@ -461,15 +445,6 @@ protected bool PlaceLocations(Terrain riverTerrain, bool saneCaves, Location? hi location.CanShuffle = false; } } - - if(location!.TerrainType == Terrain.TOWN) - { - foreach (Location linkedLocation in AllLocations.Where( - loc => !loc.AppearsOnMap && loc.ActualTown?.GetMasterTown() == location.ActualTown)) - { - linkedLocation.Pos = location.Pos; - } - } } return true; } @@ -758,9 +733,7 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr //Any of the bridge locations that are being placed need to be reset so their vanilla locations aren't avoided. if(placeSaria) { - foreach (Location location in AllLocations.Where(i => i.ActualTown == Town.SARIA_NORTH - || i.ActualTown == Town.SARIA_SOUTH - || i.ActualTown == Town.SARIA_TABLE)) + foreach (Location location in AllLocations.Where(i => i.Town?.Type == TownType.SARIA)) { location.Xpos = 0; location.Y = 0; @@ -1960,6 +1933,8 @@ public void ResetVisitabilityState() } } + public abstract void ResetCollectables(RandomizerProperties props); + protected bool DrawBridge(Direction direction) { return DrawBridge(RNG, map, bridge, walkableTerrains, direction); @@ -2287,6 +2262,19 @@ public void VisitCave2() } } + public void DebugVisitation() + { + StringBuilder sb = new(); + for(int y = 0; y < MapRows; y++) + { + for(int x = 0; x < MapColumns; x++) + { + sb.Append(visitation[y, x] ? 'x' : ' '); + } + sb.AppendLine(); + } + Debug.WriteLine(sb.ToString()); + } public List GetContinentConnections() { return new List() { cave1!, cave2!, bridge!, raft! }.Where(i => i != null).ToList(); @@ -3183,42 +3171,11 @@ public bool ValidateCaves() return true; } - /* - public bool PassthroughsIntersectRaftCoordinates(IEnumerable<(int, int)> raftCoordinates) - { - foreach (Location location in AllLocations.Where(i => i.PassThrough != 0)) - { - if (raftCoordinates.Any(i => location.Xpos == i.Item2 && location.YRaw == i.Item1)) - { - return true; - } - } - return false; - } - */ - - //Short term fix, right now linked locations aren't shuffled, and they are counted as reachable when - //their parent location is reachable, but the original location remains in the location list, creating - //a phantom reachability spot at the vanilla location. - //What _should_ happen is there should be a unified interface for updating locations that automicatically - //updates the coordinates, map, linked locations, etc. But that's more work than I want to do, so here's this lazy hack - public void SynchronizeLinkedLocations() - { - foreach (Location parent in AllLocations) - { - foreach (Location child in parent.Children) - { - child.Xpos = parent.Xpos; - child.Y = parent.Y; - } - } - } - public abstract void UpdateVisit(List requireables); public abstract IEnumerable RequiredLocations(bool hiddenPalace, bool hiddenKasuto); - protected abstract void SetVanillaCollectables(bool useDash); + //protected abstract void SetVanillaCollectables(bool useDash); public abstract string GenerateSpoiler(); } \ No newline at end of file diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 6ef10d972..480e79587 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -1,15 +1,18 @@ -using System; +using js65; +using NLog; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; -using js65; -using NLog; +using System.Xml.Linq; using Z2Randomizer.RandomizerCore.Enemy; using Z2Randomizer.RandomizerCore.Overworld; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -156,6 +159,8 @@ public class ROM public static readonly int[] ZeldaFacePaletteAddr = { 0x4023, 0x8023, 0x14047, 0x140c9 }; public static readonly int[] ZeldaDressPaletteAddr = { 0x4024, 0x8024, 0x14048, 0x140c8 }; + public static readonly byte[] DOOR_CONNECTION_BLACKLIST = [0x00, 0xFC, 0xFD, 0xFE, 0xFF]; + public byte[] rawdata { get; } public ROM(string filename, bool expandRom = false) @@ -2356,6 +2361,62 @@ public Location LoadLocation(LocationID lid, Terrain terrain, bool? passthroughO }; } + public Town LoadTown(int startMapNum, int numMaps) + { + List townMaps = []; + Dictionary connections = []; + Dictionary doorConnections = []; + for (int currentMapNum = startMapNum; currentMapNum - startMapNum < numMaps; currentMapNum++) + { + townMaps.Add(LoadTownMap(currentMapNum, false)); + + var roomDoorConnectionsRomAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x8817 + currentMapNum * 4); + var roomDoorConnections = GetBytes(roomDoorConnectionsRomAddr, 4); + + for(int connectionNumber = 0; connectionNumber < 4; connectionNumber++) + { + byte internalMapNumber = (byte)((roomDoorConnections[connectionNumber] & 0xFC) >> 2); + if(!DOOR_CONNECTION_BLACKLIST.Contains(internalMapNumber) && !townMaps.Any(i => i.Map == internalMapNumber)) + { + townMaps.Add(LoadTownMap(internalMapNumber, true)); + } + } + } + + foreach(TownMap townMap in townMaps) + { + + var romAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x871B + townMap.Map * 4); + var connectionsRaw = GetBytes(romAddr, 4); + connections.Add(townMap.Map, connectionsRaw); + romAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x8817 + townMap.Map * 4); + var doorConnectionsRaw = GetBytes(romAddr, 4); + doorConnections.Add(townMap.Map, doorConnectionsRaw); + } + + + return new Town(townMaps, connections, doorConnections); + } + + public TownMap LoadTownMap(int currentMapNum, bool isInternalMap) + { + var romPtr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x8523 + 2 * currentMapNum); + var nesAddr = GetShort(romPtr + 1, romPtr); + var romAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, nesAddr); + var length = GetByte(romAddr); + var sideviewRaw = GetBytes(romAddr, length); + + romPtr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x85a1 + 2 * currentMapNum); + //Enemy pointer points to the ram location base enemy data is copied from + //18A0 is the offset to reach the base enemy data + nesAddr = GetShort(romPtr + 1, romPtr) + 0x18A0; + romAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, nesAddr); + length = GetByte(romAddr); + byte[] enemiesRaw = GetBytes(romAddr, length); + + return new TownMap(currentMapNum, sideviewRaw, enemiesRaw, isInternalMap); + } + public void RemoveUnusedConnectors(World world) { if (world.raft == null) @@ -2380,8 +2441,7 @@ public void RemoveUnusedConnectors(World world) } //This was refactored out of EastHyrule. The signature/timing/structure needs work. - public void UpdateHiddenPalaceSpot(Biome biome, (int, int) hiddenPalaceCoords, Location hiddenPalaceLocation, - Location townAtNewKasuto, Location spellTower, bool vanillaShuffleUsesActualTerrain) + public void UpdateHiddenPalaceSpot(Biome biome, (int, int) hiddenPalaceCoords, Location hiddenPalaceLocation, bool vanillaShuffleUsesActualTerrain) { if (!biome.UsesVanillaMap()) { @@ -2395,10 +2455,6 @@ public void UpdateHiddenPalaceSpot(Biome biome, (int, int) hiddenPalaceCoords, L Put(0x1ccc0, (byte)pos); int connection = hiddenPalaceLocation.MemAddress - 0x862F; Put(0x1df76, (byte)connection); - if (hiddenPalaceLocation == spellTower) - { - throw new Exception("Child locations shouldn't be able to be hidden spots"); - } if (vanillaShuffleUsesActualTerrain || biome != Biome.VANILLA_SHUFFLE) { Put(0x1df74, (byte)hiddenPalaceLocation.TerrainType); @@ -2507,7 +2563,7 @@ public void UpdateHiddenPalaceSpot(Biome biome, (int, int) hiddenPalaceCoords, L } - public void UpdateKasuto(Location hiddenKasutoLocation, Location townAtNewKasuto, Location spellTower, Biome biome, + public void UpdateKasuto(Location hiddenKasutoLocation, Biome biome, int baseAddr, Terrain hiddenKasutoTerrain, bool vanillaShuffleUsesActualTerrain) { Put(0x1df79, (byte)(hiddenKasutoLocation.YRaw + (hiddenKasutoLocation.IsExternalWorld ? 0x80 : 0))); @@ -2517,10 +2573,6 @@ public void UpdateKasuto(Location hiddenKasutoLocation, Location townAtNewKasuto Put(0x1ccdb, (byte)hiddenKasutoLocation.YRaw); int connection = hiddenKasutoLocation.MemAddress - baseAddr; Put(0x1df77, (byte)connection); - if (hiddenKasutoLocation == spellTower) - { - throw new Exception("Child locations shouldn't be able to be hidden spots"); - } if (vanillaShuffleUsesActualTerrain || biome != Biome.VANILLA_SHUFFLE) { //Terrain t = terrains[hiddenKasutoLocation.MemAddress]; diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index 9853cc6f5..63860832d 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -9,7 +9,7 @@ using System.Runtime.CompilerServices; using NLog; using Z2Randomizer.RandomizerCore.Flags; -using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore; diff --git a/RandomizerCore/RandomizerProperties.cs b/RandomizerCore/RandomizerProperties.cs index 271d0d85d..a1fb19f02 100644 --- a/RandomizerCore/RandomizerProperties.cs +++ b/RandomizerCore/RandomizerProperties.cs @@ -244,7 +244,7 @@ public class RandomizerProperties [Key] public int Id { get; set; } - public bool StartsWithCollectable(Collectable collectable) + public bool StartsWithCollectable(Collectable? collectable) { return collectable switch { diff --git a/RandomizerCore/RequirementType.cs b/RandomizerCore/RequirementType.cs index 1a515eb2d..4b9109a63 100644 --- a/RandomizerCore/RequirementType.cs +++ b/RandomizerCore/RequirementType.cs @@ -1,4 +1,5 @@  +using System; using System.Collections.Generic; namespace Z2Randomizer.RandomizerCore; @@ -35,6 +36,7 @@ public enum RequirementType public static class RequirementTypeExtensions { + public static readonly List ALL = [..Enum.GetValues()]; /// Mirror of Collectable.AsRequirement() public static Collectable? AsCollectable(this RequirementType requirementType) { diff --git a/RandomizerCore/Requirements.cs b/RandomizerCore/Requirements.cs index 54534f6d1..7a2a1e65e 100644 --- a/RandomizerCore/Requirements.cs +++ b/RandomizerCore/Requirements.cs @@ -6,12 +6,15 @@ using System.Text.Json; using System.Text.Json.Serialization; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore; public class Requirements { public static readonly Requirements NONE = new(); + public static readonly Requirements DEFAULT_PALACE_REQUIREMENTS = new([RequirementType.FAIRY, RequirementType.KEY]); + private static readonly Dictionary ImplicitRequirements = new() { diff --git a/RandomizerCore/RomMap.cs b/RandomizerCore/RomMap.cs index c653641fe..79f6e831f 100644 --- a/RandomizerCore/RomMap.cs +++ b/RandomizerCore/RomMap.cs @@ -134,4 +134,5 @@ class RomMap public const int PALACE_PALETTE_TABLE_ENTRANCES = 0x10480; public const int PALACE_PALETTE_TABLE_PER_PALACE = 0x13f10; public const int GP_PALETTE_TABLE_MAJOR = 0x1401e; + } diff --git a/RandomizerCore/Shuffler.cs b/RandomizerCore/Shuffler.cs index 7f70b4d3a..e545e9871 100644 --- a/RandomizerCore/Shuffler.cs +++ b/RandomizerCore/Shuffler.cs @@ -3,7 +3,7 @@ using System.Linq; using js65; using NLog; -using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore; diff --git a/RandomizerCore/Sidescroll/ByEntranceDirectionItemRoomSelectionStrategy.cs b/RandomizerCore/Sidescroll/Palace/ByEntranceDirectionItemRoomSelectionStrategy.cs similarity index 96% rename from RandomizerCore/Sidescroll/ByEntranceDirectionItemRoomSelectionStrategy.cs rename to RandomizerCore/Sidescroll/Palace/ByEntranceDirectionItemRoomSelectionStrategy.cs index 2378a7e6c..6fbf29b9e 100644 --- a/RandomizerCore/Sidescroll/ByEntranceDirectionItemRoomSelectionStrategy.cs +++ b/RandomizerCore/Sidescroll/Palace/ByEntranceDirectionItemRoomSelectionStrategy.cs @@ -4,7 +4,7 @@ using System.Linq; using Z2Randomizer.RandomizerCore; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; internal class ByEntranceDirectionItemRoomSelectionStrategy : ItemRoomSelectionStrategy { diff --git a/RandomizerCore/Sidescroll/ByShapeItemRoomSelectionStrategy.cs b/RandomizerCore/Sidescroll/Palace/ByShapeItemRoomSelectionStrategy.cs similarity index 99% rename from RandomizerCore/Sidescroll/ByShapeItemRoomSelectionStrategy.cs rename to RandomizerCore/Sidescroll/Palace/ByShapeItemRoomSelectionStrategy.cs index e762d58f5..72daa27e2 100644 --- a/RandomizerCore/Sidescroll/ByShapeItemRoomSelectionStrategy.cs +++ b/RandomizerCore/Sidescroll/Palace/ByShapeItemRoomSelectionStrategy.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; internal class ByShapeItemRoomSelectionStrategy : ItemRoomSelectionStrategy, IItemRoomInShapeSelectionStrategy { diff --git a/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/ChaosPalaceGenerator.cs similarity index 98% rename from RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/ChaosPalaceGenerator.cs index 57c6545ce..429f87804 100644 --- a/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/ChaosPalaceGenerator.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; internal class ChaosPalaceGenerator : PalaceGenerator { protected static readonly Logger logger = LogManager.GetCurrentClassLogger(); @@ -18,7 +18,7 @@ internal override async Task GeneratePalace(RandomizerProperties props, debug++; bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); RoomPool roomPool = new(rooms); - Palace palace = new(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); var palaceGroup = Util.AsPalaceGrouping(palaceNumber); palace.Entrance = new(roomPool.Entrances[r.Next(roomPool.Entrances.Count)]) diff --git a/RandomizerCore/Sidescroll/CoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/CoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/CoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/CoordinatePalaceGenerator.cs index 0d615b8b8..7a4e675c4 100644 --- a/RandomizerCore/Sidescroll/CoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/CoordinatePalaceGenerator.cs @@ -4,7 +4,7 @@ using System.Linq; using NLog; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public abstract class CoordinatePalaceGenerator : PalaceGenerator { diff --git a/RandomizerCore/Sidescroll/ItemRoomSelectionStrategy.cs b/RandomizerCore/Sidescroll/Palace/ItemRoomSelectionStrategy.cs similarity index 90% rename from RandomizerCore/Sidescroll/ItemRoomSelectionStrategy.cs rename to RandomizerCore/Sidescroll/Palace/ItemRoomSelectionStrategy.cs index e9cb8eeda..d54a452a9 100644 --- a/RandomizerCore/Sidescroll/ItemRoomSelectionStrategy.cs +++ b/RandomizerCore/Sidescroll/Palace/ItemRoomSelectionStrategy.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public abstract class ItemRoomSelectionStrategy { diff --git a/RandomizerCore/Sidescroll/Palace.cs b/RandomizerCore/Sidescroll/Palace/Palace.cs similarity index 96% rename from RandomizerCore/Sidescroll/Palace.cs rename to RandomizerCore/Sidescroll/Palace/Palace.cs index 26647f65b..82859220f 100644 --- a/RandomizerCore/Sidescroll/Palace.cs +++ b/RandomizerCore/Sidescroll/Palace/Palace.cs @@ -1,24 +1,25 @@ -using System; +using NLog; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using NLog; using Z2Randomizer.RandomizerCore.Enemy; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; using static Z2Randomizer.RandomizerCore.Util; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; -public partial class Palace +public partial class Palace(int number, bool palaceItemsAreShufflable) { private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private const double DROPS_CAN_LEAD_TO_BOSS_CHANCE = 0.4; //private const bool DROPS_ARE_BLOCKERS = false; private const byte OUTSIDE_ROOM_EXIT = 0b11111100; - private readonly SortedDictionary> rooms; + private readonly SortedDictionary> rooms = []; internal bool IsValid { get; set; } = false; public static readonly ReadOnlyCollection SHUFFLABLE_SMALL_ITEMS = [ @@ -32,12 +33,13 @@ public partial class Palace Collectable.ONEUP ]; - public List AllRooms { get; private set; } + public List AllRooms { get; private set; } = []; - public Room? Entrance { get; set; } - public List ItemRooms { get; set; } + public Room? Entrance { get; set; } = null; + public List ItemRooms { get; set; } = []; + public bool PalaceItemsAreShufflable { get; set; } = palaceItemsAreShufflable; public Room? BossRoom { get; set; } - public int Number { get; set; } + public int Number { get; set; } = number; internal Room? TbirdRoom { get; set; } //DEBUG @@ -45,22 +47,6 @@ public partial class Palace public PalaceGrouping PalaceGroup => Util.AsPalaceGrouping(Number) ?? throw new Exception("Palace number out of range"); - public Palace(int number) - { - Number = number; - Entrance = null; - rooms = []; - AllRooms = []; - ItemRooms = []; - } - - /* - public void UpdateRomItem(Collectable collectable, ROM ROMData) - { - ItemRoom?.UpdateRomItem(collectable, ROMData); - } - */ - public bool RequiresThunderbird() { CheckSpecialPaths(Entrance!); @@ -961,40 +947,6 @@ public void RandomizeSmallItems(Random r, bool extraKeys) } } } - - /* - public List CheckBlocks() - { - return CheckBlocksHelper([], [], Entrance!); - } - - private List CheckBlocksHelper(List c, List blockers, Room r) - { - if (c.Contains(ItemRoom!)) - { - return c; - } - c.Add(r); - if (r.Up != null && !c.Contains(r.Up)) - { - CheckBlocksHelper(c, blockers, r.Up); - } - if (r.Down != null && !c.Contains(r.Down)) - { - CheckBlocksHelper(c, blockers, r.Down); - } - if (r.Left != null && !c.Contains(r.Left)) - { - CheckBlocksHelper(c, blockers, r.Left); - } - if (r.Right != null && !c.Contains(r.Right)) - { - CheckBlocksHelper(c, blockers, r.Right); - } - return c; - } - */ - public void ResetRooms() { foreach (Room r in AllRooms) @@ -1175,8 +1127,12 @@ public bool CanReachAnItemRoom(IEnumerable requireables) return false; } - public List GetGettableItems(IEnumerable initialRequireables) + public List GetGettableItems(IEnumerable initialRequireables, bool shufflableItemsOnly) { + if(shufflableItemsOnly && !PalaceItemsAreShufflable) + { + return []; + } List requireables = []; requireables.AddRange(initialRequireables); List pendingRooms = new() { AllRooms.First(i => i.IsEntrance) }; @@ -1235,6 +1191,27 @@ public List GetGettableItems(IEnumerable initialRe return gettableItems; } + public void SetCollectables(IEnumerable collectables) + { + Debug.Assert(collectables.Count() == ItemRooms.Count); + int i = 0; + foreach (Collectable collectable in collectables) + { + ItemRooms[i++].Collectable = collectable; + } + } + + public bool ReplaceCollectable(Collectable toReplace, Collectable replacement) + { + Room? room = ItemRooms.FirstOrDefault(i => i.Collectable == toReplace); + if (room == null) + { + return false; + } + room.Collectable = replacement; + return true; + } + [Conditional("DEBUG")] public void AssertItemRoomsAreUnique(ROM rom) { @@ -1398,22 +1375,31 @@ public void ReplaceRoom(Room roomToReplace, Room newRoom) AllRooms.Add(newRoom); } + public Collectable GetVanillaCollectable() + { + return Number switch + { + 7 => Collectable.DO_NOT_USE, + _ => GetVanillaCollectables()[0] + }; + } - public static Collectable GetVanillaCollectable(int? palaceNum) + public List GetVanillaCollectables() { - Collectable[] vanillaCollectables = [ - Collectable.CANDLE, - Collectable.GLOVE, - Collectable.RAFT, - Collectable.BOOTS, - Collectable.FLUTE, - Collectable.CROSS + List[] vanillaCollectables = [ + [Collectable.CANDLE], + [Collectable.GLOVE], + [Collectable.RAFT], + [Collectable.BOOTS], + [Collectable.FLUTE], + [Collectable.CROSS], + [] ]; - if (palaceNum == null || (int)palaceNum > 6 || (int)palaceNum < 1) + if (Number > 7 || Number < 1) { throw new Exception("Invalid palace number"); } - return vanillaCollectables[(int)palaceNum - 1]; + return vanillaCollectables[Number - 1]; } public RoomExitType GetCoordinateRoomShape(int x, int y) diff --git a/RandomizerCore/Sidescroll/PalaceColors.cs b/RandomizerCore/Sidescroll/Palace/PalaceColors.cs similarity index 99% rename from RandomizerCore/Sidescroll/PalaceColors.cs rename to RandomizerCore/Sidescroll/Palace/PalaceColors.cs index 922a87286..91dc9437c 100644 --- a/RandomizerCore/Sidescroll/PalaceColors.cs +++ b/RandomizerCore/Sidescroll/Palace/PalaceColors.cs @@ -1,5 +1,4 @@ - -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; /// /// Color combinations that may be chosen for palace bricks and diff --git a/RandomizerCore/Sidescroll/PalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/PalaceGenerator.cs similarity index 98% rename from RandomizerCore/Sidescroll/PalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/PalaceGenerator.cs index 707ba44c2..153c21aab 100644 --- a/RandomizerCore/Sidescroll/PalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/PalaceGenerator.cs @@ -5,7 +5,7 @@ using System.Numerics; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public abstract class PalaceGenerator { diff --git a/RandomizerCore/Sidescroll/PalaceRooms.cs b/RandomizerCore/Sidescroll/Palace/PalaceRooms.cs similarity index 99% rename from RandomizerCore/Sidescroll/PalaceRooms.cs rename to RandomizerCore/Sidescroll/Palace/PalaceRooms.cs index 1f3b3de5c..5a30791cf 100644 --- a/RandomizerCore/Sidescroll/PalaceRooms.cs +++ b/RandomizerCore/Sidescroll/Palace/PalaceRooms.cs @@ -5,7 +5,7 @@ using System.Text.Json; using System.Text.RegularExpressions; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public partial class PalaceRooms { diff --git a/RandomizerCore/Sidescroll/Palaces.cs b/RandomizerCore/Sidescroll/Palace/Palaces.cs similarity index 99% rename from RandomizerCore/Sidescroll/Palaces.cs rename to RandomizerCore/Sidescroll/Palace/Palaces.cs index 91de75a27..54516b7c9 100644 --- a/RandomizerCore/Sidescroll/Palaces.cs +++ b/RandomizerCore/Sidescroll/Palace/Palaces.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using NLog; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public enum PalaceGrouping { @@ -156,6 +156,7 @@ public async Task> CreatePalaces(Random r, RandomizerProperties pro } } } + return palaces; } diff --git a/RandomizerCore/Sidescroll/RandomItemRoomSelectionStrategy.cs b/RandomizerCore/Sidescroll/Palace/RandomItemRoomSelectionStrategy.cs similarity index 98% rename from RandomizerCore/Sidescroll/RandomItemRoomSelectionStrategy.cs rename to RandomizerCore/Sidescroll/Palace/RandomItemRoomSelectionStrategy.cs index fd2a86d61..f3c4baa9d 100644 --- a/RandomizerCore/Sidescroll/RandomItemRoomSelectionStrategy.cs +++ b/RandomizerCore/Sidescroll/Palace/RandomItemRoomSelectionStrategy.cs @@ -3,7 +3,7 @@ using System.Linq; using YamlDotNet.Core.Tokens; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class RandomItemRoomSelectionStrategy : ItemRoomSelectionStrategy { diff --git a/RandomizerCore/Sidescroll/RandomWalkCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/RandomWalkCoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/RandomWalkCoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/RandomWalkCoordinatePalaceGenerator.cs index e5f522994..bea281eca 100644 --- a/RandomizerCore/Sidescroll/RandomWalkCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/RandomWalkCoordinatePalaceGenerator.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class RandomWalkCoordinatePalaceGenerator : ShapeFirstCoordinatePalaceGenerator { diff --git a/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/ReconstructedLoopyPalaceGenerator.cs similarity index 96% rename from RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/ReconstructedLoopyPalaceGenerator.cs index 16fc9749e..1f34987a9 100644 --- a/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/ReconstructedLoopyPalaceGenerator.cs @@ -4,7 +4,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class ReconstructedLoopyPalaceGenerator(CancellationToken ct) : ReconstructedPalaceGenerator(ct) { diff --git a/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/ReconstructedPalaceGenerator.cs similarity index 98% rename from RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/ReconstructedPalaceGenerator.cs index 4ad33d683..0880985c9 100644 --- a/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/ReconstructedPalaceGenerator.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class ReconstructedPalaceGenerator(CancellationToken ct) : PalaceGenerator { @@ -21,7 +21,7 @@ internal override async Task GeneratePalace(RandomizerProperties props, debug++; bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); // var palaceGroup = Util.AsPalaceGrouping(palaceNumber); - Palace palace = new(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); do // while (tries >= PALACE_SHUFFLE_ATTEMPT_LIMIT); { await Task.Yield(); @@ -37,7 +37,7 @@ internal override async Task GeneratePalace(RandomizerProperties props, //No longer a loop since the room pool can't (and would need to be) regenerated after each attempt. //Now we just fail and restart if we can't place enough rooms - palace = new(palaceNumber); + palace = new(palaceNumber, props.ShufflePalaceItems); palace.Entrance = new(roomPool.Entrances[r.Next(roomPool.Entrances.Count)]) { IsRoot = true, diff --git a/RandomizerCore/Sidescroll/Room.cs b/RandomizerCore/Sidescroll/Palace/Room.cs similarity index 99% rename from RandomizerCore/Sidescroll/Room.cs rename to RandomizerCore/Sidescroll/Palace/Room.cs index dd6fce0ec..58603c808 100644 --- a/RandomizerCore/Sidescroll/Room.cs +++ b/RandomizerCore/Sidescroll/Palace/Room.cs @@ -8,7 +8,7 @@ using js65; using NLog; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public record struct Coord(int X, int Y) : IComparable { diff --git a/RandomizerCore/Sidescroll/RoomExitType.cs b/RandomizerCore/Sidescroll/Palace/RoomExitType.cs similarity index 98% rename from RandomizerCore/Sidescroll/RoomExitType.cs rename to RandomizerCore/Sidescroll/Palace/RoomExitType.cs index 5abc96c52..93718562d 100644 --- a/RandomizerCore/Sidescroll/RoomExitType.cs +++ b/RandomizerCore/Sidescroll/Palace/RoomExitType.cs @@ -1,7 +1,7 @@ using System; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public enum RoomExitType { diff --git a/RandomizerCore/Sidescroll/Palace/RoomGroup.cs b/RandomizerCore/Sidescroll/Palace/RoomGroup.cs new file mode 100644 index 000000000..44769229c --- /dev/null +++ b/RandomizerCore/Sidescroll/Palace/RoomGroup.cs @@ -0,0 +1,6 @@ +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; + +public enum RoomGroup +{ + VANILLA, V4_0, V5_0, STUBS +} diff --git a/RandomizerCore/Sidescroll/RoomPool.cs b/RandomizerCore/Sidescroll/Palace/RoomPool.cs similarity index 99% rename from RandomizerCore/Sidescroll/RoomPool.cs rename to RandomizerCore/Sidescroll/Palace/RoomPool.cs index d94e23eff..99e25ac2e 100644 --- a/RandomizerCore/Sidescroll/RoomPool.cs +++ b/RandomizerCore/Sidescroll/Palace/RoomPool.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class RoomPool { diff --git a/RandomizerCore/Sidescroll/SegmentConnectionType.cs b/RandomizerCore/Sidescroll/Palace/SegmentConnectionType.cs similarity index 95% rename from RandomizerCore/Sidescroll/SegmentConnectionType.cs rename to RandomizerCore/Sidescroll/Palace/SegmentConnectionType.cs index af0f9c4f1..2f6915966 100644 --- a/RandomizerCore/Sidescroll/SegmentConnectionType.cs +++ b/RandomizerCore/Sidescroll/Palace/SegmentConnectionType.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public enum SegmentConnectionType { diff --git a/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/SequentialPlacementCoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/SequentialPlacementCoordinatePalaceGenerator.cs index 2a4ee2304..7bd8008b8 100644 --- a/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/SequentialPlacementCoordinatePalaceGenerator.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class SequentialPlacementCoordinatePalaceGenerator : CoordinatePalaceGenerator { @@ -33,7 +33,7 @@ internal override async Task GeneratePalace(RandomizerProperties props, debug++; bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); - Palace palace = new(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); openCoords = new(); Dictionary> roomsByExitType; roomPool = new(rooms); diff --git a/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/ShapeFirstCoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/ShapeFirstCoordinatePalaceGenerator.cs index 8f9ea26c9..0611215f8 100644 --- a/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/ShapeFirstCoordinatePalaceGenerator.cs @@ -8,7 +8,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public abstract class ShapeFirstCoordinatePalaceGenerator() : CoordinatePalaceGenerator() { @@ -16,7 +16,7 @@ public abstract class ShapeFirstCoordinatePalaceGenerator() : CoordinatePalaceGe internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber) { bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); - Palace palace = new(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); RoomPool roomPool = new(rooms); var itemRoomSelector = GetItemRoomSelectionStrategy(); // var palaceGroup = Util.AsPalaceGrouping(palaceNumber); diff --git a/RandomizerCore/Sidescroll/TowerCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/TowerCoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/TowerCoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/TowerCoordinatePalaceGenerator.cs index 61affa3b4..528ada4a4 100644 --- a/RandomizerCore/Sidescroll/TowerCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/TowerCoordinatePalaceGenerator.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class TowerCoordinatePalaceGenerator : ShapeFirstCoordinatePalaceGenerator { diff --git a/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/VanillaPalaceGenerator.cs similarity index 94% rename from RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/VanillaPalaceGenerator.cs index 113f9b6e1..e4d393293 100644 --- a/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/VanillaPalaceGenerator.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using NLog; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class VanillaPalaceGenerator() : PalaceGenerator { @@ -18,14 +18,10 @@ internal override async Task GeneratePalace(RandomizerProperties props, { throw new Exception("Invalid vanilla palace room pool"); } - Palace palace = new(palaceNumber); - - // var palaceGroup = Util.AsPalaceGrouping(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); palace.Entrance = new(roomPool.Entrances.First()); - // palace.Entrance.PalaceGroup = palaceGroup; palace.BossRoom = new(roomPool.BossRooms.First()); - // palace.BossRoom.PalaceGroup = palaceGroup; palace.AllRooms.Add(palace.Entrance); palace.ItemRooms = []; diff --git a/RandomizerCore/Sidescroll/VanillaRoomPool.cs b/RandomizerCore/Sidescroll/Palace/VanillaRoomPool.cs similarity index 97% rename from RandomizerCore/Sidescroll/VanillaRoomPool.cs rename to RandomizerCore/Sidescroll/Palace/VanillaRoomPool.cs index d7ae45881..9aee72ea3 100644 --- a/RandomizerCore/Sidescroll/VanillaRoomPool.cs +++ b/RandomizerCore/Sidescroll/Palace/VanillaRoomPool.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; internal class VanillaRoomPool : RoomPool { diff --git a/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/VanillaShufflePalaceGenerator.cs similarity index 95% rename from RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/VanillaShufflePalaceGenerator.cs index 6e192bc1e..bd451f02c 100644 --- a/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/VanillaShufflePalaceGenerator.cs @@ -1,7 +1,7 @@ using System; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class VanillaShufflePalaceGenerator() : VanillaPalaceGenerator() { diff --git a/RandomizerCore/Sidescroll/VanillaWeightedPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/VanillaWeightedPalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/VanillaWeightedPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/VanillaWeightedPalaceGenerator.cs index 59f253e20..2f7240269 100644 --- a/RandomizerCore/Sidescroll/VanillaWeightedPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/VanillaWeightedPalaceGenerator.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class VanillaWeightedPalaceGenerator : RandomWalkCoordinatePalaceGenerator { diff --git a/RandomizerCore/Sidescroll/RoomGroup.cs b/RandomizerCore/Sidescroll/RoomGroup.cs deleted file mode 100644 index d45f44752..000000000 --- a/RandomizerCore/Sidescroll/RoomGroup.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Z2Randomizer.RandomizerCore.Sidescroll; - -public enum RoomGroup -{ - VANILLA, V4_0, V5_0, STUBS -} diff --git a/RandomizerCore/Sidescroll/SideviewEnums.cs b/RandomizerCore/Sidescroll/SideviewEnums.cs index fd22bb8e9..a4c5e1a1d 100644 --- a/RandomizerCore/Sidescroll/SideviewEnums.cs +++ b/RandomizerCore/Sidescroll/SideviewEnums.cs @@ -177,6 +177,39 @@ public enum GreatPalaceObject PILLAR = 0xF0 } +public enum TownObject +{ + CLOSED_DOOR = 0x00, + OPEN_DOOR1 = 0x01, + OPEN_DOOR2 = 0x02, + OPEN_DOOR3 = 0x03, + STONEHENGE = 0x04, //??? + CHAIR = 0x05, + LARGE_CLOUD = 0x06, + SMALL_CLOUD_07 = 0x07, + TABLE = 0x08, + BENCH = 0x09, + CROSS = 0x0A, + CHIMNEY_ENTRANCE = 0x0B, + CHIMNEY_NON_ENTRANCE = 0x0C, + CHURCH_CROSS = 0x0D, + SMALL_CLOUD_OE = 0x0E, + COLLECTABLE = 0x0F, + CROSS_HASH_ROOF = 0x20, + GREY_ROOF = 0x30, + GREEN_ROOF = 0x40, + BRICK_BUILDING = 0x50, + WHITE_BRICK_BUILDING = 0x60, + GRASS_COLOR_BUILDING = 0x70, + WHITE_WALL = 0x80, + LOG_BUILDING = 0x90, + SQUARE_WINDOW = 0xA0, + GRAY_WINDOW = 0xB0, + BROWN_WINDOWS = 0xC0, + UNDERGROUND_BRICKS = 0xD0, + PILLAR = 0xE0, +} + public static class ForestObjectExtensions { public static int Width(SideviewMapCommand command) diff --git a/RandomizerCore/Sidescroll/Town/Town.cs b/RandomizerCore/Sidescroll/Town/Town.cs new file mode 100644 index 000000000..262898cbf --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/Town.cs @@ -0,0 +1,213 @@ +using DynamicData; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO.MemoryMappedFiles; +using System.Linq; +using System.Text; +using Z2Randomizer.RandomizerCore.Enemy; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +public class Town +{ + + public string Name { get; set; } = ""; + public TownType? Type { get; set; } + + public List TownMaps { get; } = []; + + //When custom towns happen, make this real + //public Town(string townJson) + + public Town(List townMaps, Dictionary connections, Dictionary doorConnections) + { + Debug.Assert(connections.Count == doorConnections.Count); + TownMaps = townMaps; + foreach (TownMap townMap in townMaps.OrderBy(i => i.Map).Where(i => !i.IsInternalLocation || (i.Map == 57 && Type == TownType.DARUNIA))) + { + if(connections.TryGetValue(townMap.Map, out var connectionsRaw)) + { + byte connectionMap = (byte)((connectionsRaw[0] & 0xFC) >> 2); + townMap.Left = townMaps.FirstOrDefault(i => i.Map == connectionMap); + connectionMap = (byte)((connectionsRaw[3] & 0xFC) >> 2); + townMap.Right = townMaps.FirstOrDefault(i => i.Map == connectionMap); + } + + if (doorConnections.TryGetValue(townMap.Map, out var doorConnectionsRaw)) + { + byte connectionMap = (byte)((doorConnectionsRaw[0] & 0xFC) >> 2); + townMap.Door0 = townMaps.FirstOrDefault(i => i.Map == connectionMap); + connectionMap = (byte)((doorConnectionsRaw[1] & 0xFC) >> 2); + townMap.Door1 = townMaps.FirstOrDefault(i => i.Map == connectionMap); + connectionMap = (byte)((doorConnectionsRaw[2] & 0xFC) >> 2); + townMap.Door2 = townMaps.FirstOrDefault(i => i.Map == connectionMap); + connectionMap = (byte)((doorConnectionsRaw[3] & 0xFC) >> 2); + townMap.Door3 = townMaps.FirstOrDefault(i => i.Map == connectionMap); + } + } + } + + //Determine what items you can get from this town with these reqireables from this entrance direction + public List GetGettableItems(Direction entranceDirection, List requireables, bool shufflableItemsOnly = false) + { + TownMap startMap = entranceDirection switch + { + Direction.WEST => TownMaps.First(i => i.Left == null), + Direction.EAST => TownMaps.First(i => i.Right == null), + _ => throw new ArgumentException("Invalid entrance direction. Towns can only enter from the west/east") + }; + + List collectables = []; + + List checkedMaps = []; + List mapsToCheck = [startMap]; + TownMap currentMap; + do + { + currentMap = mapsToCheck[0]; + if (currentMap.AccessRequirements.AreSatisfiedBy(requireables)) + { + if (currentMap.Door0 != null && !checkedMaps.Contains(currentMap.Door0) && !mapsToCheck.Contains(currentMap.Door0)) + { + mapsToCheck.Add(currentMap.Door0); + } + if (currentMap.Door1 != null && !checkedMaps.Contains(currentMap.Door1) && !mapsToCheck.Contains(currentMap.Door1)) + { + mapsToCheck.Add(currentMap.Door1); + } + if (currentMap.Door2 != null && !checkedMaps.Contains(currentMap.Door2) && !mapsToCheck.Contains(currentMap.Door2)) + { + mapsToCheck.Add(currentMap.Door2); + } + if (currentMap.Door3 != null && !checkedMaps.Contains(currentMap.Door3) && !mapsToCheck.Contains(currentMap.Door3)) + { + mapsToCheck.Add(currentMap.Door3); + } + if (entranceDirection == Direction.WEST + //This is a hacky fix for the darunia upstab guy intermediary room, which is the only vanilla room + //after shortenWizards that connects to another internal room. Normal internal rooms specifically need + //to not consider their connections because there's often multiple rooms in the same map, + //but this specific one does so it's special cased here. This will need to be fixed for custom towns. + && (!currentMap.IsInternalLocation || (currentMap.Map == 57 && Type == TownType.DARUNIA)) + && currentMap.Right != null + && !checkedMaps.Contains(currentMap.Right) + && !mapsToCheck.Contains(currentMap.Right)) + { + mapsToCheck.Add(currentMap.Right); + } + if (entranceDirection == Direction.EAST + && !currentMap.IsInternalLocation + && currentMap.Left != null + && !checkedMaps.Contains(currentMap.Left) + && !mapsToCheck.Contains(currentMap.Left)) + { + mapsToCheck.Add(currentMap.Left); + } + if (currentMap.Collectable != null && (currentMap.CollectableIsShufflable || !shufflableItemsOnly)) + { + collectables.Add((Collectable)currentMap.Collectable!); + } + } + checkedMaps.Add(currentMap); + mapsToCheck.Remove(currentMap); + } + while (mapsToCheck.Count > 0); + + return collectables; + } + + public bool CanBeTraversed(List requireables) + { + TownMap leftMap = TownMaps.First(i => i.Left == null && !i.IsInternalLocation); + + //This currently only checks left-right because all towns are either bi-directionally traversable or oneway deadending right (new kasuto) + //This means it will perform weirdly if you somehow create a monodirectional town entered from the right that you also + //for some reason set as a connection. Don't do that. + List coveredMaps = []; + TownMap currentMap = leftMap; + while(coveredMaps.Count < 63) + { + if(coveredMaps.Contains(currentMap)) + { + throw new Exception($"Loop detected in town connection routing. Check your town connections for town {Name}."); + } + if(!currentMap.AccessRequirements.AreSatisfiedBy(requireables)) + { + return false; + } + if (currentMap.Right == null) + { + return false; + } + if(currentMap.RightExitIsOutside) + { + return true; + } + coveredMaps.Add(currentMap); + currentMap = currentMap.Right; + } + throw new ImpossibleException("Too many non-looping maps in town traversal check"); + } + + public void SetCollectables(IEnumerable collectables) + { + List collectableMaps = TownMaps.Where(i => i.Collectable != null).ToList(); + Debug.Assert(collectables.Count() == collectableMaps.Count); + int i = 0; + foreach(Collectable collectable in collectables) + { + collectableMaps[i++].Collectable = collectable; + } + } + + public bool ReplaceCollectable(Collectable toReplace, Collectable replacement) + { + TownMap? map = TownMaps.FirstOrDefault(i => i.Collectable == toReplace); + if(map == null) + { + return false; + } + map.Collectable = replacement; + return true; + } + + public TownMap? GetWizard() + { + if(Type == null) return null; + + VanillaTownMap map = Type switch + { + TownType.RAURU => VanillaTownMap.RAURU_WIZARD, + TownType.RUTO => VanillaTownMap.RUTO_WIZARD, + TownType.SARIA => VanillaTownMap.SARIA_WIZARD, + TownType.MIDO => VanillaTownMap.MIDO_WIZARD, + TownType.NABOORU => VanillaTownMap.NABOORU_WIZARD, + TownType.DARUNIA => VanillaTownMap.DARUNIA_WIZARD, + TownType.NEW_KASUTO => VanillaTownMap.NEW_KASUTO_WIZARD, + TownType.OLD_KASUTO => VanillaTownMap.OLD_KASUTO_WIZARD, + _ => throw new ImpossibleException("Unrecognized town type in GetWizard()") + }; + return GetTownMap(map); + } + + public TownMap? GetTownMap(int mapNumber) + { + return TownMaps.FirstOrDefault(i => i.Map == mapNumber); + } + + public TownMap? GetTownMap(VanillaTownMap map) + { + return GetTownMap((int)map); + } + + public bool HasShufflableItems() + { + return TownMaps.Any(i => i.CollectableIsShufflable); + } + + public string GenerateSpoiler() + { + return string.Join("\n", TownMaps.Where(i => i.Collectable != null).Select(i => $"\t{i.Name} : {i.Collectable?.EnglishText() ?? ""}")); + } +} diff --git a/RandomizerCore/Sidescroll/Town/TownMap.cs b/RandomizerCore/Sidescroll/Town/TownMap.cs new file mode 100644 index 000000000..f87d5ed84 --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/TownMap.cs @@ -0,0 +1,49 @@ +using System.Diagnostics; +using Z2Randomizer.RandomizerCore.Enemy; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +[DebuggerDisplay("{" + nameof(DebugString) + "(),nq}")] +public class TownMap +{ + public string Name { get; set; } = ""; + public int Map { get; set; } + public bool IsInternalLocation { get; } + public SideviewEditable Sideview { get; } + + public Collectable? Collectable { get; set; } + public bool CollectableIsShufflable { get; set; } + public TownMap? Left { get; set; } + public TownMap? Right { get; set; } + public TownMap? Door0 { get; set; } + public TownMap? Door1 { get; set; } + public TownMap? Door2 { get; set; } + public TownMap? Door3 { get; set; } + + //WTF should the script look like + + //For now, all screens that have collectables don't have separate requirements between access and collection. + //This may change in the future, which will require a bit of refactoring + public Requirements AccessRequirements { get; set; } = Requirements.NONE; + public EnemiesEditable Enemies { get; } + + //Some way to store what the Left/Right exit connection should be if it connects to the overworld. + //I'm fairly certain FC vs FD matters for saria, but I need to test it, if it doesn't matter it can just always be FC + //For now i'm assuming it doesn't matter and any null left/right connection is an external connection FC + //We can just test it when we have writing working and modify if needed + public bool LeftExitIsOutside { get; set; } + public bool RightExitIsOutside { get; set; } + + public TownMap(int mapNumber, byte[] sideviewsRaw, byte[] enemiesRaw, bool isInternalLocation) + { + Sideview = new(sideviewsRaw); + Enemies = new(enemiesRaw); + Map = mapNumber; + IsInternalLocation = isInternalLocation; + } + + public string DebugString() + { + return $"{Map} - {Name}"; + } +} \ No newline at end of file diff --git a/RandomizerCore/Sidescroll/Town/TownType.cs b/RandomizerCore/Sidescroll/Town/TownType.cs new file mode 100644 index 000000000..6233964c4 --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/TownType.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +public enum TownType +{ + INVALID = 0, + RAURU = 1, + RUTO = 2, + SARIA = 3, + MIDO = 4, + NABOORU = 5, + DARUNIA = 6, + NEW_KASUTO = 7, + OLD_KASUTO = 8, + BAGU = 9, +} + +public static class TownExtensions +{ + public const int SPELL_GET_START_ADDRESS = 0x17AF7; + public static int VanillaTownOrder(this TownType town) + { + return town switch + { + TownType.INVALID => 99, + TownType.RAURU => 1, + TownType.RUTO => 2, + TownType.SARIA => 3, + TownType.MIDO => 4, + TownType.NABOORU => 5, + TownType.DARUNIA => 6, + TownType.NEW_KASUTO => 7, + TownType.OLD_KASUTO => 8, + _ => 0 + }; + } + + public static string HintName(this TownType town) + { + return town switch + { + TownType.RAURU => "RAURU", + TownType.RUTO => "RUTO", + TownType.SARIA => "SARIA", + TownType.MIDO => "MIDO", + TownType.NABOORU => "NABOORU", + TownType.DARUNIA => "DARUNIA", + TownType.NEW_KASUTO => "NEW KASUTO", + TownType.OLD_KASUTO => "OLD KASUTO", + _ => "UNKNOWN" + }; + } +} + diff --git a/RandomizerCore/Sidescroll/Town/Towns.cs b/RandomizerCore/Sidescroll/Town/Towns.cs new file mode 100644 index 000000000..8f2721a7d --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/Towns.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +public class Towns +{ + + //There's almost certainly way cleaner way to do this but this works for now. + public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableContainerRequirements) + { + Town town; + TownMap map; + switch(townType) + { + case TownType.RAURU: + town = rom.LoadTown(1, 2); + town.Type = TownType.RAURU; + town.Name = "Rauru"; + map = town.GetTownMap(VanillaTownMap.RAURU_LEFT)!; + map.Name = "Rauru Left"; + map = town.GetTownMap(VanillaTownMap.RAURU_RIGHT)!; + map.Name = "Rauru Right"; + map = town.GetTownMap(VanillaTownMap.RAURU_WIZARD)!; + map.Name = "Rauru Wizard"; + map.Collectable = Collectable.SHIELD_SPELL; + map.AccessRequirements = disableContainerRequirements ? Requirements.NONE : new Requirements([RequirementType.ONE_CONTAINER]); + break; + case TownType.RUTO: + town = rom.LoadTown(4, 2); + town.Type = TownType.RUTO; + town.Name = "Ruto"; + map = town.GetTownMap(VanillaTownMap.RUTO_LEFT)!; + map.Name = "Ruto Left"; + map = town.GetTownMap(VanillaTownMap.RUTO_RIGHT)!; + map.Name = "Ruto Right"; + map = town.GetTownMap(VanillaTownMap.RUTO_WIZARD)!; + map.Name = "Ruto Wizard"; + map.Collectable = Collectable.JUMP_SPELL; + map.AccessRequirements = disableContainerRequirements ? new Requirements([RequirementType.TROPHY]) + : new Requirements([], [[RequirementType.TROPHY, RequirementType.TWO_CONTAINERS]]); + break; + case TownType.SARIA: + town = rom.LoadTown(6, 3); + town.Type = TownType.SARIA; + town.Name = "Saria"; + map = town.GetTownMap(VanillaTownMap.SARIA_LEFT)!; + map.Name = "Saria Moat"; + map.AccessRequirements = new Requirements([RequirementType.FAIRY, RequirementType.BAGU_LETTER], [[RequirementType.JUMP, RequirementType.DASH]]); + map = town.GetTownMap(VanillaTownMap.SARIA_MID)!; + map.Name = "Saria Middle"; + map = town.GetTownMap(VanillaTownMap.SARIA_RIGHT)!; + map.Name = "Saria Right"; + map = town.GetTownMap(VanillaTownMap.SARIA_WIZARD)!; + map.Name = "Saria Wizard"; + map.Collectable = Collectable.LIFE_SPELL; + map.AccessRequirements = disableContainerRequirements ? + new Requirements([RequirementType.MIRROR]) : new Requirements([], [[RequirementType.MIRROR, RequirementType.THREE_CONTAINERS]]); + map = town.GetTownMap(VanillaTownMap.SARIA_TABLE)!; + map.Name = "Saria Table"; + map.Collectable = Collectable.MIRROR; + break; + case TownType.MIDO: + town = rom.LoadTown(9, 3); + town.Type = TownType.MIDO; + town.Name = "Mido"; + map = town.GetTownMap(VanillaTownMap.MIDO_LEFT)!; + map.Name = "Mido Left"; + map = town.GetTownMap(VanillaTownMap.MIDO_MIDDLE)!; + map.Name = "Mido Middle"; + map = town.GetTownMap(VanillaTownMap.MIDO_RIGHT)!; + map.Name = "Mido Right"; + map = town.GetTownMap(VanillaTownMap.MIDO_WIZARD)!; + map.Name = "Mido Wizard"; + map.Collectable = Collectable.FAIRY_SPELL; + map.AccessRequirements = disableContainerRequirements ? new Requirements([RequirementType.MEDICINE]) + : new Requirements([], [[RequirementType.MEDICINE, RequirementType.FOUR_CONTAINERS]]); + map = town.GetTownMap(VanillaTownMap.MIDO_TRAINER)!; + map.Name = "Downstab Guy"; + map.Collectable = Collectable.DOWNSTAB; + map.AccessRequirements = new Requirements([RequirementType.JUMP, RequirementType.FAIRY]); + break; + case TownType.NABOORU: + town = rom.LoadTown(12, 3); + town.Type = TownType.NABOORU; + town.Name = "Nabooru"; + map = town.GetTownMap(VanillaTownMap.NABOORU_LEFT)!; + map.Name = "Nabooru Left"; + map = town.GetTownMap(VanillaTownMap.NABOORU_MID)!; + map.Name = "Nabooru Fountain"; + map.Collectable = Collectable.WATER; + map = town.GetTownMap(VanillaTownMap.NABOORU_RIGHT)!; + map.Name = "Nabooru Right"; + map = town.GetTownMap(VanillaTownMap.NABOORU_WIZARD)!; + map.Name = "Nabooru Wizard"; + map.Collectable = Collectable.FIRE_SPELL; + map.AccessRequirements = disableContainerRequirements ? new Requirements([RequirementType.WATER]) + : new Requirements([], [[RequirementType.WATER, RequirementType.FIVE_CONTAINERS]]); + break; + case TownType.DARUNIA: + town = rom.LoadTown(15, 3); + town.Type = TownType.DARUNIA; + town.Name = "Darunia"; + map = town.GetTownMap(VanillaTownMap.DARUNIA_LEFT)!; + map.Name = "Darunia Left"; + map = town.GetTownMap(VanillaTownMap.DARUNIA_MID)!; + map.Name = "Darunia Middle"; + map = town.GetTownMap(VanillaTownMap.DARUNIA_RIGHT)!; + map.Name = "Darunia Right"; + map = town.GetTownMap(VanillaTownMap.DARUNIA_WIZARD)!; + map.Name = "Darunia Wizard"; + map.Collectable = Collectable.REFLECT_SPELL; + map.AccessRequirements = disableContainerRequirements ? new Requirements([RequirementType.CHILD]) + : new Requirements([], [[RequirementType.CHILD, RequirementType.SIX_CONTAINERS]]); + //Upstab guy is the only map in wizard-shortened vanilla where an internal map connects to another internal map + //so it didn't get loaded by LoadTown, so load/connect it here. + map = rom.LoadTownMap((int)VanillaTownMap.DARUNIA_TRAINER, true); + town.TownMaps.Add(map); + map.Left = town.GetTownMap(VanillaTownMap.DARUNIA_MID); + map.Name = "Upstab Guy"; + map.Collectable = Collectable.UPSTAB; + town.GetTownMap(VanillaTownMap.DARUNIA_UPSTAB_HOUSE)!.Right = map; + map.AccessRequirements = new Requirements([RequirementType.JUMP, RequirementType.FAIRY]); + + break; + case TownType.NEW_KASUTO: + town = rom.LoadTown(18, 3); + town.Type = TownType.NEW_KASUTO; + town.Name = "New Kasuto"; + map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_LEFT)!; + map.Name = "New Kasuto Left"; + map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_MID)!; + map.Name = "New Kasuto Middle"; + map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_RIGHT)!; + map.Name = "New Kasuto Right"; + map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_WIZARD)!; + map.Name = "New Kasuto Wizard"; + map.Collectable = Collectable.SPELL_SPELL; + map.AccessRequirements = disableContainerRequirements ? Requirements.NONE : new Requirements([RequirementType.SEVEN_CONTAINERS]); + map = town.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!; + map.Name = "Granny's Basement"; + map.AccessRequirements = new Requirements([RequirementType.SEVEN_CONTAINERS]); + map = town.GetTownMap(VanillaTownMap.SPELL_TOWER_INTERIOR)!; + map.Name = "Spell Tower" + + ""; + map.AccessRequirements = new Requirements([RequirementType.SPELL]); + break; + case TownType.OLD_KASUTO: + town = rom.LoadTown(21, 3); + town.Type = TownType.OLD_KASUTO; + town.Name = "Old Kasuto"; + map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_LEFT)!; + map.Name = "Old Kasuto Left"; + map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_MID)!; + map.Name = "Old Kasuto Left"; + map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_RIGHT)!; + map.Name = "Old Kasuto Right"; + map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_WIZARD)!; + map.Name = "Old Kasuto Wizard"; + map.Collectable = Collectable.THUNDER_SPELL; + map.AccessRequirements = disableContainerRequirements ? Requirements.NONE + : new Requirements([RequirementType.EIGHT_CONTAINERS]); + break; + case TownType.BAGU: + town = rom.LoadTown(24, 1); + town.Type = TownType.BAGU; + town.Name = "Bagu's House"; + map = town.GetTownMap(VanillaTownMap.BAGU_EXTERNAL)!; + map.Name = "Bagu Exterior"; + map = town.GetTownMap(VanillaTownMap.BAGU_INDOORS)!; + map.Name = "Bagu Interior"; + map.Collectable = Collectable.BAGUS_NOTE; + break; + case TownType.INVALID: + throw new Exception($"Unrecognized vanilla town: {townType.HintName()}"); + default: + throw new Exception($"Unrecognized vanilla town: {townType.HintName()}"); + } + return town; + } +} diff --git a/RandomizerCore/Sidescroll/Town/VanillaTownMap.cs b/RandomizerCore/Sidescroll/Town/VanillaTownMap.cs new file mode 100644 index 000000000..f600b137b --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/VanillaTownMap.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +//Map indexes from the towns bank +public enum VanillaTownMap +{ + GLITCH_TOWN = 0, + RAURU_LEFT = 1, + RAURU_RIGHT = 2, + GLITCH_RUTO = 3, + RUTO_LEFT = 4, + RUTO_RIGHT = 5, + SARIA_LEFT = 6, + SARIA_MID = 7, + SARIA_RIGHT = 8, + MIDO_LEFT = 9, + MIDO_MIDDLE = 10, + MIDO_RIGHT = 11, + NABOORU_LEFT = 12, + NABOORU_MID = 13, + NABOORU_RIGHT = 14, + DARUNIA_LEFT = 15, + DARUNIA_MID = 16, + DARUNIA_RIGHT = 17, + NEW_KASUTO_LEFT = 18, + NEW_KASUTO_MID = 19, + NEW_KASUTO_RIGHT = 20, + OLD_KASUTO_LEFT = 21, + OLD_KASUTO_MID = 22, + OLD_KASUTO_RIGHT = 23, + BAGU_EXTERNAL = 24, + WIZARD_HOUSE_UPSTAIRS = 26, + SARIA_TABLE = 30, + BAGU_INDOORS = 31, + RAURU_WIZARD = 36, + RUTO_WIZARD = 37, + SARIA_WIZARD = 38, + MIDO_WIZARD = 39, + NABOORU_WIZARD = 40, + DARUNIA_WIZARD = 41, + NEW_KASUTO_WIZARD = 42, + OLD_KASUTO_WIZARD = 43, + MIDO_TRAINER = 44, + DARUNIA_TRAINER = 45, + GRANNYS_BASEMENT = 46, + SPELL_TOWER_INTERIOR = 47, + DARUNIA_UPSTAB_HOUSE = 57 +} diff --git a/RandomizerCore/Text.cs b/RandomizerCore/Text.cs index fb50e84b9..9353fe4fa 100644 --- a/RandomizerCore/Text.cs +++ b/RandomizerCore/Text.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using Z2Randomizer.RandomizerCore.Overworld; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -79,55 +80,36 @@ public bool ValidateDialogText() return true; } - public static Text GenerateHelpfulHint(List allLocations, Location location, Collectable collectable, bool useTownSpecificHints) + public static Text GenerateHelpfulHint(Location location, Collectable collectable, bool useTownSpecificHints) { string? hint = null; - if (location.PalaceNumber == 1) + if (location.Palace?.Number == 1) { hint = "horsehead$neighs$with the$%%"; } - else if (location.PalaceNumber == 2) + else if (location.Palace?.Number == 2) { hint = "helmethead$guards the$%%"; } - else if (location.PalaceNumber == 3) + else if (location.Palace?.Number == 3) { hint = "rebonack$rides$with the$%%"; } - else if (location.PalaceNumber == 4) + else if (location.Palace?.Number == 4) { hint = "carock$disappears$with the$%%"; } - else if (location.PalaceNumber == 5) + else if (location.Palace?.Number == 5) { hint = "gooma sits$on the$%%"; } - else if (location.PalaceNumber == 6) + else if (location.Palace?.Number == 6) { hint = "barba$slithers$with the$%%"; } - else if (useTownSpecificHints) + else if (useTownSpecificHints && location.Town != null) { - //For now bagu is not a town for hints. Could change in the future. - if (location.ActualTown != null && location.ActualTown != Town.BAGU) - { - hint = $"{((Town)location.ActualTown).HintName()}$has the$%%"; - } - //Saria table - if(allLocations.First(i => i.ActualTown == Town.SARIA_NORTH).Children.Contains(location)) - { - hint = $"{Town.SARIA_NORTH.HintName()}$has the$%%"; - } - //Nabooru fountain - if (allLocations.First(i => i.ActualTown == Town.NABOORU).Children.Contains(location)) - { - hint = $"{Town.NABOORU.HintName()}$has the$%%"; - } - //Spell Tower / Granny's Basement - if (allLocations.First(i => i.ActualTown == Town.NEW_KASUTO).Children.Contains(location)) - { - hint = $"{Town.NEW_KASUTO.HintName()}$has the$%%"; - } + hint = $"{((TownType)location.Town.Type!).HintName()}$has the$%%"; } if(hint == null) { diff --git a/RandomizerCore/Town.cs b/RandomizerCore/Town.cs deleted file mode 100644 index 2ba6c9621..000000000 --- a/RandomizerCore/Town.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; - -namespace Z2Randomizer.RandomizerCore; - -public enum Town -{ - INVALID = 0, - RAURU = 1, - RUTO = 2, - SARIA_NORTH = 3, - SARIA_SOUTH = 4, - MIDO_WEST = 5, - MIDO_CHURCH = 6, - NABOORU = 7, - DARUNIA_WEST = 8, - DARUNIA_ROOF = 9, - NEW_KASUTO = 10, - OLD_KASUTO = 11, - SARIA_TABLE = 12, - BAGU = 13, - NABOORU_FOUNTAIN = 14, -} - -public static class TownExtensions -{ - public const int SPELL_GET_START_ADDRESS = 0x17AF7; - public static int VanillaTownOrder(this Town town) - { - return town switch - { - Town.INVALID => 99, - Town.RAURU => 1, - Town.RUTO => 2, - Town.SARIA_NORTH => 3, - Town.MIDO_WEST => 4, - Town.NABOORU => 5, - Town.DARUNIA_WEST => 6, - Town.NEW_KASUTO => 7, - Town.OLD_KASUTO => 8, - _ => 0 - }; - } - - public static bool IsWizardTown(this Town town) - { - return town switch - { - Town.RAURU => true, - Town.RUTO => true, - Town.SARIA_NORTH => true, - Town.MIDO_WEST => true, - Town.NABOORU => true, - Town.DARUNIA_WEST => true, - Town.NEW_KASUTO => true, - Town.OLD_KASUTO => true, - Town.MIDO_CHURCH => true, - Town.DARUNIA_ROOF => true, - _ => false - }; - } - - public static string HintName(this Town town) - { - return town switch - { - Town.RAURU => "RAURU", - Town.RUTO => "RUTO", - Town.SARIA_NORTH => "SARIA", - Town.MIDO_WEST => "MIDO", - Town.NABOORU => "NABOORU", - Town.DARUNIA_WEST => "DARUNIA", - Town.NEW_KASUTO => "NEW KASUTO", - Town.OLD_KASUTO => "OLD KASUTO", - Town.MIDO_CHURCH => "MIDO", - Town.DARUNIA_ROOF => "DARUNIA", - _ => "UNKNOWN" - }; - } - - public static bool AppearsOnMap(this Town town) - { - return town switch - { - Town.RAURU => true, - Town.RUTO => true, - Town.SARIA_NORTH => true, - Town.SARIA_SOUTH => true, - Town.MIDO_WEST => true, - Town.MIDO_CHURCH => false, - Town.NABOORU => true, - Town.DARUNIA_WEST => true, - Town.DARUNIA_ROOF => false, - Town.NEW_KASUTO => true, - Town.OLD_KASUTO => true, - Town.SARIA_TABLE => false, - Town.BAGU => true, - Town.NABOORU_FOUNTAIN => false, - _ => throw new Exception("Unrecognized town: " + town.ToString()) - }; - } - - public static Town GetMasterTown(this Town town) - { - return town switch - { - Town.SARIA_TABLE => Town.SARIA_NORTH, - Town.DARUNIA_ROOF => Town.DARUNIA_WEST, - Town.NABOORU_FOUNTAIN => Town.NABOORU, - Town.MIDO_CHURCH => Town.MIDO_WEST, - _ => town - }; - } - - public static bool IsUnderConsiderationForReachable(this Town town) - { - return town switch - { - Town.RAURU => true, - Town.RUTO => true, - Town.SARIA_NORTH => true, - Town.SARIA_SOUTH => true, - Town.MIDO_WEST => true, - Town.MIDO_CHURCH => false, - Town.NABOORU => true, - Town.DARUNIA_WEST => true, - Town.DARUNIA_ROOF => false, - Town.NEW_KASUTO => true, - Town.OLD_KASUTO => true, - Town.SARIA_TABLE => false, - Town.BAGU => true, - Town.NABOORU_FOUNTAIN => false, - _ => throw new Exception("Unrecognized town: " + town.ToString()) - }; - } -} diff --git a/RandomizerCore/TownEnum.cs b/RandomizerCore/TownEnum.cs new file mode 100644 index 000000000..f4dcb6b47 --- /dev/null +++ b/RandomizerCore/TownEnum.cs @@ -0,0 +1,137 @@ +/* +using System; + +namespace Z2Randomizer.RandomizerCore; + +public enum TownEnum +{ + INVALID = 0, + RAURU = 1, + RUTO = 2, + SARIA_NORTH = 3, + SARIA_SOUTH = 4, + MIDO_WEST = 5, + MIDO_CHURCH = 6, + NABOORU = 7, + DARUNIA_WEST = 8, + DARUNIA_ROOF = 9, + NEW_KASUTO = 10, + OLD_KASUTO = 11, + SARIA_TABLE = 12, + BAGU = 13, + NABOORU_FOUNTAIN = 14, +} + +public static class TownExtensions +{ + public const int SPELL_GET_START_ADDRESS = 0x17AF7; + public static int VanillaTownOrder(this TownEnum town) + { + return town switch + { + TownEnum.INVALID => 99, + TownEnum.RAURU => 1, + TownEnum.RUTO => 2, + TownEnum.SARIA_NORTH => 3, + TownEnum.MIDO_WEST => 4, + TownEnum.NABOORU => 5, + TownEnum.DARUNIA_WEST => 6, + TownEnum.NEW_KASUTO => 7, + TownEnum.OLD_KASUTO => 8, + _ => 0 + }; + } + + public static bool IsWizardTown(this TownEnum town) + { + return town switch + { + TownEnum.RAURU => true, + TownEnum.RUTO => true, + TownEnum.SARIA_NORTH => true, + TownEnum.MIDO_WEST => true, + TownEnum.NABOORU => true, + TownEnum.DARUNIA_WEST => true, + TownEnum.NEW_KASUTO => true, + TownEnum.OLD_KASUTO => true, + TownEnum.MIDO_CHURCH => true, + TownEnum.DARUNIA_ROOF => true, + _ => false + }; + } + + public static string HintName(this TownEnum town) + { + return town switch + { + TownEnum.RAURU => "RAURU", + TownEnum.RUTO => "RUTO", + TownEnum.SARIA_NORTH => "SARIA", + TownEnum.MIDO_WEST => "MIDO", + TownEnum.NABOORU => "NABOORU", + TownEnum.DARUNIA_WEST => "DARUNIA", + TownEnum.NEW_KASUTO => "NEW KASUTO", + TownEnum.OLD_KASUTO => "OLD KASUTO", + TownEnum.MIDO_CHURCH => "MIDO", + TownEnum.DARUNIA_ROOF => "DARUNIA", + _ => "UNKNOWN" + }; + } + + public static bool AppearsOnMap(this TownEnum town) + { + return town switch + { + TownEnum.RAURU => true, + TownEnum.RUTO => true, + TownEnum.SARIA_NORTH => true, + TownEnum.SARIA_SOUTH => true, + TownEnum.MIDO_WEST => true, + TownEnum.MIDO_CHURCH => false, + TownEnum.NABOORU => true, + TownEnum.DARUNIA_WEST => true, + TownEnum.DARUNIA_ROOF => false, + TownEnum.NEW_KASUTO => true, + TownEnum.OLD_KASUTO => true, + TownEnum.SARIA_TABLE => false, + TownEnum.BAGU => true, + TownEnum.NABOORU_FOUNTAIN => false, + _ => throw new Exception("Unrecognized town: " + town.ToString()) + }; + } + + public static TownEnum GetMasterTown(this TownEnum town) + { + return town switch + { + TownEnum.SARIA_TABLE => TownEnum.SARIA_NORTH, + TownEnum.DARUNIA_ROOF => TownEnum.DARUNIA_WEST, + TownEnum.NABOORU_FOUNTAIN => TownEnum.NABOORU, + TownEnum.MIDO_CHURCH => TownEnum.MIDO_WEST, + _ => town + }; + } + + public static bool IsUnderConsiderationForReachable(this TownEnum town) + { + return town switch + { + TownEnum.RAURU => true, + TownEnum.RUTO => true, + TownEnum.SARIA_NORTH => true, + TownEnum.SARIA_SOUTH => true, + TownEnum.MIDO_WEST => true, + TownEnum.MIDO_CHURCH => false, + TownEnum.NABOORU => true, + TownEnum.DARUNIA_WEST => true, + TownEnum.DARUNIA_ROOF => false, + TownEnum.NEW_KASUTO => true, + TownEnum.OLD_KASUTO => true, + TownEnum.SARIA_TABLE => false, + TownEnum.BAGU => true, + TownEnum.NABOORU_FOUNTAIN => false, + _ => throw new Exception("Unrecognized town: " + town.ToString()) + }; + } +} +*/ \ No newline at end of file diff --git a/RandomizerCore/Towns.cs b/RandomizerCore/Towns.cs deleted file mode 100644 index 88425b6f0..000000000 --- a/RandomizerCore/Towns.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; - -namespace Z2Randomizer.RandomizerCore; - -internal class Towns -{ - /* - public static readonly Dictionary townSpellAndItemRequirements = new() - { - { Town.RAURU, new Requirements() }, - { Town.RUTO, new Requirements([RequirementType.TROPHY]) }, - { Town.SARIA_NORTH, new Requirements([RequirementType.MIRROR]) }, - { Town.MIDO_WEST, new Requirements([RequirementType.MEDICINE]) }, - { Town.MIDO_CHURCH, new Requirements([RequirementType.JUMP, RequirementType.FAIRY]) }, - { Town.NABOORU, new Requirements([], [[RequirementType.WATER, RequirementType.FIVE_CONTAINERS]]) }, - { Town.DARUNIA_WEST, new Requirements([], [[RequirementType.CHILD, RequirementType.SIX_CONTAINERS]]) }, - { Town.DARUNIA_ROOF, new Requirements([RequirementType.JUMP, RequirementType.FAIRY]) }, - { Town.NEW_KASUTO, new Requirements([RequirementType.SEVEN_CONTAINERS]) }, - { Town.OLD_KASUTO, new Requirements([RequirementType.EIGHT_CONTAINERS]) }, - { Town.NABOORU_FOUNTAIN, new Requirements() }, - { Town.BAGU, new Requirements() }, - { Town.SARIA_TABLE, new Requirements() } - }; - */ - - public static readonly List STRICT_SPELL_LOCATIONS = [ Town.RAURU, Town.RUTO, Town.SARIA_NORTH, Town.MIDO_WEST, - Town.NABOORU, Town.DARUNIA_WEST, Town.NEW_KASUTO, Town.OLD_KASUTO ]; - - public static readonly List ITEM_LOCATION_TOWNS = [ Town.RAURU, Town.RUTO, Town.SARIA_NORTH, Town.MIDO_WEST, - Town.NABOORU, Town.DARUNIA_WEST, Town.NEW_KASUTO, Town.OLD_KASUTO, - Town.MIDO_CHURCH, Town.DARUNIA_ROOF, - Town.SARIA_TABLE, Town.NABOORU_FOUNTAIN, Town.BAGU]; -} diff --git a/RandomizerCore/Util.cs b/RandomizerCore/Util.cs index 7d97494be..c4564457d 100644 --- a/RandomizerCore/Util.cs +++ b/RandomizerCore/Util.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using NLog; using Z2Randomizer.RandomizerCore.Overworld; -using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore; @@ -98,17 +98,6 @@ public static string FromGameText(IEnumerable bytes) return output.ToString(); } - public static void Swap(Location p1, Location p2) - { - //(p2.Continent, p1.Continent) = (p1.Continent, p2.Continent); - (p2.Map, p1.Map) = (p1.Map, p2.Map); - (p2.PalaceNumber, p1.PalaceNumber) = (p1.PalaceNumber, p2.PalaceNumber); - - (p2.ActualTown, p1.ActualTown) = (p1.ActualTown, p2.ActualTown); - (p2.Collectables, p1.Collectables) = (p1.Collectables, p2.Collectables); - (p2.Name, p1.Name) = (p1.Name, p2.Name); - } - public static string ByteArrayToHexString(byte[] bytes) { string hex = BitConverter.ToString(bytes); diff --git a/Statistics/Result.cs b/Statistics/Result.cs index 9790d7dd9..793edde78 100644 --- a/Statistics/Result.cs +++ b/Statistics/Result.cs @@ -6,6 +6,7 @@ using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Overworld; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.Statistics; diff --git a/Statistics/Statistics.cs b/Statistics/Statistics.cs index 11d2b74d9..ee4e5f4d1 100644 --- a/Statistics/Statistics.cs +++ b/Statistics/Statistics.cs @@ -6,6 +6,7 @@ using Desktop.Common; using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.Statistics; diff --git a/Tests/FlagsTests.cs b/Tests/FlagsTests.cs index af39ddb85..787754304 100644 --- a/Tests/FlagsTests.cs +++ b/Tests/FlagsTests.cs @@ -207,7 +207,7 @@ public void SharedSeedExportIgnoresDifficultyOnlySettings() EnemyXPDrops = XPEffectiveness.NONE }; - RandomizerProperties properties = config.Export(new Random(1234), includeDifficulty: false); + RandomizerProperties properties = config.Export(new RandomizerCore.Random(1234), includeDifficulty: false); Assert.IsFalse(properties.StartCandle); Assert.IsFalse(properties.StartCross); diff --git a/Tests/RoomSerializationTests.cs b/Tests/RoomSerializationTests.cs index 7a772db34..a154a39e0 100644 --- a/Tests/RoomSerializationTests.cs +++ b/Tests/RoomSerializationTests.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.Tests; diff --git a/Tests/Utils.cs b/Tests/Utils.cs index ff526a1cc..6a9f322c0 100644 --- a/Tests/Utils.cs +++ b/Tests/Utils.cs @@ -5,6 +5,7 @@ using System.Text.RegularExpressions; using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.Tests; diff --git a/ValidateRooms/Program.cs b/ValidateRooms/Program.cs index a005b7101..7273bff1d 100644 --- a/ValidateRooms/Program.cs +++ b/ValidateRooms/Program.cs @@ -6,6 +6,7 @@ using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Enemy; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; using static Z2Randomizer.RandomizerCore.Util; StringBuilder sb = new StringBuilder(""); diff --git a/Z2Randomizer.sln b/Z2Randomizer.sln index 22545c485..470f451a7 100644 --- a/Z2Randomizer.sln +++ b/Z2Randomizer.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.33530.505 +# Visual Studio Version 18 +VisualStudioVersion = 18.7.11911.148 stable MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RandomizerCore", "RandomizerCore\RandomizerCore.csproj", "{34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}" ProjectSection(ProjectDependencies) = postProject @@ -60,8 +60,8 @@ Global {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Release|Any CPU.Build.0 = Release|Any CPU {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Release|x64.ActiveCfg = Release|x64 {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Release|x64.Build.0 = Release|x64 - {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Unsafe Debug|Any CPU.ActiveCfg = Unsafe Debug|Any CPU - {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Unsafe Debug|Any CPU.Build.0 = Unsafe Debug|Any CPU + {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Unsafe Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Unsafe Debug|Any CPU.Build.0 = Debug|Any CPU {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Unsafe Debug|x64.ActiveCfg = Unsafe Debug|Any CPU {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Unsafe Debug|x64.Build.0 = Unsafe Debug|Any CPU {45FE0E64-60F2-47CF-AE93-FD5A41FF8F52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -180,8 +180,8 @@ Global {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Release|Any CPU.Build.0 = Release|Any CPU {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Release|x64.ActiveCfg = Release|Any CPU {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Release|x64.Build.0 = Release|Any CPU - {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Unsafe Debug|Any CPU.ActiveCfg = Unsafe Debug|Any CPU - {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Unsafe Debug|Any CPU.Build.0 = Unsafe Debug|Any CPU + {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Unsafe Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Unsafe Debug|Any CPU.Build.0 = Debug|Any CPU {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Unsafe Debug|x64.ActiveCfg = Unsafe Debug|Any CPU {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Unsafe Debug|x64.Build.0 = Unsafe Debug|Any CPU {27087DD8-40ED-60F9-0765-BA784D146BAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU From 6de2d53be3f0362ac8bde32c58776fb3e0277b91 Mon Sep 17 00:00:00 2001 From: Ellendar Date: Thu, 25 Jun 2026 16:56:55 -0400 Subject: [PATCH 02/10] Fixed custom text --- RandomizerCore/CustomTexts.cs | 1 + RandomizerCore/Hyrule.cs | 2 +- RandomizerCore/Sidescroll/Town/Town.cs | 1 + RandomizerCore/SpritePatcher.cs | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index 22a5c6e2e..939c805e4 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -124,6 +124,7 @@ public class CustomTexts { TownType.DARUNIA, 81 }, { TownType.NEW_KASUTO, 93 }, { TownType.OLD_KASUTO, 96 }, + { TownType.BAGU, baguTextIndex } }; private static readonly Dictionary wizardTextIndexesBySpell = new() diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index b78dde4ed..0ae6098f1 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -2705,7 +2705,7 @@ private void UpdateRom() { // Use the old spell menu behavior where the spells are placed in the menu in the location it came from var wizardCollectables = AllLocations() - .Where(l => l.Town?.GetWizard() != null) + .Where(l => l.Town?.GetWizard() != null && l != westHyrule.sariaSouth && l != westHyrule.bagu) .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); for (int i = 0; i < magFunction.Length; i++) diff --git a/RandomizerCore/Sidescroll/Town/Town.cs b/RandomizerCore/Sidescroll/Town/Town.cs index 262898cbf..6c127908c 100644 --- a/RandomizerCore/Sidescroll/Town/Town.cs +++ b/RandomizerCore/Sidescroll/Town/Town.cs @@ -186,6 +186,7 @@ public bool ReplaceCollectable(Collectable toReplace, Collectable replacement) TownType.DARUNIA => VanillaTownMap.DARUNIA_WIZARD, TownType.NEW_KASUTO => VanillaTownMap.NEW_KASUTO_WIZARD, TownType.OLD_KASUTO => VanillaTownMap.OLD_KASUTO_WIZARD, + TownType.BAGU => VanillaTownMap.BAGU_INDOORS, _ => throw new ImpossibleException("Unrecognized town type in GetWizard()") }; return GetTownMap(map); diff --git a/RandomizerCore/SpritePatcher.cs b/RandomizerCore/SpritePatcher.cs index 3e4a4c5c4..8ce9d6a40 100644 --- a/RandomizerCore/SpritePatcher.cs +++ b/RandomizerCore/SpritePatcher.cs @@ -117,7 +117,7 @@ public static void PatchSpriteSanitized(byte[] romData, byte[] ipsData, bool exp else { byte oldValue = romData[tgtOffs + i]; - logger.Error($"Moderating IPS patch at address: 0x{addr.ToString("x5")} from 0x{oldValue.ToString("x2")} to 0x{newValue.ToString("x2")}"); + logger.Trace($"Moderating IPS patch at address: 0x{addr.ToString("x5")} from 0x{oldValue.ToString("x2")} to 0x{newValue.ToString("x2")}"); } } if (fillValue is null) From 62de4d592f56b113fce0b3015577a50f523e9c20 Mon Sep 17 00:00:00 2001 From: Ellendar Date: Fri, 3 Jul 2026 18:53:48 -0400 Subject: [PATCH 03/10] Moderate rework of item shuffle handling. Almost certainly still buggy. --- RandomizerCore/CustomTexts.cs | 8 +- RandomizerCore/Hyrule.cs | 279 ++++++++------------- RandomizerCore/Overworld/EastHyrule.cs | 6 +- RandomizerCore/Overworld/Location.cs | 24 +- RandomizerCore/Overworld/LocationID.cs | 4 +- RandomizerCore/Overworld/WestHyrule.cs | 16 +- RandomizerCore/Overworld/World.cs | 14 +- RandomizerCore/ROM.cs | 56 ----- RandomizerCore/Sidescroll/Palace/Palace.cs | 3 +- RandomizerCore/Sidescroll/Town/Town.cs | 4 +- 10 files changed, 167 insertions(+), 247 deletions(-) diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index 939c805e4..a1804f790 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -506,7 +506,6 @@ public CustomTexts(List texts) public static List GenerateTexts( IEnumerable locations, - IEnumerable itemLocations, List texts, RandomizerProperties props, Random hashRNG) @@ -603,7 +602,7 @@ public static List GenerateTexts( if (props.HelpfulHints) { - List placedIndexes = GenerateHelpfulHints(texts, itemLocations, hashRNG, props); + List placedIndexes = GenerateHelpfulHints(texts, locations, hashRNG, props); GenerateKnowNothings(texts, placedIndexes, nonhashRNG, props.BagusWoods, props.UseCommunityText); } @@ -686,7 +685,10 @@ private static void GenerateTownNameHints(List texts, IEnumerable i.Town?.GetWizard() != null)) { - texts[TOWN_SIGN_INDEXES[(TownType)location.Town!.Type!]] = GenerateTownSignHint((Collectable)location.Town.GetWizard()!.Collectable!, linkedFire); + if(location?.Town?.Type != null && TOWN_SIGN_INDEXES.TryGetValue((TownType)location?.Town?.Type!, out int index)) + { + texts[index] = GenerateTownSignHint((Collectable)location.Town.GetWizard()!.Collectable!, linkedFire); + } } } private static Text GenerateTownSignHint(Collectable spell, bool linkedFire) diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 0ae6098f1..5f6690861 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -6,9 +6,11 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; @@ -83,8 +85,6 @@ public class Hyrule public HashSet reachableAreas; //Which vanilla spell corresponds with which shuffled spell //private Dictionary WizardCollectables { get; set; } - //Locations that contain an item - private List itemLocs; //Locations that are pbags in vanilla that are turned into hearts because maxhearts - startinghearts > 4 private List pbagHearts; //Continent connectors @@ -121,7 +121,7 @@ public class Hyrule //DEBUG/STATS #pragma warning disable CS0414 // Field is assigned but its value is never used - private static int DEBUG_THRESHOLD = 1; + private static int DEBUG_THRESHOLD = 100; #pragma warning restore CS0414 // Field is assigned but its value is never used public DateTime startTime = DateTime.Now; // public DateTime startRandomizeStartingValuesTimestamp; @@ -369,7 +369,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC List? customSpellOrder = props.IncludeSpellsInShuffle ? null : AllLocations() - .Where(l => l.Town?.GetWizard() != null) + .Where(l => l.Town?.GetWizard() != null && l != westHyrule.bagu && l != westHyrule.sariaSouth) .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); ROMData.CombineFireSpell(assembler, customSpellOrder, r); } @@ -402,7 +402,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC if (ct.IsCancellationRequested) { return new RandomizerResult(false); } UpdateProgress(progress, 9); - List texts = CustomTexts.GenerateTexts(AllLocations(), itemLocs, ROMData.GetGameText(), props, r); + List texts = CustomTexts.GenerateTexts(AllLocations(), ROMData.GetGameText(), props, r); StatRandomizer randomizedStats = new(ROMData, props); randomizedStats.Randomize(r, skipDifficultyOnly: shareSeedAcrossDifficulty); @@ -651,7 +651,7 @@ private void ReplaceDifficultyStartingItemsInWorld(Random r) { if (!props.StartsWithCollectable(item)) { continue; } Collectable replacementItem = minorItems.Sample(r); - foreach (Location location in itemLocs) + foreach (Location location in AllLocations().Where(i => i.GetCollectableCount() > 0)) { if(location.ReplaceCollectable(item, replacementItem)) { @@ -683,6 +683,7 @@ private void ShufflePalaces() private void ShuffleItems() { List shufflableItems = []; + List globalShuffleLocations = []; List minorItems = [Collectable.BLUE_JAR, Collectable.RED_JAR, Collectable.SMALL_BAG, Collectable.MEDIUM_BAG, Collectable.LARGE_BAG, Collectable.XL_BAG, Collectable.ONEUP, Collectable.KEY]; @@ -700,6 +701,7 @@ private void ShuffleItems() collectable = (Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE); eastHyrule.pbagCave2.SetCollectables([collectable]); shufflableItems.Add(collectable); + globalShuffleLocations.Add([westHyrule.pbagCave, eastHyrule.pbagCave1, eastHyrule.pbagCave2]); } else { @@ -708,7 +710,7 @@ private void ShuffleItems() eastHyrule.pbagCave2.SetCollectables([Collectable.XL_BAG]); } - if(props.IncludeSpellsInShuffle) + if (props.IncludeSpellsInShuffle) { shufflableItems.Add(Collectable.SHIELD_SPELL); shufflableItems.Add(Collectable.JUMP_SPELL); @@ -718,37 +720,32 @@ private void ShuffleItems() shufflableItems.Add(Collectable.REFLECT_SPELL); shufflableItems.Add(Collectable.SPELL_SPELL); shufflableItems.Add(Collectable.THUNDER_SPELL); + globalShuffleLocations.Add([westHyrule.rauru, westHyrule.ruto, westHyrule.sariaNorth, westHyrule.mido, + eastHyrule.nabooru, eastHyrule.darunia, eastHyrule.newKasuto, eastHyrule.oldKasuto]); } - else if(props.ShuffleSpellLocations) + else if (props.ShuffleSpellLocations) { ShuffleSpells(); } - int mirrorIndex = 0, waterindex = 0; if (props.IncludeQuestItemsInShuffle) { shufflableItems.Add(Collectable.BAGUS_NOTE); - if (props.StartWithSpellItems) - { - shufflableItems.Add(minorItems[r.Next(minorItems.Count)]); - shufflableItems.Add(minorItems[r.Next(minorItems.Count)]); - } - else - { - mirrorIndex = shufflableItems.Count; - shufflableItems.Add(Collectable.MIRROR); - waterindex = shufflableItems.Count; - shufflableItems.Add(Collectable.WATER); - } + shufflableItems.Add(Collectable.MIRROR); + shufflableItems.Add(Collectable.WATER); + globalShuffleLocations.AddRange([westHyrule.bagu, westHyrule.sariaNorth, eastHyrule.nabooru]); } if (props.IncludeSwordTechsInShuffle) { - shufflableItems.Add(Collectable.UPSTAB); - shufflableItems.Add(Collectable.DOWNSTAB); + TownMap? townMap = westHyrule.mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER); + shufflableItems.Add(props.StartWithDownstab ? minorItems.Sample(r) : Collectable.DOWNSTAB); + globalShuffleLocations.Add(westHyrule.sariaNorth); + townMap = eastHyrule.darunia.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER); + shufflableItems.Add(props.StartWithUpstab ? minorItems.Sample(r) : Collectable.UPSTAB); + globalShuffleLocations.Add(eastHyrule.darunia); } - - else if(props.SwapUpAndDownStab) + else if (props.SwapUpAndDownStab) { SwapUpAndDownstab(); } @@ -756,22 +753,8 @@ private void ShuffleItems() int heartContainersInItemPool = props.MaxHearts - props.StartHearts; int magicContainersInItemPool = props.MaxMagicContainers - props.StartMagicContainers; - foreach (Collectable item in ItemGet.Keys.ToList()) - { - ItemGet[item] = false; - } - - //TODO: Refactor these puts out of this class - ROMData.Put(RomMap.START_CANDLE, props.StartCandle ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_GLOVE, props.StartGlove ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_RAFT, props.StartRaft ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_BOOTS, props.StartBoots ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_FLUTE, props.StartFlute ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_CROSS, props.StartCross ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_HAMMER, props.StartHammer ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_MAGICAL_KEY, props.StartKey ? (byte)1 : (byte)0); - foreach(Collectable collectable in ItemGet.Keys) + foreach (Collectable collectable in ItemGet.Keys) { ItemGet[collectable] = props.StartsWithCollectable(collectable); } @@ -789,7 +772,7 @@ private void ShuffleItems() List collectables = hcLocation.GetAllCollectables(); List order = Enumerable.Range(0, collectables.Count).ToList(); order.FisherYatesShuffle(r); - foreach(int index in order) + foreach (int index in order) { if (collectables[index] == Collectable.HEART_CONTAINER) { @@ -834,64 +817,35 @@ private void ShuffleItems() } } + //Spell Item handling if (props.StartWithSpellItems) { - Debug.Assert(shufflableItems[9] == Collectable.MEDICINE); - Debug.Assert(shufflableItems[10] == Collectable.TROPHY); - Debug.Assert(shufflableItems[17] == Collectable.CHILD); - shufflableItems[9] = minorItems[r.Next(minorItems.Count)]; - shufflableItems[10] = minorItems[r.Next(minorItems.Count)]; - shufflableItems[17] = minorItems[r.Next(minorItems.Count)]; - ItemGet[Collectable.TROPHY] = true; - ItemGet[Collectable.MEDICINE] = true; - ItemGet[Collectable.CHILD] = true; - ItemGet[Collectable.MIRROR] = true; - ItemGet[Collectable.WATER] = true; - } - else if(!props.IncludeSpellsInShuffle) - { - Collectable wizardCollectable; - //wizardCollectable = westHyrule.AllLocations.First(i => i.ActualTown == TownType.RUTO).Collectables[0]; - wizardCollectable = (Collectable)westHyrule.ruto.Town!.GetWizard()!.Collectable!; - if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) + Collectable[] spellItems = [Collectable.TROPHY, Collectable.MEDICINE, Collectable.CHILD, Collectable.MIRROR, Collectable.WATER]; + foreach (Collectable spellItem in spellItems) { - Debug.Assert(shufflableItems[10] == Collectable.TROPHY); - shufflableItems[10] = minorItems[r.Next(minorItems.Count)]; - ItemGet[Collectable.TROPHY] = true; - } - - wizardCollectable = (Collectable)westHyrule.mido.Town!.GetWizard()!.Collectable!; - if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) - { - Debug.Assert(shufflableItems[9] == Collectable.MEDICINE); - shufflableItems[9] = minorItems[r.Next(minorItems.Count)]; - ItemGet[Collectable.MEDICINE] = true; + int index = shufflableItems.IndexOf(spellItem); + if (index > -1) + { + shufflableItems[index] = minorItems.Sample(r); + } + ItemGet[spellItem] = true; } - - wizardCollectable = (Collectable)eastHyrule.darunia.Town!.GetWizard()!.Collectable!; - if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) + } + else if (!props.IncludeSpellsInShuffle) + { + List spellItemTowns = [westHyrule.ruto, westHyrule.mido, eastHyrule.darunia]; + if (props.IncludeQuestItemsInShuffle) { - Debug.Assert(shufflableItems[17] == Collectable.CHILD); - shufflableItems[17] = minorItems[r.Next(minorItems.Count)]; - ItemGet[Collectable.CHILD] = true; + spellItemTowns.Add([westHyrule.sariaNorth, eastHyrule.nabooru]); } - if (props.IncludeQuestItemsInShuffle) + foreach (Location location in spellItemTowns) { - wizardCollectable = (Collectable)westHyrule.sariaNorth.Town!.GetWizard()!.Collectable!; - if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) + Collectable wizardCollectable = (Collectable)location.Town!.GetWizard()!.Collectable!; + if (props.StartsWithCollectable(wizardCollectable)) { - Debug.Assert(shufflableItems[mirrorIndex] == Collectable.MIRROR); - shufflableItems[mirrorIndex] = minorItems[r.Next(minorItems.Count)]; - ItemGet[Collectable.MIRROR] = true; - } - - wizardCollectable = (Collectable)eastHyrule.nabooru.Town!.GetWizard()!.Collectable!; - if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) - { - Debug.Assert(shufflableItems[waterindex] == Collectable.WATER); - shufflableItems[waterindex] = minorItems[r.Next(minorItems.Count)]; - ItemGet[Collectable.WATER] = true; + ItemGet[wizardCollectable] = true; + location.Town!.GetWizard()!.Collectable = minorItems.Sample(r); } } } @@ -916,15 +870,19 @@ private void ShuffleItems() Collectable.SPELL_SPELL, Collectable.THUNDER_SPELL]; - foreach(Collectable item in possibleStartItems) + foreach (Collectable item in possibleStartItems) { - if(props.StartsWithCollectable(item)) + if (props.StartsWithCollectable(item)) { - shufflableItems[shufflableItems.IndexOf(item)] = minorItems.Sample(r); + int index = shufflableItems.IndexOf(item); + if (index > -1) + { + shufflableItems[index] = minorItems.Sample(r); + } } } - if(props.IncludeSpellsInShuffle) + if (props.IncludeSpellsInShuffle) { foreach (Collectable item in possibleStartSpells) { @@ -935,31 +893,7 @@ private void ShuffleItems() } } - TownMap? townMap = itemLocs.SelectMany(i => i.Town?.TownMaps ?? []).FirstOrDefault(i => i.Map == (int)VanillaTownMap.MIDO_TRAINER); - if(townMap != null) - { - townMap.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; - } - townMap = itemLocs.SelectMany(i => i.Town?.TownMaps ?? []).FirstOrDefault(i => i.Map == (int)VanillaTownMap.DARUNIA_TRAINER); - if (townMap != null) - { - townMap.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; - } - - if (props.IncludeSwordTechsInShuffle) - { - if (props.StartWithDownstab) - { - shufflableItems[shufflableItems.IndexOf(Collectable.DOWNSTAB)] = minorItems.Sample(r); - } - if (props.StartWithUpstab) - { - shufflableItems[shufflableItems.IndexOf(Collectable.UPSTAB)] = minorItems.Sample(r); - } - } - //Handle excess items - List excessItems = []; //Heart containers over 4 are excess @@ -967,19 +901,18 @@ private void ShuffleItems() { excessItems.Add(Collectable.HEART_CONTAINER); } - + //Magic containers over 4 are excess for (int i = 4; i < magicContainersInItemPool; i++) { excessItems.Add(Collectable.MAGIC_CONTAINER); } - int extraPalaceItemCount = props.PalaceItemRoomCounts.Select(c => Math.Max(c - 1, 0)).Sum(); + int extraPalaceItemCount = props.PalaceItemRoomCounts.Sum(c => Math.Max(c - 1, 0)); for (int i = 0; i < extraPalaceItemCount; i++) { shufflableItems.Add(minorItems.Sample(r)); } - List vanillaPalaceItems = [Collectable.CANDLE, Collectable.GLOVE, Collectable.RAFT, Collectable.BOOTS, Collectable.FLUTE, Collectable.CROSS]; //No palace items handling @@ -1029,7 +962,8 @@ private void ShuffleItems() for(int i = 0; i < overflowLocationsRequired; i++) { Location overflowLocation = overflowLocations.Sample(r)!; - itemLocs.Add(overflowLocation); + overflowLocation.CollectablesAreShufflable = true; + globalShuffleLocations.Add(overflowLocation); shufflableItems.Add(Collectable.SMALL_BAG); minorItemIndexes.Add(shufflableItems.Count - 1); overflowLocations.Remove(overflowLocation); @@ -1051,8 +985,9 @@ private void ShuffleItems() if (props.MixOverworldPalaceItems) { - duplicateItemPlacementCandidates.AddRange(itemLocs); - DoShuffle(shufflableItems, itemLocs); + + duplicateItemPlacementCandidates.AddRange(globalShuffleLocations); + DoShuffle(shufflableItems, globalShuffleLocations); } else { @@ -1063,11 +998,11 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location palaceLocation in itemLocs.Where(i => i.Palace != null)) + foreach (Location palaceLocation in AllLocations().Where(i => i.Palace != null)) { shufflableItemLocations.Add(palaceLocation); Collectable vanillaCollectable = palaceLocation.Palace!.GetVanillaCollectable(); - itemsToActuallyShuffle.Add(shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)); + itemsToActuallyShuffle.Add(props.StartsWithCollectable(vanillaCollectable) ? minorItems.Sample(r) : vanillaCollectable); for (int i = 1; i < props.PalaceItemRoomCounts[(int)palaceLocation.Palace.Number! - 1]; i++) { @@ -1079,7 +1014,7 @@ private void ShuffleItems() } else //no shuffle palace items { - foreach (Location palaceLocation in itemLocs.Where(i => i.Palace != null && i.Palace.Number < 7)) + foreach (Location palaceLocation in AllLocations().Where(i => i.Palace != null && i.Palace.Number < 7)) { int palaceNumber = palaceLocation.Palace!.Number; Collectable vanillaCollectable = palaceLocation.Palace!.GetVanillaCollectable(); @@ -1101,7 +1036,7 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location nonPalaceLocation in itemLocs.Where(i => i.Palace == null)) + foreach (Location nonPalaceLocation in AllLocations().Where(i => i.Palace == null)) { shufflableItemLocations.Add(nonPalaceLocation); foreach(Collectable collectable in nonPalaceLocation.GetAllCollectables()) @@ -1114,7 +1049,7 @@ private void ShuffleItems() } else { - foreach (Location nonPalaceLocation in itemLocs.Where(i => i.Palace == null)) + foreach (Location nonPalaceLocation in AllLocations().Where(i => i.Palace == null)) { List collectables = nonPalaceLocation.GetAllCollectables(); for(int i = 0; i < collectables.Count; i++) @@ -1206,10 +1141,9 @@ private void ShuffleItems() importantItemsToDuplicate = importantItemsToDuplicate.Where(shufflableItems.Contains).ToList(); - minorItemLocations = duplicateItemPlacementCandidates.Where(i => i.GetAllCollectables().Any(c => c.IsMinorItem())).ToList(); - int replaceableMinorItemCount = duplicateItemPlacementCandidates.Sum(l => l.GetAllCollectables().Count(c => c.IsMinorItem())); + minorItemLocations = duplicateItemPlacementCandidates.Where(i => i.GetShufflableCollectables().Any(c => c.IsMinorItem())).ToList(); + int replaceableMinorItemCount = duplicateItemPlacementCandidates.Sum(l => l.GetShufflableCollectables().Count(c => c.IsMinorItem())); importantItemsToDuplicate = importantItemsToDuplicate.GetRange(0, int.Min(replaceableMinorItemCount, importantItemsToDuplicate.Count)); - importantItemsToDuplicate.FisherYatesShuffle(r); minorItemLocations.FisherYatesShuffle(r); @@ -1217,7 +1151,7 @@ private void ShuffleItems() { Location minorItemLocation = minorItemLocations.Sample(r)!; List locationCollectables = minorItemLocation.GetAllCollectables(); - List collectableIndexes = Enumerable.Range(0, minorItemLocation.GetAllCollectables().Count()).ToList(); + List collectableIndexes = Enumerable.Range(0, minorItemLocation.GetCollectableCount()).ToList(); collectableIndexes.FisherYatesShuffle(r); foreach(int collectableIndex in collectableIndexes) { @@ -1238,7 +1172,8 @@ private void ShuffleItems() private void DoShuffle(List itemsToShuffle, List itemShuffleLocations) { - int possibleItemPlacements = itemShuffleLocations.Sum(i => i.GetAllCollectables().Count); + itemShuffleLocations = itemShuffleLocations.Distinct().ToList(); + int possibleItemPlacements = itemShuffleLocations.Sum(i => i.GetCollectableCount(true)); if (itemsToShuffle.Count != possibleItemPlacements) { @@ -1251,10 +1186,10 @@ private void DoShuffle(List itemsToShuffle, List itemShuf int itemIndex = 0; foreach(Location location in itemShuffleLocations) { - int itemCount = location.GetAllCollectables().Count; - List collectables = itemsToShuffle.GetRange(itemIndex, itemCount); - itemIndex += itemCount; - location.SetCollectables(collectables); + int collectableCount = location.GetCollectableCount(true); + List newCollectables = itemsToShuffle.GetRange(itemIndex, collectableCount); + itemIndex += collectableCount; + location.SetCollectables(newCollectables); } } @@ -1422,22 +1357,22 @@ private bool IsEverythingReachable(Dictionary itemGet) foreach (World world in worlds) { - if (world.raft != null && CanGet(world.raft) && itemGet[Collectable.RAFT]) + if (world.raft != null && world.raft.Reachable && itemGet[Collectable.RAFT]) { worlds.ForEach(i => i.VisitRaft()); } - if (world.bridge != null && CanGet(world.bridge)) + if (world.bridge != null && world.bridge.Reachable) { worlds.ForEach(i => i.VisitBridge()); } - if (world.cave1 != null && CanGet(world.cave1)) + if (world.cave1 != null && world.cave1.Reachable) { worlds.ForEach(i => i.VisitCave1()); } - if (world.cave2 != null && CanGet(world.cave2)) + if (world.cave2 != null && world.cave2.Reachable) { worlds.ForEach(i => i.VisitCave2()); } @@ -1481,6 +1416,7 @@ private bool IsEverythingReachable(Dictionary itemGet) PrintRoutingDebug(reachableLocationsCount, wh, eh, dm, mi); //Debug.WriteLine(GenerateSpoiler()); //Debug.WriteLine(westHyrule.GetMapDebug()); + //XXX: return false; } //#endif @@ -1543,10 +1479,7 @@ private bool CanGet(IEnumerable locations) { return locations.All(i => i.Reachable); } - private bool CanGet(Location location) - { - return location.Reachable; - } + private void ShortenWizards() { //Change the exit leading to the intermedaite wizard room to go directly to the basement @@ -1603,7 +1536,7 @@ private int UpdateItemGets(RandomizerProperties props) accessibleMagicContainers = props.StartMagicContainers + magicContainerGetLocations.Count; requireables = GetRequireables(props); gottenItems = []; - foreach (Location location in AllLocations()) + foreach (Location location in AllLocations().Where(i => i.Reachable)) { List gettableItems = location.GetGettableItems(requireables); @@ -2036,7 +1969,6 @@ private async Task ProcessOverworld(Func progress, CancellationTok westHyrule.SetStart(); ShufflePalaces(); - LoadItemLocs(props.PalaceItemRoomCounts); ShuffleItems(); @@ -2153,8 +2085,9 @@ private bool AllContinentsAreConnected((Location, Location)[] connections) return continentsFound.Count == 4; } - //ItemLocs is specifically only those locations that contain shufflable items - //If you're looking for a global reference for which items are where... too bad it doesn't exist :( + //ItemLocs is no longer a global reference (and probably shouldn't have been to begin with). + //It's possible this makes a return at some point just for efficency but for now it's dead Jim + /* private List LoadItemLocs(int[] itemsPerPalaces) { List GetPalacesWithItems(IEnumerable palaces) => @@ -2236,6 +2169,7 @@ List GetPalacesWithItems(IEnumerable palaces) => return itemLocs; } + */ /// /// Shuffles which spells are in which towns. @@ -2308,6 +2242,7 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra shuffler.ShuffleBossDrop(rom, r, a); } + //neither of these starting item handlings really belong here, but this is good enough. if (props.StartWithSpellItems) { //ROMData.Put(0xF584, 0xA9); @@ -2320,6 +2255,14 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra rom.Put(0x17b17, 0x01); //Water rom.Put(0x17b18, 0x20); //Child } + rom.Put(RomMap.START_CANDLE, props.StartCandle ? (byte)1 : (byte)0); + rom.Put(RomMap.START_GLOVE, props.StartGlove ? (byte)1 : (byte)0); + rom.Put(RomMap.START_RAFT, props.StartRaft ? (byte)1 : (byte)0); + rom.Put(RomMap.START_BOOTS, props.StartBoots ? (byte)1 : (byte)0); + rom.Put(RomMap.START_FLUTE, props.StartFlute ? (byte)1 : (byte)0); + rom.Put(RomMap.START_CROSS, props.StartCross ? (byte)1 : (byte)0); + rom.Put(RomMap.START_HAMMER, props.StartHammer ? (byte)1 : (byte)0); + rom.Put(RomMap.START_MAGICAL_KEY, props.StartKey ? (byte)1 : (byte)0); rom.Put(ROM.ChrRomOffset + 0x1a000, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.item_sprites.chr")); //Linked fire/dash custom sprites replace fire's sprite. This lets us free up c7 for future use. @@ -2997,18 +2940,6 @@ public string GenerateSpoiler() //sb.AppendLine(); } - //sb.AppendLine("West:\n"); - //sb.AppendLine(westHyrule.GetReadableMap()); - - //sb.AppendLine("\nEast:\n"); - //sb.AppendLine(eastHyrule.GetReadableMap()); - - //sb.AppendLine("\nDeath Mountain:\n"); - //sb.AppendLine(deathMountain.GetReadableMap()); - - //sb.AppendLine("\nMaze Island:\n"); - //sb.AppendLine(mazeIsland.GetReadableMap()); - for(int i = 0; i < 6; i++) { sb.AppendLine($"\nPalace {i+1}:\n"); @@ -3102,19 +3033,31 @@ public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) sb.AppendLine("eh: " + eh + " / " + eastHyrule.AllLocations.Count); sb.AppendLine("dm: " + dm + " / " + deathMountain.AllLocations.Count); sb.AppendLine("mi: " + mi + " / " + mazeIsland.AllLocations.Count); - sb.AppendLine(""); + sb.AppendLine(); + sb.AppendLine("Missing Items:"); //Where(i => !i.ItemGet && !i.Collectable.IsInternalUse() - foreach (Location location in AllLocations()) + bool itemWasMissing = false; + foreach (Location location in AllLocations().Where(l => l.GetAllCollectables().Any(i => !i.IsMinorItem() && !ItemGet[i]))) { - foreach (Collectable collectable in location.GetAllCollectables().Where(i => !i.IsMinorItem() && !ItemGet[i])) + sb.AppendLine($"{location.Name} / [{String.Join(", ", location.GetAllCollectables() + .Where(i => !i.IsMinorItem() && !ItemGet[i]) + .Select(i => Enum.GetName(i)!))}]"); + itemWasMissing = true; + } + + if(!itemWasMissing) + { + sb.AppendLine("Missing Non-Item locations (this should have been caught by ValidateBasicRouting!"); + foreach (Location location in AllLocations().Where(l => !l.Reachable)) { - sb.AppendLine($"{location.Name} / [{String.Join(", ", location.GetAllCollectables().Select(i => Enum.GetName(i)!))}]"); + sb.AppendLine(location.Name); } } + sb.AppendLine(); + - sb.AppendLine(""); - logger.Error(sb.ToString()); +logger.Error(sb.ToString()); } private bool IsRaftAlwaysRequired(RandomizerProperties props) diff --git a/RandomizerCore/Overworld/EastHyrule.cs b/RandomizerCore/Overworld/EastHyrule.cs index a0c74b13d..b57430334 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -2004,9 +2004,9 @@ public override string GenerateSpoiler() StringBuilder sb = new(); sb.AppendLine("EAST: "); sb.AppendLine("Nabooru: \n" + nabooru.Town!.GenerateSpoiler()); - sb.AppendLine("Darunia: \n" + nabooru.Town!.GenerateSpoiler()); - sb.AppendLine("New Kasuto: \n" + nabooru.Town!.GenerateSpoiler()); - sb.AppendLine("Old Kasuto: \n" + nabooru.Town!.GenerateSpoiler()); + sb.AppendLine("Darunia: \n" + darunia.Town!.GenerateSpoiler()); + sb.AppendLine("New Kasuto: \n" + newKasuto.Town!.GenerateSpoiler()); + sb.AppendLine("Old Kasuto: \n" + oldKasuto.Town!.GenerateSpoiler()); sb.AppendLine("\tRisen Pbag Cave: " + pbagCave2.GetAllCollectables()[0].EnglishText()); sb.AppendLine("\tSunken Pbag Cave: " + pbagCave1.GetAllCollectables()[0].EnglishText()); diff --git a/RandomizerCore/Overworld/Location.cs b/RandomizerCore/Overworld/Location.cs index 142bd9b74..6ec3b596a 100644 --- a/RandomizerCore/Overworld/Location.cs +++ b/RandomizerCore/Overworld/Location.cs @@ -307,18 +307,21 @@ public List GetGettableItems(List requireables, Di return AccessRequirements.AreSatisfiedBy(requireables) ? NonPalaceTownCollectables : []; } - public void SetCollectables(IEnumerable collectables) + public void SetCollectables(IEnumerable collectables, bool shufflableOnly = false) { Debug.Assert(Town == null || Palace == null); if (Town != null) { - Town.SetCollectables(collectables); + Town.SetCollectables(collectables, shufflableOnly); } else if (Palace != null) { - Palace.SetCollectables(collectables); + Palace.SetCollectables(collectables, shufflableOnly); + } + else if (!shufflableOnly || CollectablesAreShufflable) + { + NonPalaceTownCollectables = [.. collectables]; } - else NonPalaceTownCollectables = [.. collectables]; } /// @@ -345,4 +348,17 @@ public bool ReplaceCollectable(Collectable toReplace, Collectable replacement) } return false; } + + public int GetCollectableCount(bool shufflableOnly = false) + { + if (Town != null) + { + return Town.TownMaps.Count(i => i.Collectable != null && (!shufflableOnly || i.CollectableIsShufflable)); + } + else if (Palace != null) + { + return Palace.ItemRooms.Count; + } + return NonPalaceTownCollectables.Count; + } } diff --git a/RandomizerCore/Overworld/LocationID.cs b/RandomizerCore/Overworld/LocationID.cs index 7636772ea..29489bcc2 100644 --- a/RandomizerCore/Overworld/LocationID.cs +++ b/RandomizerCore/Overworld/LocationID.cs @@ -50,7 +50,7 @@ public enum LocationID WEST_DM_ENTRANCE, // 42: (10, 95) WEST_DM_EXIT, // 43: (21, 96) WEST_KINGS_TOMB, // 44: (50, 88) - WEST_TOWN_RAURO, // 45: (46, 54) + WEST_TOWN_RAURU, // 45: (46, 54) WEST_TOWN_RUTO = 47, // 47: ( 2, 36) WEST_TOWN_SARIA_SOUTH, // 48: ( 8, 91) WEST_TOWN_SARIA_NORTH, // 49: ( 8, 89) @@ -279,7 +279,7 @@ public static class LocationIDExtensions { static readonly Dictionary townMap = new() { - [LocationID.WEST_TOWN_RAURO] = TownType.RAURU, + [LocationID.WEST_TOWN_RAURU] = TownType.RAURU, [LocationID.WEST_TOWN_RUTO] = TownType.RUTO, [LocationID.WEST_TOWN_SARIA_NORTH] = TownType.SARIA, [LocationID.WEST_TOWN_SARIA_SOUTH] = TownType.SARIA, diff --git a/RandomizerCore/Overworld/WestHyrule.cs b/RandomizerCore/Overworld/WestHyrule.cs index 56c5380d3..6e8fdf1a8 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -89,7 +89,7 @@ public sealed class WestHyrule : World //{ 0x4659, terrain.cave }, // Cave to DM //{ 0x465A, terrain.cave }, // Cave from DM { LocationID.WEST_KINGS_TOMB, Terrain.GRAVE }, //44: King's tomb - { LocationID.WEST_TOWN_RAURO, Terrain.TOWN }, + { LocationID.WEST_TOWN_RAURU, Terrain.TOWN }, { LocationID.WEST_TOWN_RUTO, Terrain.TOWN }, { LocationID.WEST_TOWN_SARIA_SOUTH, Terrain.TOWN }, { LocationID.WEST_TOWN_SARIA_NORTH, Terrain.TOWN }, @@ -130,7 +130,7 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), bagu = GetLocation(LocationID.WEST_BAGU_HOUSE); //0x4661 bagu.Town = Towns.LoadVanillaTown(rom, TownType.BAGU, props.DisableMagicRecs); - rauru = GetLocation(LocationID.WEST_TOWN_RAURO); + rauru = GetLocation(LocationID.WEST_TOWN_RAURU); rauru.Town = Towns.LoadVanillaTown(rom, TownType.RAURU, props.DisableMagicRecs); ruto = GetLocation(LocationID.WEST_TOWN_RUTO); //0x465e @@ -1409,6 +1409,8 @@ public bool ValidateBasicRouting() } visitedCoordinates[y, x] = true; //if there is a location at this coordinate + List locationsAtCoord = unreachedLocations.Where(location => location.Y == y && location.Xpos == x).ToList(); + Debug.Assert(locationsAtCoord.Count <= 1); Location? here = unreachedLocations.FirstOrDefault(location => location.Y == y && location.Xpos == x); if (here != null) { @@ -1510,11 +1512,11 @@ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("WEST: "); - sb.AppendLine("Rauru: " + rauru.Town!.GenerateSpoiler()); - sb.AppendLine("Ruto: " + ruto.Town!.GenerateSpoiler()); - sb.AppendLine("Saria: " + sariaNorth.Town!.GenerateSpoiler()); - sb.AppendLine("Mido: " + mido.Town!.GenerateSpoiler()); - sb.AppendLine("Bagu: " + bagu.Town!.GenerateSpoiler()); + sb.AppendLine("Rauru: \n" + rauru.Town!.GenerateSpoiler()); + sb.AppendLine("Ruto: \n" + ruto.Town!.GenerateSpoiler()); + sb.AppendLine("Saria: \n" + sariaNorth.Town!.GenerateSpoiler()); + sb.AppendLine("Mido: \n" + mido.Town!.GenerateSpoiler()); + sb.AppendLine("Bagu: \n" + bagu.Town!.GenerateSpoiler()); sb.AppendLine("\tMagic Container Cave: " + magicContainerCave.GetAllCollectables()[0].EnglishText()); sb.AppendLine("\tTrophy Cave: " + trophyCave.GetAllCollectables()[0].EnglishText()); diff --git a/RandomizerCore/Overworld/World.cs b/RandomizerCore/Overworld/World.cs index d9ef05d07..7e3999f0e 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -425,7 +425,7 @@ protected bool PlaceLocations(Terrain riverTerrain, bool saneCaves, Location? hi location.Y = y; location.CanShuffle = false; } - else if (location.Town == null) + else { Terrain t; do @@ -3178,4 +3178,16 @@ public bool ValidateCaves() //protected abstract void SetVanillaCollectables(bool useDash); public abstract string GenerateSpoiler(); + + public bool HasCollidingLocations() + { + foreach(Location location in AllLocations) + { + if(AllLocations.Any(i => location != i && location.CoordsY30Offset == i.CoordsY30Offset)) + { + return true; + } + } + return false; + } } \ No newline at end of file diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 480e79587..5c8a0ea2f 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -16,39 +16,6 @@ namespace Z2Randomizer.RandomizerCore; -/* -Classes Needed: - -* Location - Represents entry from overworld - * Terrain type - * Requires Jump - * Requires Fairy - * Requires Hammer - * X Position - * Y Position - * Exit Location? - * Contains Item - * Enter from right - * Map Number - -* World - Represent a single map - * Location Tree - * Root of tree is starting position, children are areas directly accessible from position - * Lists of locations broken down by terrain - * Entry / Exit Points - -* Hyrule - * Does the randomization - * Checks for Sanity - * Contains links to all World objects - -* Room - Palace only? - -* Palace - * Pallette - * Enemies? - * Rooms -*/ public class ROM { public const int RomHdrSize = 0x10; @@ -576,29 +543,6 @@ public void ApplyIps(byte[] patch, bool expandRom = false) public void UpdateSprite(CharacterSprite charSprite, bool sanitize, bool changeItems) { - /* - * Dear future digshake, - * - * This method is garbage. - * - * The first entry in sprite info contains non-sprite data. - * My lazy ass just skips it. - * - * If you update the sprite structures in Graphics.cs, you could refactor this: - * - * Make lists of all the ROM locations for sprites - * Iterate through each sprite array, putting the data at the ROM locations - * - * -Past digshake (who is still too lazy to fix this method) - */ - /* - * Dear future Ellendar, - * This method is still garbage, but better. - * Eventually we need to create a data structure that represents the individual properies of things like sprites - * that we can save in external files so they can be imported from a simple format people can use to customize all - * the cosmetics. - */ - if (charSprite.Patch != null) { if (sanitize) { diff --git a/RandomizerCore/Sidescroll/Palace/Palace.cs b/RandomizerCore/Sidescroll/Palace/Palace.cs index 82859220f..66b6a6e5b 100644 --- a/RandomizerCore/Sidescroll/Palace/Palace.cs +++ b/RandomizerCore/Sidescroll/Palace/Palace.cs @@ -1191,8 +1191,9 @@ public List GetGettableItems(IEnumerable initialRe return gettableItems; } - public void SetCollectables(IEnumerable collectables) + public void SetCollectables(IEnumerable collectables, bool shufflableOnly = false) { + //Shufflable marking NYI for palaces (and probably not needed) so it's NOP for now Debug.Assert(collectables.Count() == ItemRooms.Count); int i = 0; foreach (Collectable collectable in collectables) diff --git a/RandomizerCore/Sidescroll/Town/Town.cs b/RandomizerCore/Sidescroll/Town/Town.cs index 6c127908c..b22533a11 100644 --- a/RandomizerCore/Sidescroll/Town/Town.cs +++ b/RandomizerCore/Sidescroll/Town/Town.cs @@ -150,9 +150,9 @@ public bool CanBeTraversed(List requireables) throw new ImpossibleException("Too many non-looping maps in town traversal check"); } - public void SetCollectables(IEnumerable collectables) + public void SetCollectables(IEnumerable collectables, bool collectableOnly = false) { - List collectableMaps = TownMaps.Where(i => i.Collectable != null).ToList(); + List collectableMaps = TownMaps.Where(i => i.Collectable != null && (!collectableOnly || i.CollectableIsShufflable)).ToList(); Debug.Assert(collectables.Count() == collectableMaps.Count); int i = 0; foreach(Collectable collectable in collectables) From ba7515498d802ccb632a50c223612f824589a6fc Mon Sep 17 00:00:00 2001 From: Ellendar Date: Wed, 8 Jul 2026 14:58:31 -0400 Subject: [PATCH 04/10] Fixed a pile of item shuffle / hint generation issues. Included in this is a fix for an older bug that prevented a palace from having more than one of the same item. --- RandomizerCore/CustomTexts.cs | 35 ++--- RandomizerCore/Hyrule.cs | 142 +++++++++++--------- RandomizerCore/Overworld/EastHyrule.cs | 9 +- RandomizerCore/Overworld/MazeIsland.cs | 3 +- RandomizerCore/Overworld/WestHyrule.cs | 9 +- RandomizerCore/RandomizerConfiguration.cs | 2 + RandomizerCore/RandomizerProperties.cs | 2 +- RandomizerCore/Sidescroll/Palace/Palace.cs | 39 +++--- RandomizerCore/Sidescroll/Palace/Palaces.cs | 34 +++-- 9 files changed, 151 insertions(+), 124 deletions(-) diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index a1804f790..192d7323a 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -1,8 +1,9 @@ -using System; +using DynamicData; +using NLog; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using NLog; using Z2Randomizer.RandomizerCore.Overworld; using Z2Randomizer.RandomizerCore.Sidescroll.Town; @@ -396,7 +397,8 @@ public class CustomTexts { TownType.NABOORU, ["Do not$drink the$fountain$water here$...$trust me."] }, { TownType.DARUNIA, ["You saved$a kid$for this?", "Dont$forget to$get upstab"] }, { TownType.NEW_KASUTO, [] }, - { TownType.OLD_KASUTO, ["Sorry$about the$moas"] } + { TownType.OLD_KASUTO, ["Sorry$about the$moas"] }, + { TownType.BAGU, BAGU_TEXTS } }; public static readonly Dictionary WIZARD_SPELL_TEXTS_BY_COLLECTABLE = new() @@ -571,8 +573,6 @@ public static List GenerateTexts( if (props.SpellItemHints && props.IncludeSwordTechsInShuffle) { - var downstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.DOWNSTAB)); - var upstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.UPSTAB)); Text hint; if (props.StartWithDownstab) { @@ -581,22 +581,25 @@ public static List GenerateTexts( } else { + var downstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.DOWNSTAB)); hint = Text.GenerateHelpfulHint(downstabLoc, Collectable.DOWNSTAB, props.IncludeSpellsInShuffle); } texts[downstabClosedDoorTextIndex] = hint; - if (upstabLoc == null) + if (props.StartWithUpstab) { - hint = props.UseCommunityText ? new Text(STARTED_WITH_ITEM_TEXTS.Sample(nonhashRNG)!, Collectable.DOWNSTAB) - : new Text(NON_COMMUNITY_STARTED_WITH_TEXT, Collectable.DOWNSTAB); + hint = props.UseCommunityText ? new Text(STARTED_WITH_ITEM_TEXTS.Sample(nonhashRNG)!, Collectable.UPSTAB) + : new Text(NON_COMMUNITY_STARTED_WITH_TEXT, Collectable.UPSTAB); } else { + var upstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.UPSTAB)); hint = Text.GenerateHelpfulHint(upstabLoc, Collectable.UPSTAB, props.IncludeSpellsInShuffle); } texts[upstabClosedDoorTextIndex] = hint; if (props.SwapUpAndDownStab) { - (texts[upstabClosedDoorTextIndex], texts[downstabClosedDoorTextIndex]) = (texts[downstabClosedDoorTextIndex], texts[upstabClosedDoorTextIndex]); + (texts[upstabClosedDoorTextIndex], texts[downstabClosedDoorTextIndex]) + = (texts[downstabClosedDoorTextIndex], texts[upstabClosedDoorTextIndex]); } } @@ -717,9 +720,11 @@ private static Text GenerateWizardText(List texts, Random r, Location loca //If it's a spell, use the old behavior if(useCommunityText) { - List possibleWizardHints = GENERIC_WIZARD_TEXTS - .Union(WIZARD_SPELL_TEXTS_BY_TOWN[town]) - .Union(WIZARD_SPELL_TEXTS_BY_COLLECTABLE[collectable]).ToList(); + List possibleWizardHints = WIZARD_SPELL_TEXTS_BY_TOWN[town].Union(WIZARD_SPELL_TEXTS_BY_COLLECTABLE[collectable]).ToList(); + if(town != TownType.BAGU) + { + possibleWizardHints.Add(GENERIC_WIZARD_TEXTS); + } int selectedHintIndex = r.Next(possibleWizardHints.Count()); return new Text(possibleWizardHints[selectedHintIndex]); } @@ -859,7 +864,7 @@ private static List GenerateHelpfulHints(List hints, IEnumerable placedIndex = []; - List placedItems = []; + List placedItems = []; List placedTowns = []; @@ -908,7 +913,7 @@ private static List GenerateHelpfulHints(List hints, IEnumerable GenerateHelpfulHints(List hints, IEnumerable Randomize(byte[] vanillaRomData, RandomizerC { List? customSpellOrder = props.IncludeSpellsInShuffle ? null - : AllLocations() + : ItemLocations() .Where(l => l.Town?.GetWizard() != null && l != westHyrule.bagu && l != westHyrule.sariaSouth) .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); ROMData.CombineFireSpell(assembler, customSpellOrder, r); @@ -402,7 +402,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC if (ct.IsCancellationRequested) { return new RandomizerResult(false); } UpdateProgress(progress, 9); - List texts = CustomTexts.GenerateTexts(AllLocations(), ROMData.GetGameText(), props, r); + List texts = CustomTexts.GenerateTexts(ItemLocations(), ROMData.GetGameText(), props, r); StatRandomizer randomizedStats = new(ROMData, props); randomizedStats.Randomize(r, skipDifficultyOnly: shareSeedAcrossDifficulty); @@ -651,7 +651,7 @@ private void ReplaceDifficultyStartingItemsInWorld(Random r) { if (!props.StartsWithCollectable(item)) { continue; } Collectable replacementItem = minorItems.Sample(r); - foreach (Location location in AllLocations().Where(i => i.GetCollectableCount() > 0)) + foreach (Location location in ItemLocations()) { if(location.ReplaceCollectable(item, replacementItem)) { @@ -682,8 +682,16 @@ private void ShufflePalaces() private void ShuffleItems() { - List shufflableItems = []; - List globalShuffleLocations = []; + List possibleItemLocations = ItemLocations().ToList(); + IEnumerable palaceLocations = AllLocations().Where(i => i.Palace != null); + + //Non-pbag, non-wizard, non-palace items are inherently in the global shuffle pool + List shufflableItems = [Collectable.TROPHY, Collectable.MEDICINE, Collectable.HEART_CONTAINER, Collectable.MAGIC_CONTAINER, + Collectable.HEART_CONTAINER, Collectable.HAMMER, Collectable.MAGIC_CONTAINER, Collectable.HEART_CONTAINER, Collectable.HEART_CONTAINER, + Collectable.MAGIC_CONTAINER, Collectable.MAGIC_KEY, Collectable.MAGIC_CONTAINER, Collectable.CHILD]; + List globalShuffleLocations = [westHyrule.trophyCave, westHyrule.medicineCave, westHyrule.heartContainerCave, westHyrule.magicContainerCave, + westHyrule.grassTile, deathMountain.hammerCave, deathMountain.specRock, eastHyrule.desertTile, eastHyrule.waterTile, eastHyrule.newKasuto, + mazeIsland.childDrop, mazeIsland.magicContainerDrop]; List minorItems = [Collectable.BLUE_JAR, Collectable.RED_JAR, Collectable.SMALL_BAG, Collectable.MEDIUM_BAG, Collectable.LARGE_BAG, Collectable.XL_BAG, Collectable.ONEUP, Collectable.KEY]; @@ -753,14 +761,13 @@ private void ShuffleItems() int heartContainersInItemPool = props.MaxHearts - props.StartHearts; int magicContainersInItemPool = props.MaxMagicContainers - props.StartMagicContainers; - foreach (Collectable collectable in ItemGet.Keys) { ItemGet[collectable] = props.StartsWithCollectable(collectable); } pbagHearts = []; - List heartContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.HEART_CONTAINER)).ToList(); + List heartContainerLocations = possibleItemLocations.Where(i => i.GetAllCollectables().Contains(Collectable.HEART_CONTAINER)).ToList(); //Replace any unused heart containers with small items if (heartContainersInItemPool < 4) @@ -779,6 +786,8 @@ private void ShuffleItems() collectables[index] = minorItems.Sample(r); hcLocation.SetCollectables(collectables); heartContainersToRemove--; + shufflableItems.Remove(Collectable.HEART_CONTAINER); + shufflableItems.Add(collectables[index]); if (!collectables.Any(i => i == Collectable.HEART_CONTAINER)) { heartContainerLocations.Remove(hcLocation); @@ -789,7 +798,7 @@ private void ShuffleItems() } } - List magicContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.MAGIC_CONTAINER)).ToList(); + List magicContainerLocations = possibleItemLocations.Where(i => i.GetAllCollectables().Contains(Collectable.MAGIC_CONTAINER)).ToList(); //Replace any unused magic containers with small items if (magicContainersInItemPool < 4) { @@ -807,6 +816,8 @@ private void ShuffleItems() collectables[index] = minorItems.Sample(r); mcLocation.SetCollectables(collectables); magicContainersToRemove--; + shufflableItems.Remove(Collectable.MAGIC_CONTAINER); + shufflableItems.Add(collectables[index]); if (!collectables.Any(i => i == Collectable.MAGIC_CONTAINER)) { magicContainerLocations.Remove(mcLocation); @@ -828,6 +839,11 @@ private void ShuffleItems() { shufflableItems[index] = minorItems.Sample(r); } + else + { + Location? defaultLocation = possibleItemLocations.FirstOrDefault(i => i.GetAllCollectables().Contains(spellItem)); + defaultLocation?.ReplaceCollectable(spellItem, minorItems.Sample(r)); + } ItemGet[spellItem] = true; } } @@ -907,12 +923,6 @@ private void ShuffleItems() excessItems.Add(Collectable.MAGIC_CONTAINER); } - int extraPalaceItemCount = props.PalaceItemRoomCounts.Sum(c => Math.Max(c - 1, 0)); - for (int i = 0; i < extraPalaceItemCount; i++) - { - shufflableItems.Add(minorItems.Sample(r)); - } - List vanillaPalaceItems = [Collectable.CANDLE, Collectable.GLOVE, Collectable.RAFT, Collectable.BOOTS, Collectable.FLUTE, Collectable.CROSS]; //No palace items handling @@ -921,17 +931,12 @@ private void ShuffleItems() if (props.PalaceItemRoomCounts[i] == 0) { var vanillaPalaceItem = vanillaPalaceItems[i]; - if (props.StartsWithCollectable(vanillaPalaceItem)) - { - // since the palace item is replaced by a minor item, remove a minor item from the shuffle - var firstMinorItemIndex = shufflableItems.FindIndex(c => c.IsMinorItem()); - Debug.Assert(firstMinorItemIndex != -1); - shufflableItems.RemoveAt(firstMinorItemIndex); - } - else + + //If we didn't start with the item that should have been here, it still has to be somewhere, + //so add it to the excess items and it will eventually replace a minor item + if (!props.StartsWithCollectable(vanillaPalaceItem)) { excessItems.Add(vanillaPalaceItem); - shufflableItems.Remove(vanillaPalaceItem); } } } @@ -985,7 +990,12 @@ private void ShuffleItems() if (props.MixOverworldPalaceItems) { - + //This should probably always be true but safety + if(props.ShufflePalaceItems) + { + shufflableItems.Add(palaceLocations.SelectMany(i => i.GetAllCollectables())); + globalShuffleLocations.Add(palaceLocations); + } duplicateItemPlacementCandidates.AddRange(globalShuffleLocations); DoShuffle(shufflableItems, globalShuffleLocations); } @@ -998,37 +1008,21 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location palaceLocation in AllLocations().Where(i => i.Palace != null)) + foreach (Location palaceLocation in globalShuffleLocations.Where(i => i.Palace != null)) { shufflableItemLocations.Add(palaceLocation); - Collectable vanillaCollectable = palaceLocation.Palace!.GetVanillaCollectable(); - itemsToActuallyShuffle.Add(props.StartsWithCollectable(vanillaCollectable) ? minorItems.Sample(r) : vanillaCollectable); - - for (int i = 1; i < props.PalaceItemRoomCounts[(int)palaceLocation.Palace.Number! - 1]; i++) - { - itemsToActuallyShuffle.Add(minorItems.Sample(r)); - } + itemsToActuallyShuffle.Add(palaceLocation.GetAllCollectables()); } duplicateItemPlacementCandidates.AddRange(shufflableItemLocations); DoShuffle(itemsToActuallyShuffle, shufflableItemLocations); } - else //no shuffle palace items + else //vanilla palace items { - foreach (Location palaceLocation in AllLocations().Where(i => i.Palace != null && i.Palace.Number < 7)) + foreach (Location palaceLocation in possibleItemLocations.Where(i => i.Palace != null && i.Palace.Number < 7)) { int palaceNumber = palaceLocation.Palace!.Number; - Collectable vanillaCollectable = palaceLocation.Palace!.GetVanillaCollectable(); - List collectables = []; - collectables = [shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)]; - for (int i = 0; i < props.PalaceItemRoomCounts[(int)palaceNumber! - 1]; i++) - { - if (palaces[(int)palaceNumber! - 1].ItemRooms[i].Collectable == null) - { - Collectable smallItem = minorItems.Sample(r); - collectables.Add(smallItem); - } - } - palaceLocation.SetCollectables(collectables); + List vanillaCollectables = palaceLocation.Palace!.GetVanillaCollectables(palaceLocation.Palace!.ItemRooms.Count); + palaceLocation.SetCollectables(vanillaCollectables); } } @@ -1036,10 +1030,10 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location nonPalaceLocation in AllLocations().Where(i => i.Palace == null)) + foreach (Location nonPalaceLocation in possibleItemLocations.Where(i => i.Palace == null)) { shufflableItemLocations.Add(nonPalaceLocation); - foreach(Collectable collectable in nonPalaceLocation.GetAllCollectables()) + foreach(Collectable collectable in nonPalaceLocation.GetShufflableCollectables()) { itemsToActuallyShuffle.Add(shufflableItems.Contains(collectable) ? collectable : minorItems.Sample(r)); } @@ -1049,7 +1043,7 @@ private void ShuffleItems() } else { - foreach (Location nonPalaceLocation in AllLocations().Where(i => i.Palace == null)) + foreach (Location nonPalaceLocation in possibleItemLocations.Where(i => i.Palace == null)) { List collectables = nonPalaceLocation.GetAllCollectables(); for(int i = 0; i < collectables.Count; i++) @@ -1062,11 +1056,12 @@ private void ShuffleItems() } //Assigning unshuffled locations can make the number of containers wrong, so re-adjust them - heartContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.HEART_CONTAINER)).ToList(); + heartContainerLocations = possibleItemLocations.Where(i => i.GetAllCollectables().Contains(Collectable.HEART_CONTAINER)).ToList(); int heartContainerCount = heartContainerLocations.SelectMany(i => i.GetAllCollectables()).Count(i => i == Collectable.HEART_CONTAINER); while(heartContainerCount > heartContainersInItemPool) { Location location = heartContainerLocations.Sample(r)!; + Debug.Assert(location != null); Collectable minorItem = minorItems.Sample(r); if(!location.ReplaceCollectable(Collectable.HEART_CONTAINER, minorItem)) { @@ -1074,7 +1069,7 @@ private void ShuffleItems() } } - List minorItemLocations = AllLocations().Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); + List minorItemLocations = possibleItemLocations.Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); while (heartContainerCount < heartContainersInItemPool) { @@ -1092,19 +1087,25 @@ private void ShuffleItems() heartContainerCount++; } - magicContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.MAGIC_CONTAINER)).ToList(); + magicContainerLocations = possibleItemLocations.Where(i => i.GetAllCollectables().Contains(Collectable.MAGIC_CONTAINER)).ToList(); int magicContainerCount = magicContainerLocations.SelectMany(i => i.GetAllCollectables()).Count(i => i == Collectable.MAGIC_CONTAINER); while (magicContainerCount > magicContainersInItemPool) { - Location location = magicContainerLocations.Sample(r)!; - Collectable minorItem = minorItems.Sample(r); - if (!location.ReplaceCollectable(Collectable.MAGIC_CONTAINER, minorItem)) + Location location = minorItemLocations.Sample(r)!; + Collectable[] locationMinorItems = location.GetAllCollectables().Where(i => i.IsMinorItem()).ToArray(); + if (locationMinorItems.Length == 0) + { + minorItemLocations.Remove(location); + continue; + } + if (!location.ReplaceCollectable(locationMinorItems.Sample(r), Collectable.MAGIC_CONTAINER)) { magicContainerLocations.Remove(location); } + magicContainerCount++; } - minorItemLocations = AllLocations().Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); + minorItemLocations = possibleItemLocations.Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); while (magicContainerCount < magicContainersInItemPool) { @@ -1177,19 +1178,20 @@ private void DoShuffle(List itemsToShuffle, List itemShuf if (itemsToShuffle.Count != possibleItemPlacements) { + itemShuffleLocations.ForEach(i => Debug.WriteLine($"{i.Name} - {i.GetShufflableCollectables().Count}")); + itemsToShuffle.ForEach(i => Debug.WriteLine($"{i}")); throw new Exception("Item locations must match number of items"); } itemsToShuffle.FisherYatesShuffle(r); - int itemIndex = 0; foreach(Location location in itemShuffleLocations) { int collectableCount = location.GetCollectableCount(true); List newCollectables = itemsToShuffle.GetRange(itemIndex, collectableCount); itemIndex += collectableCount; - location.SetCollectables(newCollectables); + location.SetCollectables(newCollectables, true); } } @@ -1417,7 +1419,7 @@ private bool IsEverythingReachable(Dictionary itemGet) //Debug.WriteLine(GenerateSpoiler()); //Debug.WriteLine(westHyrule.GetMapDebug()); //XXX: - return false; + return palaces[2].ItemRooms[0].Collectable == Collectable.GLOVE; } //#endif return false; @@ -1525,7 +1527,7 @@ private int UpdateItemGets(RandomizerProperties props) accessibleHeartContainers = props.StartHearts; Location newKasuto = eastHyrule.newKasuto; List gottenItems = [], lastIterationGottenItems = []; - List locations = AllLocations().ToList(); + List locations = ItemLocations().ToList(); List heartContainerGetLocations = [], magicContainerGetLocations = []; bool stalled = false; //No more delayed evaluation because too many locations could give you containers or require containers. @@ -1536,7 +1538,7 @@ private int UpdateItemGets(RandomizerProperties props) accessibleMagicContainers = props.StartMagicContainers + magicContainerGetLocations.Count; requireables = GetRequireables(props); gottenItems = []; - foreach (Location location in AllLocations().Where(i => i.Reachable)) + foreach (Location location in ItemLocations().Where(i => i.Reachable)) { List gettableItems = location.GetGettableItems(requireables); @@ -2647,7 +2649,7 @@ private void UpdateRom() else { // Use the old spell menu behavior where the spells are placed in the menu in the location it came from - var wizardCollectables = AllLocations() + var wizardCollectables = ItemLocations() .Where(l => l.Town?.GetWizard() != null && l != westHyrule.sariaSouth && l != westHyrule.bagu) .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); @@ -3024,6 +3026,14 @@ public IEnumerable AllLocations() return locations ?? []; } + public IEnumerable ItemLocations() + { + List possibleItemLocations = AllLocations().Where(i => i.GetCollectableCount() > 0).ToList(); + possibleItemLocations.Remove(westHyrule.sariaSouth); + return possibleItemLocations; + } + + public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) { StringBuilder sb = new StringBuilder(); @@ -3038,18 +3048,18 @@ public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) sb.AppendLine("Missing Items:"); //Where(i => !i.ItemGet && !i.Collectable.IsInternalUse() bool itemWasMissing = false; - foreach (Location location in AllLocations().Where(l => l.GetAllCollectables().Any(i => !i.IsMinorItem() && !ItemGet[i]))) + foreach (Location location in ItemLocations().Where(l => l.GetAllCollectables().Any(i => !i.IsMinorItem() && !ItemGet[i]))) { sb.AppendLine($"{location.Name} / [{String.Join(", ", location.GetAllCollectables() .Where(i => !i.IsMinorItem() && !ItemGet[i]) - .Select(i => Enum.GetName(i)!))}]"); + .Select(i => Enum.GetName(i)))}]"); itemWasMissing = true; } if(!itemWasMissing) { sb.AppendLine("Missing Non-Item locations (this should have been caught by ValidateBasicRouting!"); - foreach (Location location in AllLocations().Where(l => !l.Reachable)) + foreach (Location location in ItemLocations().Where(l => !l.Reachable)) { sb.AppendLine(location.Name); } diff --git a/RandomizerCore/Overworld/EastHyrule.cs b/RandomizerCore/Overworld/EastHyrule.cs index b57430334..7c43b9911 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -2110,8 +2110,11 @@ public override void ResetCollectables(RandomizerProperties props) pbagCave2.CollectablesAreShufflable = props.ShuffleOverworldItems && props.PbagItemShuffle; - locationAtPalace5.SetCollectables(locationAtPalace5.Palace!.GetVanillaCollectables()); - locationAtPalace6.SetCollectables(locationAtPalace6.Palace!.GetVanillaCollectables()); - locationAtGP.SetCollectables(locationAtGP.Palace!.GetVanillaCollectables()); + locationAtPalace5.SetCollectables(locationAtPalace5.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtPalace5.Palace!.Number - 1])); + locationAtPalace6.SetCollectables(locationAtPalace6.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtPalace6.Palace!.Number - 1])); + locationAtGP.SetCollectables(locationAtGP.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtGP.Palace!.Number - 1])); } } \ No newline at end of file diff --git a/RandomizerCore/Overworld/MazeIsland.cs b/RandomizerCore/Overworld/MazeIsland.cs index 8f9934706..9c5d40137 100644 --- a/RandomizerCore/Overworld/MazeIsland.cs +++ b/RandomizerCore/Overworld/MazeIsland.cs @@ -1056,6 +1056,7 @@ public override void ResetCollectables(RandomizerProperties props) magicContainerDrop.SetCollectables([Collectable.MAGIC_CONTAINER]); magicContainerDrop.CollectablesAreShufflable = props.ShuffleOverworldItems; - locationAtPalace4.SetCollectables(locationAtPalace4.Palace!.GetVanillaCollectables()); + locationAtPalace4.SetCollectables(locationAtPalace4.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtPalace4.Palace!.Number - 1])); } } diff --git a/RandomizerCore/Overworld/WestHyrule.cs b/RandomizerCore/Overworld/WestHyrule.cs index 6e8fdf1a8..411f60bf4 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -1603,8 +1603,11 @@ public override void ResetCollectables(RandomizerProperties props) grassTile.SetCollectables([Collectable.HEART_CONTAINER]); grassTile.CollectablesAreShufflable = props.ShuffleOverworldItems; - locationAtPalace1.SetCollectables(locationAtPalace1.Palace!.GetVanillaCollectables()); - locationAtPalace2.SetCollectables(locationAtPalace2.Palace!.GetVanillaCollectables()); - locationAtPalace3.SetCollectables(locationAtPalace3.Palace!.GetVanillaCollectables()); + locationAtPalace1.SetCollectables(locationAtPalace1.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtPalace1.Palace!.Number - 1])); + locationAtPalace2.SetCollectables(locationAtPalace2.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtPalace2.Palace!.Number - 1])); + locationAtPalace3.SetCollectables(locationAtPalace3.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtPalace3.Palace!.Number - 1])); } } diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index 63860832d..6e96dfa6c 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -1634,6 +1634,8 @@ public void AssignPalaceItemCounts(RandomizerProperties properties, Random r) } } } + //for ease of some calculations, add a count in for GP that is always 0 + properties.PalaceItemRoomCounts[6] = 0; } /// Note: this value can be limited by other things, like the style being Vanilla diff --git a/RandomizerCore/RandomizerProperties.cs b/RandomizerCore/RandomizerProperties.cs index a1fb19f02..a651dbd3d 100644 --- a/RandomizerCore/RandomizerProperties.cs +++ b/RandomizerCore/RandomizerProperties.cs @@ -135,7 +135,7 @@ public class RandomizerProperties public bool HardBosses { get; set; } public bool AggressiveTbird { get; set; } [NotMapped] - public int[] PalaceItemRoomCounts { get; set; } = new int[6]; + public int[] PalaceItemRoomCounts { get; set; } = new int[7]; public bool UsePalaceItemRoomCountIndicator { get; set; } public bool RevealWalkthroughWalls { get; set; } diff --git a/RandomizerCore/Sidescroll/Palace/Palace.cs b/RandomizerCore/Sidescroll/Palace/Palace.cs index 66b6a6e5b..4a2d02ebd 100644 --- a/RandomizerCore/Sidescroll/Palace/Palace.cs +++ b/RandomizerCore/Sidescroll/Palace/Palace.cs @@ -1146,6 +1146,11 @@ public List GetGettableItems(IEnumerable initialRe while (pendingRooms.Count > 0) { Room room = pendingRooms.First(); + if (coveredRooms.Contains(room)) + { + pendingRooms.Remove(room); + continue; + } if (ItemRooms.Contains(room)) { if (room.Collectable == null) @@ -1153,20 +1158,12 @@ public List GetGettableItems(IEnumerable initialRe throw new Exception("Unable to assess gettability of item room with no item"); } Collectable roomCollectable = room.Collectable ?? Collectable.DO_NOT_USE; - if(!gettableItems.Contains(roomCollectable)) + gettableItems.Add(roomCollectable); + if (roomCollectable.AsRequirement() != null) { - gettableItems.Add(roomCollectable); - if (roomCollectable.AsRequirement() != null) - { - requireables.Add(roomCollectable.AsRequirement() ?? RequirementType.JUMP); - } + requireables.Add(roomCollectable.AsRequirement() ?? RequirementType.JUMP); } } - if (coveredRooms.Contains(room)) - { - pendingRooms.Remove(room); - continue; - } coveredRooms.Add(room); pendingRooms.Remove(room); if (room.Left != null && room.Left.IsTraversable(requireables)) @@ -1376,17 +1373,12 @@ public void ReplaceRoom(Room roomToReplace, Room newRoom) AllRooms.Add(newRoom); } - public Collectable GetVanillaCollectable() + public List GetVanillaCollectables(int itemCount) { - return Number switch + if(itemCount == 0) { - 7 => Collectable.DO_NOT_USE, - _ => GetVanillaCollectables()[0] - }; - } - - public List GetVanillaCollectables() - { + return []; + } List[] vanillaCollectables = [ [Collectable.CANDLE], [Collectable.GLOVE], @@ -1400,7 +1392,12 @@ public List GetVanillaCollectables() { throw new Exception("Invalid palace number"); } - return vanillaCollectables[Number - 1]; + List collectables = vanillaCollectables[Number - 1]; + for(int i = 1; i < itemCount; i++) + { + collectables.Add(Collectable.LARGE_BAG); + } + return collectables; } public RoomExitType GetCoordinateRoomShape(int x, int y) diff --git a/RandomizerCore/Sidescroll/Palace/Palaces.cs b/RandomizerCore/Sidescroll/Palace/Palaces.cs index 54516b7c9..256af7050 100644 --- a/RandomizerCore/Sidescroll/Palace/Palaces.cs +++ b/RandomizerCore/Sidescroll/Palace/Palaces.cs @@ -294,7 +294,7 @@ private static bool ValidatePalaces(RandomizerProperties props, bool raftIsRequi return false; } } - return CanGetGlove(props, palaces[1]) + return CanGetGlove(props, palaces) && CanGetRaft(props, raftIsRequired, palaces[1], palaces[2]) && AtLeastOnePalaceCanHaveGlove(props, palaces); } @@ -326,29 +326,35 @@ private static bool AtLeastOnePalaceCanHaveGlove(RandomizerProperties props, Lis return false; } - private static bool CanGetGlove(RandomizerProperties props, Palace palace2) + private static bool CanGetGlove(RandomizerProperties props, IEnumerable palaces) { - if (!props.ShufflePalaceItems) + List requireables = [.. RequirementTypeExtensions.UpToXContainers(props.StartMagicContainers)]; + //If shuffle overworld items is on, we assume you can get all the items / spells + //as all progression items will eventually shuffle into spots that work + if (props.ShuffleOverworldItems) { - List requireables = [..RequirementTypeExtensions.UpToXContainers(props.StartMagicContainers)]; - //If shuffle overworld items is on, we assume you can get all the items / spells - //as all progression items will eventually shuffle into spots that work - if (props.ShuffleOverworldItems) - { - requireables = [RequirementType.KEY, ..RequirementTypeExtensions.UpToXContainers(8)]; + requireables = [RequirementType.KEY, .. RequirementTypeExtensions.UpToXContainers(8)]; + } + //Otherwise if it's vanilla items we can't get the magic key, because we could need glove for boots for flute to get to new kasuto + requireables.Add(props.SwapUpAndDownStab ? RequirementType.UPSTAB : RequirementType.DOWNSTAB); + requireables.Add(RequirementType.JUMP); + requireables.Add(RequirementType.FAIRY); - } - //Otherwise if it's vanilla items we can't get the magic key, because we could need glove for boots for flute to get to new kasuto - requireables.Add(props.SwapUpAndDownStab ? RequirementType.UPSTAB : RequirementType.DOWNSTAB); - requireables.Add(RequirementType.JUMP); - requireables.Add(RequirementType.FAIRY); + if (!props.ShufflePalaceItems) + { //If we can't clear 2 without the items available, we can never get the glove, so the palace is unbeatable + Palace palace2 = palaces.FirstOrDefault(i => i.Number == 2)!; if (!palace2.CanReachAnItemRoom(requireables)) { return false; } } + else if(!props.MixOverworldPalaceItems) + { + //If palace items are shuffled, but not mixed, we have to be able to get to at least one item room without the glove + return palaces.Any(palace => palace.CanReachAnItemRoom(requireables)); + } return true; } From 3fd8ef580aa447e0b9ed8e789356c7cc1d0b00e4 Mon Sep 17 00:00:00 2001 From: Ellendar Date: Wed, 15 Jul 2026 20:45:23 -0400 Subject: [PATCH 05/10] Fixed a bunch more issues with shuffles / town routing --- RandomizerCore/Hyrule.cs | 58 +++++++++++++++-------- RandomizerCore/Overworld/Location.cs | 2 +- RandomizerCore/RandomizerConfiguration.cs | 2 +- RandomizerCore/Sidescroll/Town/Towns.cs | 17 +++++++ Statistics/Statistics.cs | 4 +- 5 files changed, 58 insertions(+), 25 deletions(-) diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 22bb01413..59ad156af 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -106,6 +106,7 @@ public class Hyrule //private Character character; public Dictionary ItemGet { get; set; } + private List spellListOrder { get; set; } //private bool[] spellGet; public WestHyrule westHyrule; @@ -366,12 +367,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC if (props.CombineFire) { - List? customSpellOrder = props.IncludeSpellsInShuffle - ? null - : ItemLocations() - .Where(l => l.Town?.GetWizard() != null && l != westHyrule.bagu && l != westHyrule.sariaSouth) - .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); - ROMData.CombineFireSpell(assembler, customSpellOrder, r); + ROMData.CombineFireSpell(assembler, spellListOrder, r); } if (props.StartsWithCollectable(westHyrule.ruto.Town?.GetWizard()?.Collectable)) @@ -685,6 +681,10 @@ private void ShuffleItems() List possibleItemLocations = ItemLocations().ToList(); IEnumerable palaceLocations = AllLocations().Where(i => i.Palace != null); + spellListOrder = [Collectable.SHIELD_SPELL, Collectable.JUMP_SPELL, Collectable.LIFE_SPELL, Collectable.FAIRY_SPELL, + props.ReplaceFireWithDash ? Collectable.DASH_SPELL : Collectable.FIRE_SPELL, Collectable.SPELL_SPELL, Collectable.REFLECT_SPELL, + Collectable.THUNDER_SPELL]; + //Non-pbag, non-wizard, non-palace items are inherently in the global shuffle pool List shufflableItems = [Collectable.TROPHY, Collectable.MEDICINE, Collectable.HEART_CONTAINER, Collectable.MAGIC_CONTAINER, Collectable.HEART_CONTAINER, Collectable.HAMMER, Collectable.MAGIC_CONTAINER, Collectable.HEART_CONTAINER, Collectable.HEART_CONTAINER, @@ -718,6 +718,8 @@ private void ShuffleItems() eastHyrule.pbagCave2.SetCollectables([Collectable.XL_BAG]); } + //For now this is just for classic spell shuffle. Maybe this is extended later + bool canReplaceStartingSpellsWithMinorItems = !props.ShuffleSpellLocations; if (props.IncludeSpellsInShuffle) { shufflableItems.Add(Collectable.SHIELD_SPELL); @@ -734,8 +736,13 @@ private void ShuffleItems() else if (props.ShuffleSpellLocations) { ShuffleSpells(); + spellListOrder = possibleItemLocations.Where(i => i.Town != null && i?.Town?.Type != TownType.BAGU) + .OrderBy(i => (int)i.Town!.Type!) + .Select(i => (Collectable)i.Town!.GetWizard()!.Collectable!) + .ToList(); } + if (props.IncludeQuestItemsInShuffle) { shufflableItems.Add(Collectable.BAGUS_NOTE); @@ -888,7 +895,7 @@ private void ShuffleItems() foreach (Collectable item in possibleStartItems) { - if (props.StartsWithCollectable(item)) + if (props.StartsWithCollectable(item) && (canReplaceStartingSpellsWithMinorItems || !item.IsSpell())) { int index = shufflableItems.IndexOf(item); if (index > -1) @@ -1008,7 +1015,7 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location palaceLocation in globalShuffleLocations.Where(i => i.Palace != null)) + foreach (Location palaceLocation in possibleItemLocations.Where(i => i.Palace != null)) { shufflableItemLocations.Add(palaceLocation); itemsToActuallyShuffle.Add(palaceLocation.GetAllCollectables()); @@ -1021,8 +1028,13 @@ private void ShuffleItems() foreach (Location palaceLocation in possibleItemLocations.Where(i => i.Palace != null && i.Palace.Number < 7)) { int palaceNumber = palaceLocation.Palace!.Number; - List vanillaCollectables = palaceLocation.Palace!.GetVanillaCollectables(palaceLocation.Palace!.ItemRooms.Count); - palaceLocation.SetCollectables(vanillaCollectables); + List collectables = palaceLocation.Palace!.GetVanillaCollectables(palaceLocation.Palace!.ItemRooms.Count); + + for (int i = 0; i < collectables.Count; i++) + { + collectables[i] = props.StartsWithCollectable(collectables[i]) ? minorItems.Sample(r) : collectables[i]; + } + palaceLocation.SetCollectables(collectables); } } @@ -1035,7 +1047,9 @@ private void ShuffleItems() shufflableItemLocations.Add(nonPalaceLocation); foreach(Collectable collectable in nonPalaceLocation.GetShufflableCollectables()) { - itemsToActuallyShuffle.Add(shufflableItems.Contains(collectable) ? collectable : minorItems.Sample(r)); + itemsToActuallyShuffle.Add( + props.StartsWithCollectable(collectable) && (!collectable.IsSpell() || canReplaceStartingSpellsWithMinorItems) + ? minorItems.Sample(r) : collectable); } } duplicateItemPlacementCandidates.AddRange(shufflableItemLocations); @@ -1048,7 +1062,7 @@ private void ShuffleItems() List collectables = nonPalaceLocation.GetAllCollectables(); for(int i = 0; i < collectables.Count; i++) { - collectables[i] = shufflableItems.Contains(collectables[i]) ? collectables[i] : minorItems.Sample(r); + collectables[i] = props.StartsWithCollectable(collectables[i]) ? minorItems.Sample(r) : collectables[i]; } nonPalaceLocation.SetCollectables(collectables); } @@ -1419,7 +1433,7 @@ private bool IsEverythingReachable(Dictionary itemGet) //Debug.WriteLine(GenerateSpoiler()); //Debug.WriteLine(westHyrule.GetMapDebug()); //XXX: - return palaces[2].ItemRooms[0].Collectable == Collectable.GLOVE; + return false; } //#endif return false; @@ -2649,31 +2663,32 @@ private void UpdateRom() else { // Use the old spell menu behavior where the spells are placed in the menu in the location it came from - var wizardCollectables = ItemLocations() + /*var wizardCollectables = ItemLocations() .Where(l => l.Town?.GetWizard() != null && l != westHyrule.sariaSouth && l != westHyrule.bagu) .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); + */ for (int i = 0; i < magFunction.Length; i++) { - magFunction[i] = ROMData.GetByte(functionBase + wizardCollectables[i].VanillaSpellOrder()); + magFunction[i] = ROMData.GetByte(functionBase + spellListOrder[i].VanillaSpellOrder()); } for (int i = 0; i < magEffects.Length; i = i + 2) { - magEffects[i] = ROMData.GetByte(effectBase + wizardCollectables[i / 2].VanillaSpellOrder() * 2); - magEffects[i + 1] = ROMData.GetByte(effectBase + wizardCollectables[i / 2].VanillaSpellOrder() * 2 + 1); + magEffects[i] = ROMData.GetByte(effectBase + spellListOrder[i / 2].VanillaSpellOrder() * 2); + magEffects[i + 1] = ROMData.GetByte(effectBase + spellListOrder[i / 2].VanillaSpellOrder() * 2 + 1); } for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { - magLevels[i, j] = ROMData.GetByte(spellCostBase + (wizardCollectables[i].VanillaSpellOrder() * 8 + j)); + magLevels[i, j] = ROMData.GetByte(spellCostBase + (spellListOrder[i].VanillaSpellOrder() * 8 + j)); } for (int j = 0; j < 8; j++) { - magNames[i, j] = ROMData.GetByte(spellNameBase + (wizardCollectables[i].VanillaSpellOrder() * 0xe + j)); + magNames[i, j] = ROMData.GetByte(spellNameBase + (spellListOrder[i].VanillaSpellOrder() * 0xe + j)); } } @@ -2703,7 +2718,7 @@ private void UpdateRom() for (int i = 0; i < 8; i++) { - ROMData.Put(TownExtensions.SPELL_GET_START_ADDRESS + i, props.StartsWithCollectable(wizardCollectables[i]) ? (byte)1 : (byte)0); + ROMData.Put(TownExtensions.SPELL_GET_START_ADDRESS + i, props.StartsWithCollectable(spellListOrder[i]) ? (byte)1 : (byte)0); } } //fix for rope graphical glitch @@ -3050,7 +3065,8 @@ public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) bool itemWasMissing = false; foreach (Location location in ItemLocations().Where(l => l.GetAllCollectables().Any(i => !i.IsMinorItem() && !ItemGet[i]))) { - sb.AppendLine($"{location.Name} / [{String.Join(", ", location.GetAllCollectables() + string name = location.Name + (location.Palace == null ? String.Empty : $"({location?.Palace?.Number})"); + sb.AppendLine($"{name} / [{String.Join(", ", location!.GetAllCollectables() .Where(i => !i.IsMinorItem() && !ItemGet[i]) .Select(i => Enum.GetName(i)))}]"); itemWasMissing = true; diff --git a/RandomizerCore/Overworld/Location.cs b/RandomizerCore/Overworld/Location.cs index 6ec3b596a..cc9774e54 100644 --- a/RandomizerCore/Overworld/Location.cs +++ b/RandomizerCore/Overworld/Location.cs @@ -359,6 +359,6 @@ public int GetCollectableCount(bool shufflableOnly = false) { return Palace.ItemRooms.Count; } - return NonPalaceTownCollectables.Count; + return (CollectablesAreShufflable || !shufflableOnly) ? NonPalaceTownCollectables.Count : 0; } } diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index 6e96dfa6c..8cc00aeb0 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -1607,7 +1607,7 @@ public void AssignPalaceItemCounts(RandomizerProperties properties, Random r) properties.UsePalaceItemRoomCountIndicator = true; break; default: - properties.PalaceItemRoomCounts = Enumerable.Repeat((int)palaceItemRoomCount, 6).ToArray(); + properties.PalaceItemRoomCounts = Enumerable.Repeat((int)palaceItemRoomCount, 7).ToArray(); properties.UsePalaceItemRoomCountIndicator = false; break; } diff --git a/RandomizerCore/Sidescroll/Town/Towns.cs b/RandomizerCore/Sidescroll/Town/Towns.cs index 8f2721a7d..16caaca88 100644 --- a/RandomizerCore/Sidescroll/Town/Towns.cs +++ b/RandomizerCore/Sidescroll/Town/Towns.cs @@ -20,8 +20,10 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Rauru"; map = town.GetTownMap(VanillaTownMap.RAURU_LEFT)!; map.Name = "Rauru Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.RAURU_RIGHT)!; map.Name = "Rauru Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.RAURU_WIZARD)!; map.Name = "Rauru Wizard"; map.Collectable = Collectable.SHIELD_SPELL; @@ -33,8 +35,10 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Ruto"; map = town.GetTownMap(VanillaTownMap.RUTO_LEFT)!; map.Name = "Ruto Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.RUTO_RIGHT)!; map.Name = "Ruto Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.RUTO_WIZARD)!; map.Name = "Ruto Wizard"; map.Collectable = Collectable.JUMP_SPELL; @@ -47,11 +51,13 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Saria"; map = town.GetTownMap(VanillaTownMap.SARIA_LEFT)!; map.Name = "Saria Moat"; + map.LeftExitIsOutside = true; map.AccessRequirements = new Requirements([RequirementType.FAIRY, RequirementType.BAGU_LETTER], [[RequirementType.JUMP, RequirementType.DASH]]); map = town.GetTownMap(VanillaTownMap.SARIA_MID)!; map.Name = "Saria Middle"; map = town.GetTownMap(VanillaTownMap.SARIA_RIGHT)!; map.Name = "Saria Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.SARIA_WIZARD)!; map.Name = "Saria Wizard"; map.Collectable = Collectable.LIFE_SPELL; @@ -67,10 +73,12 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Mido"; map = town.GetTownMap(VanillaTownMap.MIDO_LEFT)!; map.Name = "Mido Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.MIDO_MIDDLE)!; map.Name = "Mido Middle"; map = town.GetTownMap(VanillaTownMap.MIDO_RIGHT)!; map.Name = "Mido Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.MIDO_WIZARD)!; map.Name = "Mido Wizard"; map.Collectable = Collectable.FAIRY_SPELL; @@ -87,11 +95,13 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Nabooru"; map = town.GetTownMap(VanillaTownMap.NABOORU_LEFT)!; map.Name = "Nabooru Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.NABOORU_MID)!; map.Name = "Nabooru Fountain"; map.Collectable = Collectable.WATER; map = town.GetTownMap(VanillaTownMap.NABOORU_RIGHT)!; map.Name = "Nabooru Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.NABOORU_WIZARD)!; map.Name = "Nabooru Wizard"; map.Collectable = Collectable.FIRE_SPELL; @@ -104,10 +114,12 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Darunia"; map = town.GetTownMap(VanillaTownMap.DARUNIA_LEFT)!; map.Name = "Darunia Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.DARUNIA_MID)!; map.Name = "Darunia Middle"; map = town.GetTownMap(VanillaTownMap.DARUNIA_RIGHT)!; map.Name = "Darunia Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.DARUNIA_WIZARD)!; map.Name = "Darunia Wizard"; map.Collectable = Collectable.REFLECT_SPELL; @@ -130,10 +142,12 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "New Kasuto"; map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_LEFT)!; map.Name = "New Kasuto Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_MID)!; map.Name = "New Kasuto Middle"; map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_RIGHT)!; map.Name = "New Kasuto Right"; + map.RightExitIsOutside = false; map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_WIZARD)!; map.Name = "New Kasuto Wizard"; map.Collectable = Collectable.SPELL_SPELL; @@ -152,10 +166,12 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Old Kasuto"; map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_LEFT)!; map.Name = "Old Kasuto Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_MID)!; map.Name = "Old Kasuto Left"; map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_RIGHT)!; map.Name = "Old Kasuto Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_WIZARD)!; map.Name = "Old Kasuto Wizard"; map.Collectable = Collectable.THUNDER_SPELL; @@ -168,6 +184,7 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Bagu's House"; map = town.GetTownMap(VanillaTownMap.BAGU_EXTERNAL)!; map.Name = "Bagu Exterior"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.BAGU_INDOORS)!; map.Name = "Bagu Interior"; map.Collectable = Collectable.BAGUS_NOTE; diff --git a/Statistics/Statistics.cs b/Statistics/Statistics.cs index ee4e5f4d1..823538445 100644 --- a/Statistics/Statistics.cs +++ b/Statistics/Statistics.cs @@ -20,9 +20,9 @@ namespace Z2Randomizer.Statistics; /// class Statistics { - //private static readonly string FLAGS = "hEAK0thCqbLyhAAL4XpGU+!5@W4xeWvdAALhA"; //Random% vanilla + private static readonly string FLAGS = "hEABexKyA3zW5qAw0LJid9wd2COH+0c4YUCfstWWdAALh+h"; //Random% //private static readonly string FLAGS = "AAAA2dJALw3kToaJESXL#o4++o6WFU9WaAkyAAWAoh"; //Standard Swiss - private static readonly string FLAGS = "hEABexKyA3zWoqAw0LJidsqd2COQ+56swoE@WXss7AAWA+h"; //test + //private static readonly string FLAGS = "hEABexKyA3zWoqAw0LJidsqd2COQ+56swoE@WXss7AAWA+h"; //test //private static readonly string FLAGS = "g7+y2SHCACtOJ3hAAAAACsWhtocY+#caVWVprWmCQAABVAbh"; //short/short //private static readonly string FLAGS = "g7+y2SHCACtOJ3hAAAAACsWhtocY+#caVWVprWmCQAABVFbh"; //long/long From b3e6eefcfb3c4b5966dbe17f414bd06188fa1c68 Mon Sep 17 00:00:00 2001 From: Ellendar Date: Thu, 25 Jun 2026 03:11:03 -0400 Subject: [PATCH 06/10] Works but still not great. Internal rooms don't connect back, which doesn't currently matter but should be added for later Script management not implemented Possibly unrelated palace bug exists. --- CommandLine/Program.cs | 1 + .../ViewModels/GenerateRomViewModel.cs | 1 + DumpRooms/Program.cs | 1 + RandomizerCore/CustomTexts.cs | 128 +-- RandomizerCore/Enemy/Enemies.cs | 44 + RandomizerCore/Enemy/PalaceEnemyShuffler.cs | 1 + RandomizerCore/Hyrule.cs | 924 ++++++++---------- RandomizerCore/Music.cs | 9 +- RandomizerCore/Overworld/DeathMountain.cs | 42 +- RandomizerCore/Overworld/EastHyrule.cs | 235 ++--- RandomizerCore/Overworld/Location.cs | 110 ++- RandomizerCore/Overworld/LocationID.cs | 27 +- RandomizerCore/Overworld/MazeIsland.cs | 26 +- RandomizerCore/Overworld/WestHyrule.cs | 211 ++-- RandomizerCore/Overworld/World.cs | 83 +- RandomizerCore/ROM.cs | 80 +- RandomizerCore/RandomizerConfiguration.cs | 2 +- RandomizerCore/RandomizerProperties.cs | 2 +- RandomizerCore/RequirementType.cs | 2 + RandomizerCore/Requirements.cs | 3 + RandomizerCore/RomMap.cs | 1 + RandomizerCore/Shuffler.cs | 2 +- ...ranceDirectionItemRoomSelectionStrategy.cs | 2 +- .../ByShapeItemRoomSelectionStrategy.cs | 2 +- .../{ => Palace}/ChaosPalaceGenerator.cs | 4 +- .../{ => Palace}/CoordinatePalaceGenerator.cs | 2 +- .../{ => Palace}/ItemRoomSelectionStrategy.cs | 2 +- .../Sidescroll/{ => Palace}/Palace.cs | 126 ++- .../Sidescroll/{ => Palace}/PalaceColors.cs | 3 +- .../{ => Palace}/PalaceGenerator.cs | 2 +- .../Sidescroll/{ => Palace}/PalaceRooms.cs | 2 +- .../Sidescroll/{ => Palace}/Palaces.cs | 3 +- .../RandomItemRoomSelectionStrategy.cs | 2 +- .../RandomWalkCoordinatePalaceGenerator.cs | 2 +- .../ReconstructedLoopyPalaceGenerator.cs | 2 +- .../ReconstructedPalaceGenerator.cs | 6 +- .../Sidescroll/{ => Palace}/Room.cs | 2 +- .../Sidescroll/{ => Palace}/RoomExitType.cs | 2 +- RandomizerCore/Sidescroll/Palace/RoomGroup.cs | 6 + .../Sidescroll/{ => Palace}/RoomPool.cs | 2 +- .../{ => Palace}/SegmentConnectionType.cs | 2 +- ...ntialPlacementCoordinatePalaceGenerator.cs | 4 +- .../ShapeFirstCoordinatePalaceGenerator.cs | 4 +- .../TowerCoordinatePalaceGenerator.cs | 2 +- .../{ => Palace}/VanillaPalaceGenerator.cs | 8 +- .../{ => Palace}/VanillaRoomPool.cs | 2 +- .../VanillaShufflePalaceGenerator.cs | 2 +- .../VanillaWeightedPalaceGenerator.cs | 2 +- RandomizerCore/Sidescroll/RoomGroup.cs | 6 - RandomizerCore/Sidescroll/SideviewEnums.cs | 33 + RandomizerCore/Sidescroll/Town/Town.cs | 213 ++++ RandomizerCore/Sidescroll/Town/TownMap.cs | 49 + RandomizerCore/Sidescroll/Town/TownType.cs | 57 ++ RandomizerCore/Sidescroll/Town/Towns.cs | 182 ++++ .../Sidescroll/Town/VanillaTownMap.cs | 51 + RandomizerCore/Text.cs | 38 +- RandomizerCore/Town.cs | 135 --- RandomizerCore/TownEnum.cs | 137 +++ RandomizerCore/Towns.cs | 33 - RandomizerCore/Util.cs | 13 +- Statistics/Result.cs | 1 + Statistics/Statistics.cs | 1 + Tests/FlagsTests.cs | 2 +- Tests/RoomSerializationTests.cs | 1 + Tests/Utils.cs | 1 + ValidateRooms/Program.cs | 1 + Z2Randomizer.sln | 12 +- 67 files changed, 1782 insertions(+), 1317 deletions(-) rename RandomizerCore/Sidescroll/{ => Palace}/ByEntranceDirectionItemRoomSelectionStrategy.cs (96%) rename RandomizerCore/Sidescroll/{ => Palace}/ByShapeItemRoomSelectionStrategy.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/ChaosPalaceGenerator.cs (98%) rename RandomizerCore/Sidescroll/{ => Palace}/CoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/ItemRoomSelectionStrategy.cs (90%) rename RandomizerCore/Sidescroll/{ => Palace}/Palace.cs (96%) rename RandomizerCore/Sidescroll/{ => Palace}/PalaceColors.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/PalaceGenerator.cs (98%) rename RandomizerCore/Sidescroll/{ => Palace}/PalaceRooms.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/Palaces.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/RandomItemRoomSelectionStrategy.cs (98%) rename RandomizerCore/Sidescroll/{ => Palace}/RandomWalkCoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/ReconstructedLoopyPalaceGenerator.cs (96%) rename RandomizerCore/Sidescroll/{ => Palace}/ReconstructedPalaceGenerator.cs (98%) rename RandomizerCore/Sidescroll/{ => Palace}/Room.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/RoomExitType.cs (98%) create mode 100644 RandomizerCore/Sidescroll/Palace/RoomGroup.cs rename RandomizerCore/Sidescroll/{ => Palace}/RoomPool.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/SegmentConnectionType.cs (95%) rename RandomizerCore/Sidescroll/{ => Palace}/SequentialPlacementCoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/ShapeFirstCoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/TowerCoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/VanillaPalaceGenerator.cs (94%) rename RandomizerCore/Sidescroll/{ => Palace}/VanillaRoomPool.cs (97%) rename RandomizerCore/Sidescroll/{ => Palace}/VanillaShufflePalaceGenerator.cs (95%) rename RandomizerCore/Sidescroll/{ => Palace}/VanillaWeightedPalaceGenerator.cs (99%) delete mode 100644 RandomizerCore/Sidescroll/RoomGroup.cs create mode 100644 RandomizerCore/Sidescroll/Town/Town.cs create mode 100644 RandomizerCore/Sidescroll/Town/TownMap.cs create mode 100644 RandomizerCore/Sidescroll/Town/TownType.cs create mode 100644 RandomizerCore/Sidescroll/Town/Towns.cs create mode 100644 RandomizerCore/Sidescroll/Town/VanillaTownMap.cs delete mode 100644 RandomizerCore/Town.cs create mode 100644 RandomizerCore/TownEnum.cs delete mode 100644 RandomizerCore/Towns.cs diff --git a/CommandLine/Program.cs b/CommandLine/Program.cs index 63c085c7f..b30c5052f 100644 --- a/CommandLine/Program.cs +++ b/CommandLine/Program.cs @@ -6,6 +6,7 @@ using NLog; using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.CommandLine; diff --git a/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs b/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs index 6106c28f7..8bf7dcf41 100644 --- a/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs +++ b/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs @@ -17,6 +17,7 @@ using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; using CrossPlatformUI.Services; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace CrossPlatformUI.ViewModels; diff --git a/DumpRooms/Program.cs b/DumpRooms/Program.cs index 70c0788c0..e65401844 100644 --- a/DumpRooms/Program.cs +++ b/DumpRooms/Program.cs @@ -5,6 +5,7 @@ using System.Text.Json; using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; ROM ROMData = new ROM(args[0], true); diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index 0e09c2b12..22a5c6e2e 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using Z2Randomizer.RandomizerCore.Overworld; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -76,16 +77,16 @@ public class CustomTexts ]; //Indexes in the hints list - private static readonly Dictionary TOWN_SIGN_INDEXES = new() + private static readonly Dictionary TOWN_SIGN_INDEXES = new() { - {Town.RAURU, 11}, - {Town.RUTO, 20}, - {Town.SARIA_NORTH, 29}, - {Town.MIDO_WEST, 41}, - {Town.NABOORU, 62}, - {Town.DARUNIA_WEST, 76}, - {Town.NEW_KASUTO, 86}, - {Town.OLD_KASUTO, 94}, + {TownType.RAURU, 11}, + {TownType.RUTO, 20}, + {TownType.SARIA, 29}, + {TownType.MIDO, 41}, + {TownType.NABOORU, 62}, + {TownType.DARUNIA, 76}, + {TownType.NEW_KASUTO, 86}, + {TownType.OLD_KASUTO, 94}, }; public const int WEST_TEXT_COUNT = 52; @@ -113,18 +114,16 @@ public class CustomTexts private const int HELPFUL_HINTS_COUNT = 4; //private static readonly Dictionary spellTextIndexes = { 15, 24, 35, 46, 70, 81, 93, 96 }; - private static readonly Dictionary townWizardTextIndexes = new() + private static readonly Dictionary townWizardTextIndexes = new() { - { Town.RAURU, 15 }, - { Town.RUTO, 24 }, - { Town.SARIA_NORTH, 35 }, - { Town.MIDO_WEST, 46 }, - { Town.MIDO_CHURCH, downstabGuyGotItemTextIndex }, - { Town.NABOORU, 70 }, - { Town.DARUNIA_ROOF, upstabGuyGotItemTextIndex }, - { Town.DARUNIA_WEST, 81 }, - { Town.NEW_KASUTO, 93 }, - { Town.OLD_KASUTO, 96 }, + { TownType.RAURU, 15 }, + { TownType.RUTO, 24 }, + { TownType.SARIA, 35 }, + { TownType.MIDO, 46 }, + { TownType.NABOORU, 70 }, + { TownType.DARUNIA, 81 }, + { TownType.NEW_KASUTO, 93 }, + { TownType.OLD_KASUTO, 96 }, }; private static readonly Dictionary wizardTextIndexesBySpell = new() @@ -387,18 +386,16 @@ public class CustomTexts "i wish$i had a$%%$like yours" ]; - public static readonly Dictionary WIZARD_SPELL_TEXTS_BY_TOWN = new() + public static readonly Dictionary WIZARD_SPELL_TEXTS_BY_TOWN = new() { - { Town.RAURU, [] }, - { Town.RUTO, ["A winner$is you"] }, - { Town.SARIA_NORTH, ["Water$you$doing?"] }, - { Town.MIDO_WEST, [] }, - { Town.MIDO_CHURCH, [] }, - { Town.NABOORU, ["Do not$drink the$fountain$water here$...$trust me."] }, - { Town.DARUNIA_ROOF, [] }, - { Town.DARUNIA_WEST, ["You saved$a kid$for this?", "Dont$forget to$get upstab"] }, - { Town.NEW_KASUTO, [] }, - { Town.OLD_KASUTO, ["Sorry$about the$moas"] } + { TownType.RAURU, [] }, + { TownType.RUTO, ["A winner$is you"] }, + { TownType.SARIA, ["Water$you$doing?"] }, + { TownType.MIDO, [] }, + { TownType.NABOORU, ["Do not$drink the$fountain$water here$...$trust me."] }, + { TownType.DARUNIA, ["You saved$a kid$for this?", "Dont$forget to$get upstab"] }, + { TownType.NEW_KASUTO, [] }, + { TownType.OLD_KASUTO, ["Sorry$about the$moas"] } }; public static readonly Dictionary WIZARD_SPELL_TEXTS_BY_COLLECTABLE = new() @@ -535,26 +532,29 @@ public static List GenerateTexts( GenerateSpellHints(locations, texts, props); } - Location baguLocation = locations.FirstOrDefault(i => i.ActualTown == Town.BAGU)!; + Location baguLocation = locations.FirstOrDefault(i => i.Town?.Type == TownType.BAGU)!; if(baguLocation == null) { throw new Exception("Bagu location not found while generating text"); } - Text? baguText = GenerateBaguText(baguLocation.Collectables[0], nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); + Text? baguText = GenerateBaguText(baguLocation.GetAllCollectables()[0], nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); if(baguText != null) { texts[baguTextIndex] = baguText; } - Location tableLocation = locations.FirstOrDefault(i => i.ActualTown == Town.SARIA_TABLE)!; - Text? mirrorText = GenerateMirrorTableText(tableLocation.Collectables[0], nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); + Collectable tableColletable = (Collectable)locations.First(i => i.Town?.Type == TownType.SARIA) + .Town!.GetTownMap(VanillaTownMap.SARIA_TABLE)!.Collectable!; + Text? mirrorText = GenerateMirrorTableText(tableColletable, nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); if (mirrorText != null) { texts[gotMirrorTextIndex] = mirrorText; } - Location fountainLocation = locations.FirstOrDefault(i => i.ActualTown == Town.NABOORU_FOUNTAIN)!; - Text? fountainText = GenerateFountainText(fountainLocation.Collectables[0], nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); + + Collectable fountainCollectable = (Collectable)locations.First(i => i.Town?.Type == TownType.NABOORU) + .Town!.GetTownMap(VanillaTownMap.NABOORU_MID)!.Collectable!; + Text? fountainText = GenerateFountainText(fountainCollectable, nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); if (fountainText != null) { texts[gotWaterTextIndex] = fountainText; @@ -571,17 +571,17 @@ public static List GenerateTexts( if (props.SpellItemHints && props.IncludeSwordTechsInShuffle) { - var downstabLoc = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.DOWNSTAB)); - var upstabLoc = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.UPSTAB)); + var downstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.DOWNSTAB)); + var upstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.UPSTAB)); Text hint; - if (downstabLoc == null) + if (props.StartWithDownstab) { hint = props.UseCommunityText ? new Text(STARTED_WITH_ITEM_TEXTS.Sample(nonhashRNG)!, Collectable.DOWNSTAB) : new Text(NON_COMMUNITY_STARTED_WITH_TEXT, Collectable.DOWNSTAB); } else { - hint = Text.GenerateHelpfulHint(locations.ToList(), downstabLoc, Collectable.DOWNSTAB, props.IncludeSpellsInShuffle); + hint = Text.GenerateHelpfulHint(downstabLoc, Collectable.DOWNSTAB, props.IncludeSpellsInShuffle); } texts[downstabClosedDoorTextIndex] = hint; if (upstabLoc == null) @@ -591,7 +591,7 @@ public static List GenerateTexts( } else { - hint = Text.GenerateHelpfulHint(locations.ToList(), upstabLoc, Collectable.UPSTAB, props.IncludeSpellsInShuffle); + hint = Text.GenerateHelpfulHint(upstabLoc, Collectable.UPSTAB, props.IncludeSpellsInShuffle); } texts[upstabClosedDoorTextIndex] = hint; if (props.SwapUpAndDownStab) @@ -683,9 +683,9 @@ private static Text GenerateBaguWoodsHint(Location bagu) private static void GenerateTownNameHints(List texts, IEnumerable locations, bool linkedFire) { - foreach (Location location in locations.Where(i => i.ActualTown != null && i.VanillaCollectable.IsSpell())) + foreach (Location location in locations.Where(i => i.Town?.GetWizard() != null)) { - texts[TOWN_SIGN_INDEXES[(Town)location.ActualTown!]] = GenerateTownSignHint(location.Collectables[0], linkedFire); + texts[TOWN_SIGN_INDEXES[(TownType)location.Town!.Type!]] = GenerateTownSignHint((Collectable)location.Town.GetWizard()!.Collectable!, linkedFire); } } private static Text GenerateTownSignHint(Collectable spell, bool linkedFire) @@ -701,12 +701,12 @@ private static Text GenerateTownSignHint(Collectable spell, bool linkedFire) private static Text GenerateWizardText(List texts, Random r, Location location, bool useCommunityText) { - if(location.ActualTown == null) + if(location.Town == null) { throw new Exception("Cannot generate text for Wizard outside of town"); } - Town town = (Town)location.ActualTown; - Collectable collectable = location.Collectables[0]; + TownType town = (TownType)location.Town.Type!; + Collectable collectable = (Collectable)location.Town.GetWizard()!.Collectable!; if (collectable.IsSpell() || collectable == Collectable.DOWNSTAB || collectable == Collectable.UPSTAB) @@ -860,7 +860,7 @@ private static List GenerateHelpfulHints(List hints, IEnumerable placedTowns = []; - List items = locations.SelectMany(i => i.Collectables).ToList(); + List items = locations.SelectMany(i => i.GetAllCollectables()).ToList(); items = items.Where(i => !i.IsInternalUse()).ToList(); if (props.SpellItemHints && props.IncludeSwordTechsInShuffle) @@ -921,7 +921,7 @@ private static List GenerateHelpfulHints(List hints, IEnumerable possibleHintLocations = locations.Where(i => i.Collectables.Contains(hintCollectable)).ToList(); + List possibleHintLocations = locations.Where(i => i.GetAllCollectables().Contains(hintCollectable)).ToList(); Location hintLocation; int town; @@ -941,9 +941,9 @@ private static List GenerateHelpfulHints(List hints, IEnumerable locations, List i.Collectables.Contains(Collectable.TROPHY))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.TROPHY))!; if(itemLocation != null) { - Text trophyHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.TROPHY, props.IncludeSpellsInShuffle); + Text trophyHint = Text.GenerateHelpfulHint(itemLocation, Collectable.TROPHY, props.IncludeSpellsInShuffle); hints[trophySpellHintIndex] = trophyHint; } - itemLocation = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.MEDICINE))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.MEDICINE))!; if (itemLocation != null) { - Text medHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.MEDICINE, props.IncludeSpellsInShuffle); + Text medHint = Text.GenerateHelpfulHint(itemLocation, Collectable.MEDICINE, props.IncludeSpellsInShuffle); hints[medicineSpellHintIndex] = medHint; } - itemLocation = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.CHILD))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.CHILD))!; if (itemLocation != null) { - Text kidHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.CHILD, props.IncludeSpellsInShuffle); + Text kidHint = Text.GenerateHelpfulHint(itemLocation, Collectable.CHILD, props.IncludeSpellsInShuffle); hints[childSpellHintIndex] = kidHint; } if (props.IncludeQuestItemsInShuffle) { - itemLocation = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.MIRROR))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.MIRROR))!; if (itemLocation != null) { - Text mirrorHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.MIRROR, props.IncludeSpellsInShuffle); + Text mirrorHint = Text.GenerateHelpfulHint(itemLocation, Collectable.MIRROR, props.IncludeSpellsInShuffle); hints[mirrorSpellHintIndex] = mirrorHint; } - itemLocation = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.WATER))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.WATER))!; if (itemLocation != null) { - Text waterHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.WATER, props.IncludeSpellsInShuffle); + Text waterHint = Text.GenerateHelpfulHint(itemLocation, Collectable.WATER, props.IncludeSpellsInShuffle); hints[waterSpellHintIndex] = waterHint; } } @@ -1027,10 +1027,10 @@ private static void GenerateWizardTexts(List texts, IEnumerable { List vanillaText = new(texts); List usedWizardTexts = []; - List wizardLocations = itemLocs.Where(i => i.ActualTown != 0 && (i?.ActualTown?.IsWizardTown() ?? false)).ToList(); + List wizardLocations = itemLocs.Where(i => i.Town?.GetWizard() != null).ToList(); foreach (Location location in wizardLocations) { - if(location.ActualTown == null) + if(location.Town == null) { throw new Exception("Invalid town that is not a town"); } @@ -1041,7 +1041,7 @@ private static void GenerateWizardTexts(List texts, IEnumerable wizardHint = GenerateWizardText(vanillaText, r, location, useCommunityText); } while (useCommunityText && usedWizardTexts.Contains(wizardHint) && tries++ < 100); usedWizardTexts.Add(wizardHint); - texts[townWizardTextIndexes[(Town)location.ActualTown]] = wizardHint; + texts[townWizardTextIndexes[(TownType)location.Town.Type!]] = wizardHint; } } diff --git a/RandomizerCore/Enemy/Enemies.cs b/RandomizerCore/Enemy/Enemies.cs index 10865afea..949e8dafb 100644 --- a/RandomizerCore/Enemy/Enemies.cs +++ b/RandomizerCore/Enemy/Enemies.cs @@ -281,6 +281,50 @@ public enum EnemiesEast BOULDER_TOSSING_LIZALFOS = 0x1D, } +/// +/// IDs for town "enemies" +/// +public enum EnemiesTown +{ + FAIRY = 0x00, + CANDLE = 0x01, + DOOR = 0x02, + PURPLE_KEESE_03 = 0x03, + PURPLE_BOT = 0x04, + BIT = 0x05, + PURPLE_MOA = 0x06, + PURPLE_KEESE_07 = 0x07, + GOLD_GIRL = 0x08, + TOWNPERSON_09 = 0x09, + TALKING_ACHE = 0x0A, + TALKING_BIT = 0x0B, + INVISIBLE_DIALOG = 0x0C, + RIVER_MAN = 0x0D, + BLUE_LUMBERJACK_0E = 0x0E, + WIZARD = 0x0F, + KID = 0x10, + STATIONARY_BLUE_LADY_WITH_BOWL_11 = 0x11, + STATIONARY_RED_GRANNY = 0x12, + STATIONARY_RED_LUMBERJACK = 0x13, + STATIONARY_BLUE_LADY_WITH_BOWL_14 = 0x14, + STATIONARY_BLUE_GRANNY = 0x15, + STATIONARY_RED_LADY = 0x16, + LIFE_REFILL_LADY = 0x17, + MAGIC_REFILL_GRANNY = 0x18, + BLUE_LUMBERJACK_19 = 0x19, + RED_LADY_WITH_BOWL = 0x1A, + RED_GRANNY = 0x1B, + BLUE_LADY = 0x1C, + BLUE_LUMBERJACK_1D = 0x1D, + RED_GUY_1E = 0x1E, + RED_GUY_1F = 0x1F, + WALKING_LADY = 0x20, + MIRROR = 0x21, + SIGN = 0x22, + BUSY_TOWNSPERSON_GENERATOR = 0x23 + +} + /// /// IDs that are shared in palace 1-6 /// diff --git a/RandomizerCore/Enemy/PalaceEnemyShuffler.cs b/RandomizerCore/Enemy/PalaceEnemyShuffler.cs index 3985776c0..f9d0620be 100644 --- a/RandomizerCore/Enemy/PalaceEnemyShuffler.cs +++ b/RandomizerCore/Enemy/PalaceEnemyShuffler.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore.Enemy; diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 8f01c64f7..503dfee17 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -1,4 +1,9 @@ -using System; +using DynamicData; +using FtRandoLib.Importer; +using js65; +using NLog; +using NLog.Targets; +using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; @@ -8,12 +13,11 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using NLog; -using js65; -using FtRandoLib.Importer; using Z2Randomizer.RandomizerCore.Enemy; using Z2Randomizer.RandomizerCore.Overworld; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -261,7 +265,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC } } ItemGet.Remove(props.ReplaceFireWithDash ? Collectable.FIRE_SPELL : Collectable.DASH_SPELL); - accessibleMagicContainers = 4; + reachableAreas = new HashSet(); //areasByLocation = new SortedDictionary>(); @@ -315,12 +319,9 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC AsmModule sideviewModule = new(); passedValidation = await FillPalaceRooms(sideviewModule); - assembler.Add(sideviewModule); } - //palaces.ForEach(i => Debug.WriteLine(i.GetLayoutDebug(PalaceStyle.TOWER, false))); - ROMData.WriteKasutoJarAmount(props.NewKasutoBasementRequirement); ROMData.DoHackyFixes(); ROMData.AdjustGpProjectileDamage(); @@ -346,6 +347,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC firstProcessOverworldTimestamp = DateTime.Now; await ProcessOverworld(progress, ct); + if (ct.IsCancellationRequested) { return new RandomizerResult(false); } UpdateProgress(progress, 8); @@ -360,43 +362,29 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC { List? customSpellOrder = props.IncludeSpellsInShuffle ? null - : AllLocationsForReal() - .Where(l => l.ActualTown != null && Towns.STRICT_SPELL_LOCATIONS.Contains((Town)l.ActualTown)) - //This makes the assumption that is currently true that each "town" has exactly one item. - //If we later restructure towns to be omni-towns to get rid of fake towns, this will be untrue - .Select(l => l.Collectables[0]).ToList(); + : AllLocations() + .Where(l => l.Town?.GetWizard() != null) + .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); ROMData.CombineFireSpell(assembler, customSpellOrder, r); } - Dictionary spellMap = new() - { - { (Town)westHyrule.locationAtRauru.ActualTown!, westHyrule.locationAtRauru.Collectables[0] }, - { (Town)westHyrule.locationAtMido.ActualTown!, westHyrule.locationAtMido.Collectables[0] }, - { (Town)westHyrule.locationAtSariaNorth.ActualTown!, westHyrule.locationAtSariaNorth.Collectables[0] }, - { (Town)westHyrule.locationAtRuto.ActualTown!, westHyrule.locationAtRuto.Collectables[0] }, - { (Town)eastHyrule.townAtNabooru.ActualTown!, eastHyrule.townAtNabooru.Collectables[0] }, - { (Town)eastHyrule.townAtDarunia.ActualTown!, eastHyrule.townAtDarunia.Collectables[0] }, - { (Town)eastHyrule.townAtNewKasuto.ActualTown!, eastHyrule.townAtNewKasuto.Collectables[0] }, - { (Town)eastHyrule.townAtOldKasuto.ActualTown!, eastHyrule.townAtOldKasuto.Collectables[0] }, - }; - - if (props.StartsWithCollectable(spellMap[Town.RUTO])) + if (props.StartsWithCollectable(westHyrule.ruto.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b14, 0x10); //Trophy } - if (props.StartsWithCollectable(spellMap[Town.SARIA_NORTH])) + if (props.StartsWithCollectable(westHyrule.sariaNorth.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b15, 0x01); //Mirror } - if (props.StartsWithCollectable(spellMap[Town.MIDO_WEST])) + if (props.StartsWithCollectable(westHyrule.mido.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b16, 0x40); //Medicine } - if (props.StartsWithCollectable(spellMap[Town.NABOORU])) + if (props.StartsWithCollectable(eastHyrule.nabooru.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b17, 0x01); //Water } - if (props.StartsWithCollectable(spellMap[Town.DARUNIA_WEST])) + if (props.StartsWithCollectable(eastHyrule.darunia.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b18, 0x20); //Child } @@ -408,7 +396,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC if (ct.IsCancellationRequested) { return new RandomizerResult(false); } UpdateProgress(progress, 9); - List texts = CustomTexts.GenerateTexts(AllLocationsForReal(), itemLocs, ROMData.GetGameText(), props, r); + List texts = CustomTexts.GenerateTexts(AllLocations(), itemLocs, ROMData.GetGameText(), props, r); StatRandomizer randomizedStats = new(ROMData, props); randomizedStats.Randomize(r, skipDifficultyOnly: shareSeedAcrossDifficulty); @@ -651,75 +639,67 @@ private static void SanitizeHashCharacters(byte[] z2Hash) // hand out a duplicate. private void ReplaceDifficultyStartingItemsInWorld(Random r) { - List minorItems = [ - Collectable.BLUE_JAR, Collectable.RED_JAR, Collectable.SMALL_BAG, - Collectable.MEDIUM_BAG, Collectable.LARGE_BAG, Collectable.XL_BAG, - Collectable.ONEUP, Collectable.KEY - ]; Collectable[] difficultyStartingItems = [Collectable.CANDLE, Collectable.CROSS]; - + Collectable[] minorItems = Enum.GetValues().Where(i => i.IsMinorItem()).ToArray(); foreach (Collectable item in difficultyStartingItems) { if (!props.StartsWithCollectable(item)) { continue; } + Collectable replacementItem = minorItems.Sample(r); foreach (Location location in itemLocs) { - for (int i = 0; i < location.Collectables.Count; i++) + if(location.ReplaceCollectable(item, replacementItem)) { - if (location.Collectables[i] == item) - { - location.Collectables[i] = minorItems.Sample(r); - } + return; } } } } - /* - Text Notes: - - Community Text Changes - ---------------------- - Shield Spell 15 43 - Cannot Help 16 35 - Jump Spell 24 34 - Life Spell 35 37 - You know..? 37 42 - Fairy 46 37 - Downstab 47 38 - Bagu 48 44 - Fire 70 43 - You know 71 34 - Reflect 81 37 - Upstab 82 32 - Spell 93 25 - Thunder 96 36 - */ + //Shuffle the palace itself, not just the metadata + private void ShufflePalaces() + { + if (!props.PalacesCanSwapContinent) return; + List palaces = [westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, westHyrule.locationAtPalace3, mazeIsland.locationAtPalace4, eastHyrule.locationAtPalace5, eastHyrule.locationAtPalace6]; + + if (props.P7shuffle) + { + palaces.Add(eastHyrule.locationAtGP); + } + + for (int i = palaces.Count - 1; i > 0; i--) + { + int swap = r.Next(i + 1); + (palaces[i].Palace, palaces[swap].Palace) = (palaces[swap].Palace, palaces[i].Palace); + } + + } private void ShuffleItems() { - List shufflableItems = [ - Collectable.CANDLE, Collectable.GLOVE, Collectable.RAFT, Collectable.BOOTS, - Collectable.FLUTE, Collectable.CROSS, Collectable.HEART_CONTAINER, Collectable.HEART_CONTAINER, - Collectable.MAGIC_CONTAINER, Collectable.MEDICINE, Collectable.TROPHY, Collectable.HEART_CONTAINER, - Collectable.HEART_CONTAINER, Collectable.MAGIC_CONTAINER, Collectable.MAGIC_KEY, Collectable.MAGIC_CONTAINER, - Collectable.HAMMER, Collectable.CHILD, Collectable.MAGIC_CONTAINER]; + List shufflableItems = []; + List minorItems = [Collectable.BLUE_JAR, Collectable.RED_JAR, Collectable.SMALL_BAG, Collectable.MEDIUM_BAG, Collectable.LARGE_BAG, Collectable.XL_BAG, Collectable.ONEUP, Collectable.KEY]; if (props.PbagItemShuffle) { - westHyrule.pbagCave.Collectables = [(Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE)]; - eastHyrule.pbagCave1.Collectables = [(Collectable)ROMData.GetByte(RomMap.EAST_PBAG_CAVE1_COLLECTABLE)]; - eastHyrule.pbagCave2.Collectables = [(Collectable)ROMData.GetByte(RomMap.EAST_PBAG_CAVE2_COLLECTABLE)]; - shufflableItems.Add(westHyrule.pbagCave.Collectables[0]); - shufflableItems.Add(eastHyrule.pbagCave1.Collectables[0]); - shufflableItems.Add(eastHyrule.pbagCave2.Collectables[0]); + Collectable collectable = (Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE); + westHyrule.pbagCave.SetCollectables([collectable]); + shufflableItems.Add(collectable); + + collectable = (Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE); + eastHyrule.pbagCave1.SetCollectables([collectable]); + shufflableItems.Add(collectable); + + collectable = (Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE); + eastHyrule.pbagCave2.SetCollectables([collectable]); + shufflableItems.Add(collectable); } else { - westHyrule.pbagCave.Collectables = [Collectable.LARGE_BAG]; - eastHyrule.pbagCave1.Collectables = [Collectable.LARGE_BAG]; - eastHyrule.pbagCave2.Collectables = [Collectable.XL_BAG]; + westHyrule.pbagCave.SetCollectables([Collectable.LARGE_BAG]); + eastHyrule.pbagCave1.SetCollectables([Collectable.LARGE_BAG]); + eastHyrule.pbagCave2.SetCollectables([Collectable.XL_BAG]); } if(props.IncludeSpellsInShuffle) @@ -791,32 +771,59 @@ private void ShuffleItems() } pbagHearts = []; + List heartContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.HEART_CONTAINER)).ToList(); + //Replace any unused heart containers with small items if (heartContainersInItemPool < 4) { int heartContainersToRemove = 4 - heartContainersInItemPool; while (heartContainersToRemove > 0) { - int remove = r.Next(shufflableItems.Count); - if (shufflableItems[remove] == Collectable.HEART_CONTAINER) + Location hcLocation = heartContainerLocations.Sample(r)!; + List collectables = hcLocation.GetAllCollectables(); + List order = Enumerable.Range(0, collectables.Count).ToList(); + order.FisherYatesShuffle(r); + foreach(int index in order) { - shufflableItems[remove] = minorItems[r.Next(minorItems.Count)]; - heartContainersToRemove--; + if (collectables[index] == Collectable.HEART_CONTAINER) + { + collectables[index] = minorItems.Sample(r); + hcLocation.SetCollectables(collectables); + heartContainersToRemove--; + if (!collectables.Any(i => i == Collectable.HEART_CONTAINER)) + { + heartContainerLocations.Remove(hcLocation); + } + break; + } } } } + List magicContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.MAGIC_CONTAINER)).ToList(); //Replace any unused magic containers with small items if (magicContainersInItemPool < 4) { int magicContainersToRemove = 4 - magicContainersInItemPool; while (magicContainersToRemove > 0) { - int remove = r.Next(shufflableItems.Count); - if (shufflableItems[remove] == Collectable.MAGIC_CONTAINER) + Location mcLocation = magicContainerLocations.Sample(r)!; + List collectables = mcLocation.GetAllCollectables(); + List order = Enumerable.Range(0, collectables.Count).ToList(); + order.FisherYatesShuffle(r); + foreach (int index in order) { - shufflableItems[remove] = minorItems[r.Next(minorItems.Count)]; - magicContainersToRemove--; + if (collectables[index] == Collectable.MAGIC_CONTAINER) + { + collectables[index] = minorItems.Sample(r); + mcLocation.SetCollectables(collectables); + magicContainersToRemove--; + if (!collectables.Any(i => i == Collectable.MAGIC_CONTAINER)) + { + magicContainerLocations.Remove(mcLocation); + } + break; + } } } } @@ -837,25 +844,26 @@ private void ShuffleItems() } else if(!props.IncludeSpellsInShuffle) { - Collectable townCollectable; - townCollectable = westHyrule.AllLocations.First(i => i.ActualTown == Town.RUTO).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + Collectable wizardCollectable; + //wizardCollectable = westHyrule.AllLocations.First(i => i.ActualTown == TownType.RUTO).Collectables[0]; + wizardCollectable = (Collectable)westHyrule.ruto.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[10] == Collectable.TROPHY); shufflableItems[10] = minorItems[r.Next(minorItems.Count)]; ItemGet[Collectable.TROPHY] = true; } - townCollectable = westHyrule.AllLocations.First(i => i.ActualTown == Town.MIDO_WEST).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + wizardCollectable = (Collectable)westHyrule.mido.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[9] == Collectable.MEDICINE); shufflableItems[9] = minorItems[r.Next(minorItems.Count)]; ItemGet[Collectable.MEDICINE] = true; } - townCollectable = eastHyrule.AllLocations.First(i => i.ActualTown == Town.DARUNIA_WEST).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + wizardCollectable = (Collectable)eastHyrule.darunia.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[17] == Collectable.CHILD); shufflableItems[17] = minorItems[r.Next(minorItems.Count)]; @@ -864,16 +872,16 @@ private void ShuffleItems() if (props.IncludeQuestItemsInShuffle) { - townCollectable = westHyrule.AllLocations.First(i => i.ActualTown == Town.SARIA_NORTH).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + wizardCollectable = (Collectable)westHyrule.sariaNorth.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[mirrorIndex] == Collectable.MIRROR); shufflableItems[mirrorIndex] = minorItems[r.Next(minorItems.Count)]; ItemGet[Collectable.MIRROR] = true; } - townCollectable = eastHyrule.AllLocations.First(i => i.ActualTown == Town.NABOORU).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + wizardCollectable = (Collectable)eastHyrule.nabooru.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[waterindex] == Collectable.WATER); shufflableItems[waterindex] = minorItems[r.Next(minorItems.Count)]; @@ -921,7 +929,18 @@ private void ShuffleItems() } } - if(props.IncludeSwordTechsInShuffle) + TownMap? townMap = itemLocs.SelectMany(i => i.Town?.TownMaps ?? []).FirstOrDefault(i => i.Map == (int)VanillaTownMap.MIDO_TRAINER); + if(townMap != null) + { + townMap.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + } + townMap = itemLocs.SelectMany(i => i.Town?.TownMaps ?? []).FirstOrDefault(i => i.Map == (int)VanillaTownMap.DARUNIA_TRAINER); + if (townMap != null) + { + townMap.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + } + + if (props.IncludeSwordTechsInShuffle) { if (props.StartWithDownstab) { @@ -1023,7 +1042,7 @@ private void ShuffleItems() //Do the actual shuffling List duplicateItemPlacementCandidates = []; - palaces.ForEach(i => i.ItemRooms.ForEach(j => j.Collectable = null)); + if (props.MixOverworldPalaceItems) { duplicateItemPlacementCandidates.AddRange(itemLocs); @@ -1038,13 +1057,13 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location palaceLocation in itemLocs.Where(i => i.PalaceNumber != null)) + foreach (Location palaceLocation in itemLocs.Where(i => i.Palace != null)) { shufflableItemLocations.Add(palaceLocation); - Collectable vanillaCollectable = Palace.GetVanillaCollectable(palaceLocation.PalaceNumber); + Collectable vanillaCollectable = palaceLocation.Palace!.GetVanillaCollectable(); itemsToActuallyShuffle.Add(shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)); - for (int i = 1; i < props.PalaceItemRoomCounts[(int)palaceLocation.PalaceNumber! - 1]; i++) + for (int i = 1; i < props.PalaceItemRoomCounts[(int)palaceLocation.Palace.Number! - 1]; i++) { itemsToActuallyShuffle.Add(minorItems.Sample(r)); } @@ -1052,22 +1071,23 @@ private void ShuffleItems() duplicateItemPlacementCandidates.AddRange(shufflableItemLocations); DoShuffle(itemsToActuallyShuffle, shufflableItemLocations); } - else + else //no shuffle palace items { - foreach (Location palaceLocation in itemLocs.Where(i => i.PalaceNumber != null && i.PalaceNumber < 7)) + foreach (Location palaceLocation in itemLocs.Where(i => i.Palace != null && i.Palace.Number < 7)) { - Collectable vanillaCollectable = Palace.GetVanillaCollectable(palaceLocation.PalaceNumber); - palaceLocation.Collectables = [shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)]; - palaces[(int)palaceLocation.PalaceNumber! - 1].ItemRooms.Sample(r)!.Collectable = vanillaCollectable; - for (int i = 0; i < props.PalaceItemRoomCounts[(int)palaceLocation.PalaceNumber! - 1]; i++) + int palaceNumber = palaceLocation.Palace!.Number; + Collectable vanillaCollectable = palaceLocation.Palace!.GetVanillaCollectable(); + List collectables = []; + collectables = [shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)]; + for (int i = 0; i < props.PalaceItemRoomCounts[(int)palaceNumber! - 1]; i++) { - if (palaces[(int)palaceLocation.PalaceNumber! - 1].ItemRooms[i].Collectable == null) + if (palaces[(int)palaceNumber! - 1].ItemRooms[i].Collectable == null) { Collectable smallItem = minorItems.Sample(r); - palaceLocation.Collectables.Add(smallItem); - palaces[(int)palaceLocation.PalaceNumber! - 1].ItemRooms[i].Collectable = smallItem; + collectables.Add(smallItem); } } + palaceLocation.SetCollectables(collectables); } } @@ -1075,91 +1095,90 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location nonPalaceLocation in itemLocs.Where(i => i.PalaceNumber == null)) + foreach (Location nonPalaceLocation in itemLocs.Where(i => i.Palace == null)) { shufflableItemLocations.Add(nonPalaceLocation); - itemsToActuallyShuffle.Add(shufflableItems.Contains(nonPalaceLocation.VanillaCollectable) - ? nonPalaceLocation.VanillaCollectable : minorItems.Sample(r)); + foreach(Collectable collectable in nonPalaceLocation.GetAllCollectables()) + { + itemsToActuallyShuffle.Add(shufflableItems.Contains(collectable) ? collectable : minorItems.Sample(r)); + } } duplicateItemPlacementCandidates.AddRange(shufflableItemLocations); DoShuffle(itemsToActuallyShuffle, shufflableItemLocations); } else { - foreach (Location nonPalaceLocation in itemLocs.Where(i => i.PalaceNumber == null)) + foreach (Location nonPalaceLocation in itemLocs.Where(i => i.Palace == null)) { - Collectable vanillaCollectable = nonPalaceLocation.VanillaCollectable; - nonPalaceLocation.Collectables = [shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)]; + List collectables = nonPalaceLocation.GetAllCollectables(); + for(int i = 0; i < collectables.Count; i++) + { + collectables[i] = shufflableItems.Contains(collectables[i]) ? collectables[i] : minorItems.Sample(r); + } + nonPalaceLocation.SetCollectables(collectables); } } } //Assigning unshuffled locations can make the number of containers wrong, so re-adjust them - List heartContainerLocations = itemLocs.Where(i => i.Collectables.Contains(Collectable.HEART_CONTAINER)).ToList(); - int heartContainerCount = itemLocs.SelectMany(i => i.Collectables).Count(i => i == Collectable.HEART_CONTAINER); + heartContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.HEART_CONTAINER)).ToList(); + int heartContainerCount = heartContainerLocations.SelectMany(i => i.GetAllCollectables()).Count(i => i == Collectable.HEART_CONTAINER); while(heartContainerCount > heartContainersInItemPool) { Location location = heartContainerLocations.Sample(r)!; - int index = r.Next(location.Collectables.Count); - if (location.Collectables[index] == Collectable.HEART_CONTAINER) + Collectable minorItem = minorItems.Sample(r); + if(!location.ReplaceCollectable(Collectable.HEART_CONTAINER, minorItem)) { - location.Collectables[index] = minorItems.Sample(r); - heartContainerCount--; - if (!location.Collectables.Any(i => i == Collectable.HEART_CONTAINER)) - { - heartContainerLocations.Remove(location); - } + heartContainerLocations.Remove(location); } } - while(heartContainerCount < heartContainersInItemPool) - { - List minorItemLocations = itemLocs.Where(i => i.Collectables.Any(j => j.IsMinorItem())).ToList(); + List minorItemLocations = AllLocations().Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); + while (heartContainerCount < heartContainersInItemPool) + { Location location = minorItemLocations.Sample(r)!; - int index = r.Next(location.Collectables.Count); - if (location.Collectables[index].IsMinorItem()) + Collectable[] locationMinorItems = location.GetAllCollectables().Where(i => i.IsMinorItem()).ToArray(); + if(locationMinorItems.Length == 0) { - location.Collectables[index] = Collectable.HEART_CONTAINER; - heartContainerCount++; - if (!location.Collectables.Any(i => i.IsMinorItem())) - { - minorItemLocations.Remove(location); - } + minorItemLocations.Remove(location); + continue; } + if (!location.ReplaceCollectable(locationMinorItems.Sample(r), Collectable.HEART_CONTAINER)) + { + heartContainerLocations.Remove(location); + } + heartContainerCount++; } - List magicContainerLocations = itemLocs.Where(i => i.Collectables.Contains(Collectable.MAGIC_CONTAINER)).ToList(); - int magicContainerCount = itemLocs.SelectMany(i => i.Collectables).Count(i => i == Collectable.MAGIC_CONTAINER); + + magicContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.MAGIC_CONTAINER)).ToList(); + int magicContainerCount = magicContainerLocations.SelectMany(i => i.GetAllCollectables()).Count(i => i == Collectable.MAGIC_CONTAINER); while (magicContainerCount > magicContainersInItemPool) { Location location = magicContainerLocations.Sample(r)!; - int index = r.Next(location.Collectables.Count); - if (location.Collectables[index] == Collectable.MAGIC_CONTAINER) + Collectable minorItem = minorItems.Sample(r); + if (!location.ReplaceCollectable(Collectable.MAGIC_CONTAINER, minorItem)) { - location.Collectables[index] = minorItems.Sample(r); - magicContainerCount--; - if (!location.Collectables.Any(i => i == Collectable.MAGIC_CONTAINER)) - { - magicContainerLocations.Remove(location); - } + magicContainerLocations.Remove(location); } } + minorItemLocations = AllLocations().Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); + while (magicContainerCount < magicContainersInItemPool) { - List minorItemLocations = itemLocs.Where(i => i.Collectables.Any(j => j.IsMinorItem())).ToList(); - Location location = minorItemLocations.Sample(r)!; - int index = r.Next(location.Collectables.Count); - if (location.Collectables[index].IsMinorItem()) + Collectable[] locationMinorItems = location.GetAllCollectables().Where(i => i.IsMinorItem()).ToArray(); + if (locationMinorItems.Length == 0) { - location.Collectables[index] = Collectable.MAGIC_CONTAINER; - magicContainerCount++; - if (!location.Collectables.Any(i => i.IsMinorItem())) - { - minorItemLocations.Remove(location); - } + minorItemLocations.Remove(location); + continue; + } + if (!location.ReplaceCollectable(locationMinorItems.Sample(r), Collectable.MAGIC_CONTAINER)) + { + magicContainerLocations.Remove(location); } + magicContainerCount++; } if (props.AllowImportantItemDuplicates) @@ -1181,8 +1200,8 @@ private void ShuffleItems() importantItemsToDuplicate = importantItemsToDuplicate.Where(shufflableItems.Contains).ToList(); - List minorItemLocations = duplicateItemPlacementCandidates.Where(i => i.Collectables.Any(c => c.IsMinorItem())).ToList(); - int replaceableMinorItemCount = duplicateItemPlacementCandidates.Sum(l => l.Collectables.Count(c => c.IsMinorItem())); + minorItemLocations = duplicateItemPlacementCandidates.Where(i => i.GetAllCollectables().Any(c => c.IsMinorItem())).ToList(); + int replaceableMinorItemCount = duplicateItemPlacementCandidates.Sum(l => l.GetAllCollectables().Count(c => c.IsMinorItem())); importantItemsToDuplicate = importantItemsToDuplicate.GetRange(0, int.Min(replaceableMinorItemCount, importantItemsToDuplicate.Count)); importantItemsToDuplicate.FisherYatesShuffle(r); @@ -1191,13 +1210,16 @@ private void ShuffleItems() for (int itemIndex = 0; itemIndex < importantItemsToDuplicate.Count; itemIndex++) { Location minorItemLocation = minorItemLocations.Sample(r)!; - minorItemLocation.Collectables.FisherYatesShuffle(r); - for(int collectableIndex = 0; collectableIndex < minorItemLocation.Collectables.Count; collectableIndex++) + List locationCollectables = minorItemLocation.GetAllCollectables(); + List collectableIndexes = Enumerable.Range(0, minorItemLocation.GetAllCollectables().Count()).ToList(); + collectableIndexes.FisherYatesShuffle(r); + foreach(int collectableIndex in collectableIndexes) { - if(minorItemLocation.Collectables[collectableIndex].IsMinorItem()) + if(locationCollectables[collectableIndex].IsMinorItem()) { - minorItemLocation.Collectables[collectableIndex] = importantItemsToDuplicate[itemIndex]; - if(!minorItemLocation.Collectables.Any(c => c.IsMinorItem())) + locationCollectables[collectableIndex] = importantItemsToDuplicate[itemIndex]; + minorItemLocation.SetCollectables(locationCollectables); + if(!locationCollectables.Any(c => c.IsMinorItem())) { minorItemLocations.Remove(minorItemLocation); } @@ -1255,48 +1277,24 @@ bool IsChainLocation(Location l) => private void DoShuffle(List itemsToShuffle, List itemShuffleLocations) { - int itemShuffleLocationsCount = itemShuffleLocations.Count; - if(itemShuffleLocations.Any(i => i.PalaceNumber != null)) - { - itemShuffleLocationsCount = itemShuffleLocationsCount - - itemShuffleLocations.Count(i => i.PalaceNumber != null) - + props.PalaceItemRoomCounts.Sum(i => i); - //- props.PalaceItemRoomCounts.Where(i => i == 0).Count(); - } - if (itemsToShuffle.Count != itemShuffleLocationsCount) + int possibleItemPlacements = itemShuffleLocations.Sum(i => i.GetAllCollectables().Count); + + if (itemsToShuffle.Count != possibleItemPlacements) { throw new Exception("Item locations must match number of items"); } itemsToShuffle.FisherYatesShuffle(r); - //Clear all locations, then set them to the newly shuffled items - itemShuffleLocations.ForEach(i => i.Collectables.Clear()); - using var itemLocsIterator = itemShuffleLocations.GetEnumerator(); - Location? location = null; - int subIndex = 0; - foreach (Collectable item in itemsToShuffle) + int itemIndex = 0; + foreach(Location location in itemShuffleLocations) { - if (location?.PalaceNumber == null) - { - if (!itemLocsIterator.MoveNext()) { throw new InvalidOperationException("Ran out of item locations."); } - location = itemLocsIterator.Current; - } - while (location.PalaceNumber is int palaceNum && ++subIndex > props.PalaceItemRoomCounts[palaceNum - 1]) - { - subIndex = 0; - if (!itemLocsIterator.MoveNext()) { throw new InvalidOperationException("Ran out of item locations."); } - location = itemLocsIterator.Current; - } - - location.Collectables.Add(item); - if (location.PalaceNumber is int palaceNumInIf) - { - palaces[palaceNumInIf - 1].ItemRooms[subIndex - 1].Collectable = item; - } + int itemCount = location.GetAllCollectables().Count; + List collectables = itemsToShuffle.GetRange(itemIndex, itemCount); + itemIndex += itemCount; + location.SetCollectables(collectables); } - Debug.Assert(!itemLocsIterator.MoveNext(), "All item locations were not used. This should not happen."); } private async Task FillPalaceRooms(AsmModule sideviewModule) @@ -1515,7 +1513,7 @@ private bool IsEverythingReachable(Dictionary itemGet) if (ItemGet[item] == false && item.IsItemGetItem()) { itemGetReachableFailures++; -#if UNSAFE_DEBUG +//#if UNSAFE_DEBUG if (reachableLocationsCount >= DEBUG_THRESHOLD) { Debug.WriteLine("Failed on collectables"); @@ -1524,7 +1522,7 @@ private bool IsEverythingReachable(Dictionary itemGet) //Debug.WriteLine(westHyrule.GetMapDebug()); return false; } -#endif +//#endif return false; } } @@ -1588,50 +1586,38 @@ private bool CanGet(Location location) { return location.Reachable; } - - /// - /// Removes the intermediate room between entering the spell house and reaching the old man's basement - /// private void ShortenWizards() { - /* - Spell swap notes: - Shield exit: 0xC7BB, 0xC1; enter: 0xC7EC, 0x90 //change map 48 pointer to map 40 pointer - Jump exit: 0xC7BF, 0xC5; enter: 0xC7F0, 0x94 //change map 49 pointer to map 41 pointer - Life exit: 0xC7C3, 0xC9; enter 0xC7F4, 0x98 //change map 50 pointer to map 42 pointer - Fairy exit: 0xC7C7, 0xCD; enter 0xC7F8, 0x9C //change map 51 pointer to map 43 pointer - Fire exit: 0xC7Cb, 0xD1; enter 0xC7FC, 0xA0 //change map 52 pointer to map 44 pointer - Reflect exit: 0xC7Cf, 0xD5; enter 0xC800, 0xA4 //change map 53 pointer to map 45 pointer - Spell exit: 0xC7D3, 0x6A; enter 0xC795, 0xC796, 0x4D //new kasuto item? - Thunder exit: 0xC7D7, 0xDD; enter 0xC808, 0xAC - Downstab exit: 0xC7DB, 0xE1; enter 0xC80C, 0xB0 - Upstab exit: 0xC7DF, 0xE5; enter 0xC810, 0xB4 - */ - for (int i = 0; i < 16; i = i + 2) - { - ROMData.Put(0xC611 + i, (byte)0x75); - ROMData.Put(0xC611 + i + 1, (byte)0x70); - ROMData.Put(0xC593 + i, (byte)0x48); - ROMData.Put(0xC593 + i + 1, (byte)0x9B); - } - ROMData.Put(0xC7BB, (byte)0x07); - ROMData.Put(0xC7BF, (byte)0x13); - ROMData.Put(0xC7C3, (byte)0x21); - ROMData.Put(0xC7C7, (byte)0x27); - ROMData.Put(0xC7CB, (byte)0x37); - ROMData.Put(0xC7CF, (byte)0x3F); - ROMData.Put(0xC850, (byte)0xB0); - //ROMData.put(0xC7D3, (byte)0x4D); - ROMData.Put(0xC7D7, (byte)0x5E); - ROMData.Put(0xC7DF, (byte)0x43); - ROMData.Put(0xC870, (byte)0xB8); - ROMData.Put(0xC7E3, (byte)0x49); - ROMData.Put(0xC874, (byte)0xA8); - ROMData.Put(0xC7D3, (byte)0x4D); - ROMData.Put(0xC7DB, (byte)0x29); - //ROMData.put(0xC7E3, (byte)0x49); - // ROMData.put(0xC874, (byte)0xA8); - //ROMData.put(0x8560, (byte)0xBC); + //Change the exit leading to the intermedaite wizard room to go directly to the basement + ROMData.Put(0xC827 + 1 * 4 + 2, 36 << 2); + ROMData.Put(0xC827 + 4 * 4 + 3, 37 << 2); + ROMData.Put(0xC827 + 8 * 4 + 1, 38 << 2); + ROMData.Put(0xC827 + 9 * 4 + 3, 39 << 2); + ROMData.Put(0xC827 + 13 * 4 + 3, 40 << 2); + ROMData.Put(0xC827 + 15 * 4 + 3, 41 << 2); + ROMData.Put(0xC827 + 19 * 4 + 1, 42 << 2); + ROMData.Put(0xC827 + 23 * 4 + 2, 43 << 2); + + //Change the exit from the wizard room to point back out to the town + ROMData.Put(0xC827 + 36 * 4, (1 << 2) + 2); + ROMData.Put(0xC827 + 37 * 4, (4 << 2) + 3); + ROMData.Put(0xC827 + 38 * 4, (8 << 2) + 1); + ROMData.Put(0xC827 + 39 * 4, (9 << 2) + 3); + ROMData.Put(0xC827 + 40 * 4, (13 << 2) + 3); + ROMData.Put(0xC827 + 41 * 4, (15 << 2) + 3); + ROMData.Put(0xC827 + 42 * 4, (19 << 2) + 1); + ROMData.Put(0xC827 + 43 * 4, (23 << 2) + 2); + + //downstab guy + ROMData.Put(0xC827 + 10 * 4 + 1, 44 << 2); + ROMData.Put(0xC827 + 44 * 4, (10 << 2) + 1); + + //granny's basement + ROMData.Put(0xC827 + 18 * 4 + 1, 46 << 2); + ROMData.Put(0xC827 + 46 * 4, (18 << 2) + 1); + + //upstab guy entrance is unchanged because you drop into it, but still change the exit to lead directly out + ROMData.Put(0xC827 + 45 * 4, (16 << 2) + 3); } /// @@ -1643,9 +1629,9 @@ private int UpdateItemGets(RandomizerProperties props) List requireables; accessibleMagicContainers = props.StartMagicContainers; accessibleHeartContainers = props.StartHearts; - Location newKasuto = eastHyrule.AllLocations.First(i => i.ActualTown == Town.NEW_KASUTO); + Location newKasuto = eastHyrule.newKasuto; List gottenItems = [], lastIterationGottenItems = []; - List locations = AllLocationsForReal().ToList(); + List locations = AllLocations().ToList(); List heartContainerGetLocations = [], magicContainerGetLocations = []; bool stalled = false; //No more delayed evaluation because too many locations could give you containers or require containers. @@ -1656,41 +1642,20 @@ private int UpdateItemGets(RandomizerProperties props) accessibleMagicContainers = props.StartMagicContainers + magicContainerGetLocations.Count; requireables = GetRequireables(props); gottenItems = []; - foreach (Location location in AllLocationsForReal()) + foreach (Location location in AllLocations()) { - foreach (Collectable collectable in location.Collectables) - { - if (collectable.IsInternalUse()) - { - continue; - } - bool canGet; - //Location is a palace - if (location.PalaceNumber != null && location.PalaceNumber < 7) - { - Debug.Assert(location.CollectableRequirements == World.DEFAULT_PALACE_REQUIREMENTS); - Palace palace = palaces[(int)location.PalaceNumber - 1]; - canGet = CanGet(location) - //All palaces inherently require fairy spell or magic key - && location.CollectableRequirements.AreSatisfiedBy(requireables) - && palace.GetGettableItems(requireables).Contains(collectable); - } - else - { - canGet = CanGet(location) && location.CollectableRequirements.AreSatisfiedBy(requireables); - } + List gettableItems = location.GetGettableItems(requireables); - ItemGet[collectable] = canGet; - if (canGet) - { - gottenItems.Add(collectable); - } + foreach(Collectable item in gettableItems) + { + ItemGet[item] = true; + gottenItems.Add(item); - if (canGet && collectable == Collectable.HEART_CONTAINER && !heartContainerGetLocations.Contains(location)) + if (item == Collectable.HEART_CONTAINER && !heartContainerGetLocations.Contains(location)) { heartContainerGetLocations.Add(location); } - if (canGet && collectable == Collectable.MAGIC_CONTAINER && !magicContainerGetLocations.Contains(location)) + if (item == Collectable.MAGIC_CONTAINER && !magicContainerGetLocations.Contains(location)) { magicContainerGetLocations.Add(location); } @@ -1785,7 +1750,8 @@ private async Task ProcessOverworld(Func progress, CancellationTok worlds.Add(deathMountain); worlds.Add(eastHyrule); worlds.Add(mazeIsland); - ResetTowns(); + + AssignPalaceLocations(); if (props.ContinentConnections == ContinentConnectionType.NORMAL) { @@ -1798,7 +1764,7 @@ private async Task ProcessOverworld(Func progress, CancellationTok } else if (props.ContinentConnections == ContinentConnectionType.TRANSPORTATION_SHUFFLE) { - List chosen = new List(); + List chosen = []; int type = r.Next(4); if (props.WestBiome.UsesVanillaMap() || props.DmBiome.UsesVanillaMap()) @@ -2075,7 +2041,6 @@ private async Task ProcessOverworld(Func progress, CancellationTok timeSpentBuildingMI += (int)DateTime.Now.Subtract(timestamp).TotalMilliseconds; worlds.Where(i => i.biome == Biome.VANILLA_SHUFFLE).ToList().ForEach(i => i.DisableDisallowedPassthroughs()); - worlds.ForEach(i => i.SynchronizeLinkedLocations()); if (ct.IsCancellationRequested) { return; } UpdateProgress(progress, 6); @@ -2103,11 +2068,8 @@ private async Task ProcessOverworld(Func progress, CancellationTok location.Reachable = false; } - eastHyrule.spellTower.Reachable = false; - westHyrule.ResetVisitabilityState(); - eastHyrule.ResetVisitabilityState(); - mazeIsland.ResetVisitabilityState(); - deathMountain.ResetVisitabilityState(); + worlds.ForEach(i => i.ResetVisitabilityState()); + worlds.ForEach(i => i.ResetCollectables(props)); //There was a spooky extra call to LoadItemLocs that used to be here that shouldn't be needed, but be aware. westHyrule.SetStart(); @@ -2230,132 +2192,86 @@ private bool AllContinentsAreConnected((Location, Location)[] connections) return continentsFound.Count == 4; } - private void ResetTowns() - { - westHyrule.locationAtRauru.ActualTown = Town.RAURU; - westHyrule.locationAtRuto.ActualTown = Town.RUTO; - westHyrule.locationAtSariaNorth.ActualTown = Town.SARIA_NORTH; - westHyrule.locationAtSariaSouth.ActualTown = Town.SARIA_SOUTH; - westHyrule.locationAtMido.ActualTown = Town.MIDO_WEST; - eastHyrule.townAtNabooru.ActualTown = Town.NABOORU; - eastHyrule.townAtDarunia.ActualTown = Town.DARUNIA_WEST; - eastHyrule.townAtNewKasuto.ActualTown = Town.NEW_KASUTO; - eastHyrule.townAtOldKasuto.ActualTown = Town.OLD_KASUTO; - } - - private void ShufflePalaces() - { - if (!props.PalacesCanSwapContinent) return; - List pals = [westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, westHyrule.locationAtPalace3, mazeIsland.locationAtPalace4, eastHyrule.locationAtPalace5, eastHyrule.locationAtPalace6]; - - if (props.P7shuffle) - { - pals.Add(eastHyrule.locationAtGP); - } - - for (int i = pals.Count - 1; i > 0; i--) - { - int swap = r.Next(i + 1); - Util.Swap(pals[i], pals[swap]); - } - - } - //ItemLocs is specifically only those locations that contain shufflable items //If you're looking for a global reference for which items are where... too bad it doesn't exist :( private List LoadItemLocs(int[] itemsPerPalaces) { List GetPalacesWithItems(IEnumerable palaces) => - palaces.Where(p => p.PalaceNumber is int n && n != 7 && props.PalaceItemRoomCounts[n - 1] != 0).ToList(); + palaces.Where(p => p.Palace?.Number is int n && n != 7 && props.PalaceItemRoomCounts[n - 1] != 0).ToList(); // Listed somewhat in order of expected progression. This way we can // use the indexes of two locations to determine approx distance. itemLocs = new List(30); + //palaces itemLocs = GetPalacesWithItems([ westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, - westHyrule.locationAtPalace3 + westHyrule.locationAtPalace3, + mazeIsland.locationAtPalace4, + eastHyrule.locationAtPalace5, + eastHyrule.locationAtPalace6, + eastHyrule.locationAtGP ]); + + //always live locations itemLocs.AddRange([ westHyrule.grassTile, westHyrule.heartContainerCave, westHyrule.magicContainerCave, westHyrule.medicineCave, westHyrule.trophyCave, - ]); - if (props.PbagItemShuffle) - { - itemLocs.Add(westHyrule.pbagCave); - } - if (props.IncludeQuestItemsInShuffle) - { - itemLocs.Add(westHyrule.bagu); - } - if (props.IncludeSpellsInShuffle) - { - itemLocs.Add(westHyrule.locationAtRauru); - itemLocs.Add(westHyrule.locationAtRuto); - itemLocs.Add(westHyrule.locationAtSariaNorth); - } - if (props.IncludeQuestItemsInShuffle) - { - itemLocs.Add(westHyrule.mirrorTable); - } - if (props.IncludeSpellsInShuffle) - { - itemLocs.Add(westHyrule.locationAtMido); - } - if (props.IncludeSwordTechsInShuffle) - { - itemLocs.Add(westHyrule.midoChurch); - } - - itemLocs.AddRange([ deathMountain.specRock, deathMountain.hammerCave, - ]); - - itemLocs.AddRange(GetPalacesWithItems([ - eastHyrule.locationAtPalace5, - eastHyrule.locationAtPalace6, - eastHyrule.locationAtGP - ])); - itemLocs.AddRange([ eastHyrule.waterTile, eastHyrule.desertTile, + mazeIsland.childDrop, + mazeIsland.magicContainerDrop ]); + + //pbag shuffle if (props.PbagItemShuffle) { + itemLocs.Add(westHyrule.pbagCave); itemLocs.Add(eastHyrule.pbagCave1); itemLocs.Add(eastHyrule.pbagCave2); } + + + //Quest item shuffle if (props.IncludeQuestItemsInShuffle) { - itemLocs.Add(eastHyrule.fountain); - } - if (props.IncludeSpellsInShuffle) - { - itemLocs.Add(eastHyrule.townAtNabooru); - itemLocs.Add(eastHyrule.townAtDarunia); - } - if (props.IncludeSwordTechsInShuffle) - { - itemLocs.Add(eastHyrule.daruniaRoof); - } - if (props.IncludeSpellsInShuffle) - { - itemLocs.Add(eastHyrule.townAtOldKasuto); - itemLocs.Add(eastHyrule.townAtNewKasuto); + itemLocs.Add(westHyrule.bagu); } - itemLocs.Add(eastHyrule.newKasutoBasement); - itemLocs.Add(eastHyrule.spellTower); + westHyrule.bagu.Town!.GetTownMap(VanillaTownMap.BAGU_INDOORS)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + westHyrule.sariaNorth.Town!.GetTownMap(VanillaTownMap.SARIA_TABLE)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + eastHyrule.nabooru.Town!.GetTownMap(VanillaTownMap.NABOORU_MID)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; - itemLocs.AddRange(GetPalacesWithItems([mazeIsland.locationAtPalace4])); - itemLocs.AddRange([ - mazeIsland.childDrop, - mazeIsland.magicContainerDrop - ]); + + itemLocs.Add(westHyrule.rauru); + itemLocs.Add(westHyrule.ruto); + itemLocs.Add(westHyrule.sariaNorth); + itemLocs.Add(westHyrule.mido); + itemLocs.Add(eastHyrule.nabooru); + itemLocs.Add(eastHyrule.darunia); + itemLocs.Add(eastHyrule.oldKasuto); + itemLocs.Add(eastHyrule.newKasuto); + + westHyrule.rauru.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + westHyrule.ruto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + westHyrule.sariaNorth.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + westHyrule.mido.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + eastHyrule.nabooru.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + eastHyrule.darunia.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + eastHyrule.newKasuto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + eastHyrule.oldKasuto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + + //stab shuffle + westHyrule.mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER)!.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + eastHyrule.darunia.Town!.GetTownMap(VanillaTownMap.DARUNIA_TRAINER)!.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + + //Remove locations that don't have any shufflable items + itemLocs.RemoveAll(i => i.GetShufflableCollectables().Count == 0); return itemLocs; } @@ -2375,8 +2291,8 @@ private void ShuffleSpells() } } - List unallocatedTowns = new List { Town.RAURU, Town.RUTO, Town.SARIA_NORTH, Town.MIDO_WEST, - Town.NABOORU, Town.DARUNIA_WEST, Town.NEW_KASUTO, Town.OLD_KASUTO }; + List unallocatedTowns = new List { westHyrule.rauru, westHyrule.ruto, westHyrule.sariaNorth, westHyrule.mido, + eastHyrule.nabooru, eastHyrule.darunia, eastHyrule.newKasuto, eastHyrule.oldKasuto }; var filteredToJustSpells = Enum.GetValues(typeof(Collectable)).Cast().Where(i => i.IsSpell()); foreach (Collectable spell in filteredToJustSpells) @@ -2388,17 +2304,16 @@ private void ShuffleSpells() { continue; } - Town town = unallocatedTowns[r.Next(unallocatedTowns.Count)]; - Location townLocation = GetTownLocation(town); - townLocation.Collectables = [spell]; - unallocatedTowns.Remove(town); + Location townLocation = unallocatedTowns.Sample(r)!; + townLocation.Town!.GetWizard()!.Collectable = spell; + unallocatedTowns.Remove(townLocation); } } private void SwapUpAndDownstab() { - eastHyrule.daruniaRoof.Collectables = [Collectable.DOWNSTAB]; - westHyrule.midoChurch.Collectables = [Collectable.UPSTAB]; + eastHyrule.darunia.Town!.GetTownMap(VanillaTownMap.DARUNIA_TRAINER)!.Collectable = Collectable.DOWNSTAB; + westHyrule.mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER)!.Collectable = Collectable.UPSTAB; } //Updated to use fisher-yates. Eventually i'll catch all of these. N is small enough here it REALLY makes a difference @@ -2658,13 +2573,13 @@ private void UpdateRom() palace.WriteConnections(ROMData); } - ROMData.Put(0x1CD3A, (byte)palGraphics[(int)westHyrule.locationAtPalace1.PalaceNumber!]); - ROMData.Put(0x1CD3B, (byte)palGraphics[(int)westHyrule.locationAtPalace2.PalaceNumber!]); - ROMData.Put(0x1CD3C, (byte)palGraphics[(int)westHyrule.locationAtPalace3.PalaceNumber!]); - ROMData.Put(0x1CD46, (byte)palGraphics[(int)mazeIsland.locationAtPalace4.PalaceNumber!]); - ROMData.Put(0x1CD42, (byte)palGraphics[(int)eastHyrule.locationAtPalace5.PalaceNumber!]); - ROMData.Put(0x1CD43, (byte)palGraphics[(int)eastHyrule.locationAtPalace6.PalaceNumber!]); - ROMData.Put(0x1CD44, (byte)palGraphics[(int)eastHyrule.locationAtGP.PalaceNumber!]); + ROMData.Put(0x1CD3A, (byte)palGraphics[(int)westHyrule.locationAtPalace1.Palace!.Number!]); + ROMData.Put(0x1CD3B, (byte)palGraphics[(int)westHyrule.locationAtPalace2.Palace!.Number!]); + ROMData.Put(0x1CD3C, (byte)palGraphics[(int)westHyrule.locationAtPalace3.Palace!.Number!]); + ROMData.Put(0x1CD46, (byte)palGraphics[(int)mazeIsland.locationAtPalace4.Palace!.Number!]); + ROMData.Put(0x1CD42, (byte)palGraphics[(int)eastHyrule.locationAtPalace5.Palace!.Number!]); + ROMData.Put(0x1CD43, (byte)palGraphics[(int)eastHyrule.locationAtPalace6.Palace!.Number!]); + ROMData.Put(0x1CD44, (byte)palGraphics[(int)eastHyrule.locationAtGP.Palace!.Number!]); if (props.ShuffleEnemyPalettes) { @@ -2674,98 +2589,37 @@ private void UpdateRom() RerollPaletteTable(RomMap.TOWN_PALETTE_TABLE, customizationRng); RerollPaletteTable(RomMap.PALACE_PALETTE_TABLE_MAJOR, customizationRng); RerollPaletteTable(RomMap.GP_PALETTE_TABLE_MAJOR, customizationRng); - - /* - lets no longer shuffle orange/red/blue bits for enemies, it changes - what is visible in the dark and also doesn't add enough to be worth it imo. - I also prefer if we can have the palette shuffle as a customize (non-flag) option. - - for (int i = RomMap.WEST_ENEMY_STATS_TABLE + 0x3; i < RomMap.WEST_ENEMY_STATS_TABLE + 0x23; i++) - { - if (i != RomMap.WEST_ENEMY_STATS_TABLE + 0x13) // skip elevator - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - for (int i = 0x94e8; i < 0x9508; i++) - { - if (i != 0x94f8) - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - for (int i = 0x114e8; i < 0x11508; i++) - { - if (i != 0x114f8) - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - for (int i = 0x129e8; i < 0x12a09; i++) - { - if (i != 0x129f8) - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - for (int i = 0x154e8; i < 0x15508; i++) - { - if (i != 0x154f8) - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - */ } //WRITE UPDATES TO WIZARD/QUEST COLLECTABLES HERE - ROMData.Put(RomMap.WEST_TROPHY_CAVE_COLLECTABLE, (byte)westHyrule.trophyCave.Collectables[0]); - ROMData.Put(RomMap.WEST_MAGIC_CONTAINER_CAVE_COLLECTABLE, (byte)westHyrule.magicContainerCave.Collectables[0]); - ROMData.Put(RomMap.WEST_HEART_CONTAINER_CAVE_COLLECTABLE, (byte)westHyrule.heartContainerCave.Collectables[0]); - ROMData.Put(RomMap.WEST_MEDICINE_CAVE_COLLECTABLE, (byte)westHyrule.medicineCave.Collectables[0]); - ROMData.Put(RomMap.WEST_GRASS_TILE_COLLECTABLE, (byte)westHyrule.grassTile.Collectables[0]); - ROMData.Put(RomMap.DM_SPECTACLE_ROCK_COLLECTABLE, (byte)deathMountain.specRock.Collectables[0]); - ROMData.Put(RomMap.DM_HAMMER_CAVE_COLLECTABLE, (byte)deathMountain.hammerCave.Collectables[0]); - ROMData.Put(RomMap.EAST_WATER_TILE_COLLECTABLE, (byte)eastHyrule.waterTile.Collectables[0]); - ROMData.Put(RomMap.EAST_DESERT_TILE_COLLECTABLE, (byte)eastHyrule.desertTile.Collectables[0]); + ROMData.Put(RomMap.WEST_TROPHY_CAVE_COLLECTABLE, (byte)westHyrule.trophyCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.WEST_MAGIC_CONTAINER_CAVE_COLLECTABLE, (byte)westHyrule.magicContainerCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.WEST_HEART_CONTAINER_CAVE_COLLECTABLE, (byte)westHyrule.heartContainerCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.WEST_MEDICINE_CAVE_COLLECTABLE, (byte)westHyrule.medicineCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.WEST_GRASS_TILE_COLLECTABLE, (byte)westHyrule.grassTile.GetAllCollectables()[0]); + ROMData.Put(RomMap.DM_SPECTACLE_ROCK_COLLECTABLE, (byte)deathMountain.specRock.GetAllCollectables()[0]); + ROMData.Put(RomMap.DM_HAMMER_CAVE_COLLECTABLE, (byte)deathMountain.hammerCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.EAST_WATER_TILE_COLLECTABLE, (byte)eastHyrule.waterTile.GetAllCollectables()[0]); + ROMData.Put(RomMap.EAST_DESERT_TILE_COLLECTABLE, (byte)eastHyrule.desertTile.GetAllCollectables()[0]); ROMData.ElevatorBossFix(props.BossItem); // Update item rooms and entrances for all palaces - Location[] locations = [ + Location[] palaceLocations = [ westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, westHyrule.locationAtPalace3, eastHyrule.locationAtPalace5, eastHyrule.locationAtPalace6, eastHyrule.locationAtGP, mazeIsland.locationAtPalace4, ]; - foreach (var loc in locations) + foreach (var loc in palaceLocations) { - var idx = (int)loc.PalaceNumber! - 1; - if (loc.PalaceNumber != 7) + var idx = (int)loc.Palace!.Number! - 1; + if (loc.Palace!.Number != 7) { - for(int i = 0; i < palaces[idx].ItemRooms.Count; i++) + foreach(Room itemRoom in loc.Palace.ItemRooms) { - ROMData.UpdateItem(loc.Collectables[i], palaces[idx].ItemRooms[i]); + Debug.Assert(itemRoom.Collectable != null, $"Item room {itemRoom.Name} map {itemRoom.Map} in palace {loc.Palace.Number} is missing its item"); + ROMData.UpdateItem((Collectable)itemRoom.Collectable, itemRoom); } } @@ -2777,30 +2631,33 @@ what is visible in the dark and also doesn't add enough to be worth it imo. ROMData.Put(loc.MemAddress + 0x7e, root.Map); } - ROMData.Put(RomMap.EAST_SPELL_TOWER_COLLECTABLE, (byte)eastHyrule.spellTower.Collectables[0]); //map 47 - ROMData.Put(RomMap.EAST_NEW_KASUTO_BASEMENT_COLLECTABLE, (byte)eastHyrule.newKasutoBasement.Collectables[0]); //map 46 + ROMData.Put(RomMap.EAST_SPELL_TOWER_COLLECTABLE, (byte)eastHyrule.newKasuto.Town!.GetTownMap(VanillaTownMap.SPELL_TOWER_INTERIOR)!.Collectable!); + ROMData.Put(RomMap.EAST_NEW_KASUTO_BASEMENT_COLLECTABLE, (byte)eastHyrule.newKasuto.Town!.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!.Collectable!); - ROMData.Put(RomMap.MI_MAGIC_CONTAINER_DROP_COLLECTABLE, (byte)mazeIsland.magicContainerDrop.Collectables[0]); - ROMData.Put(RomMap.MI_CHILD_DROP_COLLECTABLE, (byte)mazeIsland.childDrop.Collectables[0]); + ROMData.Put(RomMap.MI_MAGIC_CONTAINER_DROP_COLLECTABLE, (byte)mazeIsland.magicContainerDrop.GetAllCollectables()[0]); + ROMData.Put(RomMap.MI_CHILD_DROP_COLLECTABLE, (byte)mazeIsland.childDrop.GetAllCollectables()[0]); - ROMData.Put(RomMap.WEST_PBAG_CAVE_COLLECTABLE, (byte)westHyrule.pbagCave.Collectables[0]); - ROMData.Put(RomMap.EAST_PBAG_CAVE1_COLLECTABLE, (byte)eastHyrule.pbagCave1.Collectables[0]); - ROMData.Put(RomMap.EAST_PBAG_CAVE2_COLLECTABLE, (byte)eastHyrule.pbagCave2.Collectables[0]); + if (props.PbagItemShuffle) + { + ROMData.Put(RomMap.WEST_PBAG_CAVE_COLLECTABLE, (byte)westHyrule.pbagCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.EAST_PBAG_CAVE1_COLLECTABLE, (byte)eastHyrule.pbagCave1.GetAllCollectables()[0]); + ROMData.Put(RomMap.EAST_PBAG_CAVE2_COLLECTABLE, (byte)eastHyrule.pbagCave2.GetAllCollectables()[0]); + } foreach (Location location in pbagHearts) { if (location == westHyrule.pbagCave) { - ROMData.Put(RomMap.WEST_PBAG_CAVE_COLLECTABLE, (byte)westHyrule.pbagCave.Collectables[0]); + ROMData.Put(RomMap.WEST_PBAG_CAVE_COLLECTABLE, (byte)westHyrule.pbagCave.GetAllCollectables()[0]); } if (location == eastHyrule.pbagCave1) { - ROMData.Put(RomMap.EAST_PBAG_CAVE1_COLLECTABLE, (byte)eastHyrule.pbagCave1.Collectables[0]); + ROMData.Put(RomMap.EAST_PBAG_CAVE1_COLLECTABLE, (byte)eastHyrule.pbagCave1.GetAllCollectables()[0]); } if (location == eastHyrule.pbagCave2) { - ROMData.Put(RomMap.EAST_PBAG_CAVE2_COLLECTABLE, (byte)eastHyrule.pbagCave2.Collectables[0]); + ROMData.Put(RomMap.EAST_PBAG_CAVE2_COLLECTABLE, (byte)eastHyrule.pbagCave2.GetAllCollectables()[0]); } } @@ -2845,12 +2702,12 @@ what is visible in the dark and also doesn't add enough to be worth it imo. } //Update world check for p7 - if (westHyrule.locationAtPalace1.PalaceNumber == 7 || westHyrule.locationAtPalace2.PalaceNumber == 7 || westHyrule.locationAtPalace3.PalaceNumber == 7) + if (westHyrule.locationAtPalace1.Palace!.Number == 7 || westHyrule.locationAtPalace2.Palace!.Number == 7 || westHyrule.locationAtPalace3.Palace!.Number == 7) { ROMData.Put(0x1dd3b, 0x05); } - if (mazeIsland.locationAtPalace4.PalaceNumber == 7) + if (mazeIsland.locationAtPalace4.Palace!.Number == 7) { ROMData.Put(0x1dd3b, 0x14); } @@ -2886,9 +2743,9 @@ what is visible in the dark and also doesn't add enough to be worth it imo. else { // Use the old spell menu behavior where the spells are placed in the menu in the location it came from - var wizardCollectables = AllLocationsForReal() - .Where(l => l.ActualTown != null && Towns.STRICT_SPELL_LOCATIONS.Contains((Town)l.ActualTown)) - .Select(l => l.Collectables[0]).ToList(); + var wizardCollectables = AllLocations() + .Where(l => l.Town?.GetWizard() != null) + .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); for (int i = 0; i < magFunction.Length; i++) { @@ -3106,6 +2963,9 @@ public void RandomizeSmallItems(int world, bool first) public void PrintDebugSpoiler(LogLevel logLevel) { + //I didn't want to port this and we mostly use spoiler log for this now anyway so maybe this just dies. + throw new NotImplementedException(); + /* logger.Log(logLevel, "ITEMS:"); List magicContainerLocations = []; List heartContainerLocations = []; @@ -3154,6 +3014,7 @@ public void PrintDebugSpoiler(LogLevel logLevel) sb.AppendLine(locationName); } logger.Log(logLevel, sb.ToString()); + */ } public string GenerateSpoiler() @@ -3261,7 +3122,8 @@ .define blueTile $fe """, "crop_guides.s"); } - public IEnumerable AllLocationsForReal() + //No longer AllLocationsForReal since all fakeness has been removed. + public IEnumerable AllLocations() { List? locations = westHyrule?.AllLocations .Union(eastHyrule?.AllLocations ?? []) @@ -3270,14 +3132,6 @@ public IEnumerable AllLocationsForReal() return locations ?? []; } - public IEnumerable GetNonSideviewItemLocations() - { - return AllLocationsForReal().Where( - i => i.VanillaCollectable.IsSpell() - || i.VanillaCollectable.IsQuestItem() - || i.VanillaCollectable.IsSwordTech()); - } - public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) { StringBuilder sb = new StringBuilder(); @@ -3290,15 +3144,11 @@ public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) sb.AppendLine(""); //Where(i => !i.ItemGet && !i.Collectable.IsInternalUse() - foreach (Location location in AllLocationsForReal()) + foreach (Location location in AllLocations()) { - foreach (Collectable collectable in location.Collectables) + foreach (Collectable collectable in location.GetAllCollectables().Where(i => !i.IsMinorItem() && !ItemGet[i])) { - if (!collectable.IsMinorItem() && !ItemGet[collectable]) - { - sb.AppendLine(location.Name + " / " + Enum.GetName(typeof(Collectable), collectable)); - } - //TODO: figure out how to display individual heart/magic containers + sb.AppendLine($"{location.Name} / [{String.Join(", ", location.GetAllCollectables().Select(i => Enum.GetName(i)!))}]"); } } @@ -3328,17 +3178,19 @@ private bool IsRaftAlwaysRequired(RandomizerProperties props) return false; } - private void FullItemShuffle(Assembler asm, IEnumerable nonSideviewLocations) + private void FullItemShuffle(Assembler asm) { var a = asm.Module(); foreach (var collect in Enum.GetValues()) { a.Set($"{collect.ToString().ToUpper()}_ITEMLOC", (int)collect); } + /* foreach (var loc in nonSideviewLocations) { - a.Set($"{loc.VanillaCollectable.ToString().ToUpper()}_ITEMLOC", (int)loc.Collectables[0]); + //a.Set($"{loc.VanillaCollectable.ToString().ToUpper()}_ITEMLOC", (int)loc.Collectables[0]); } + */ foreach (var val in Enum.GetValues()) { a.Set($"{val.ToString()}_DIALOG_WEST_INDEX", (int)val); @@ -3522,13 +3374,13 @@ beq @EndOfData public void SetPalacePalettes(Assembler asm) { var a = asm.Module(); - a.Assign("P1Palette", (byte)palPalettes[westHyrule?.locationAtPalace1.PalaceNumber ?? 0]); - a.Assign("P2Palette", (byte)palPalettes[westHyrule?.locationAtPalace2.PalaceNumber ?? 1]); - a.Assign("P3Palette", (byte)palPalettes[westHyrule?.locationAtPalace3.PalaceNumber ?? 2]); - a.Assign("P4Palette", (byte)palPalettes[mazeIsland?.locationAtPalace4.PalaceNumber ?? 3]); - a.Assign("P5Palette", (byte)palPalettes[eastHyrule?.locationAtPalace5.PalaceNumber ?? 4]); - a.Assign("P6Palette", (byte)palPalettes[eastHyrule?.locationAtPalace6.PalaceNumber ?? 5]); - a.Assign("PGreatPalette", (byte)palPalettes[eastHyrule?.locationAtGP.PalaceNumber ?? 6]); + a.Assign("P1Palette", (byte)palPalettes[westHyrule?.locationAtPalace1.Palace!.Number ?? 0]); + a.Assign("P2Palette", (byte)palPalettes[westHyrule?.locationAtPalace2.Palace!.Number ?? 1]); + a.Assign("P3Palette", (byte)palPalettes[westHyrule?.locationAtPalace3.Palace!.Number ?? 2]); + a.Assign("P4Palette", (byte)palPalettes[mazeIsland?.locationAtPalace4.Palace!.Number ?? 3]); + a.Assign("P5Palette", (byte)palPalettes[eastHyrule?.locationAtPalace5.Palace!.Number ?? 4]); + a.Assign("P6Palette", (byte)palPalettes[eastHyrule?.locationAtPalace6.Palace!.Number ?? 5]); + a.Assign("PGreatPalette", (byte)palPalettes[eastHyrule?.locationAtGP.Palace!.Number ?? 6]); a.Code(""" .include "z2r.inc" @@ -3716,13 +3568,13 @@ private static void UpdateTexts(Assembler asm, List hints) private void AssignRealPalaceLocations(AsmModule a) { - a.Assign("RealPalaceAtLocation1", (westHyrule?.locationAtPalace1.PalaceNumber ?? 1) - 1); - a.Assign("RealPalaceAtLocation2", (westHyrule?.locationAtPalace2.PalaceNumber ?? 2) - 1); - a.Assign("RealPalaceAtLocation3", (westHyrule?.locationAtPalace3.PalaceNumber ?? 3) - 1); - a.Assign("RealPalaceAtLocation4", (mazeIsland?.locationAtPalace4.PalaceNumber ?? 4) - 1); - a.Assign("RealPalaceAtLocation5", (eastHyrule?.locationAtPalace5.PalaceNumber ?? 5) - 1); - a.Assign("RealPalaceAtLocation6", (eastHyrule?.locationAtPalace6.PalaceNumber ?? 6) - 1); - a.Assign("RealPalaceAtLocationGP", (eastHyrule?.locationAtGP.PalaceNumber ?? 7) - 1); + a.Assign("RealPalaceAtLocation1", (westHyrule?.locationAtPalace1.Palace!.Number ?? 1) - 1); + a.Assign("RealPalaceAtLocation2", (westHyrule?.locationAtPalace2.Palace!.Number ?? 2) - 1); + a.Assign("RealPalaceAtLocation3", (westHyrule?.locationAtPalace3.Palace!.Number ?? 3) - 1); + a.Assign("RealPalaceAtLocation4", (mazeIsland?.locationAtPalace4.Palace!.Number ?? 4) - 1); + a.Assign("RealPalaceAtLocation5", (eastHyrule?.locationAtPalace5.Palace!.Number ?? 5) - 1); + a.Assign("RealPalaceAtLocation6", (eastHyrule?.locationAtPalace6.Palace!.Number ?? 6) - 1); + a.Assign("RealPalaceAtLocationGP", (eastHyrule?.locationAtGP.Palace!.Number ?? 7) - 1); } public void StatTracking(RandomizerProperties props, Assembler asm) @@ -3831,7 +3683,7 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando rom.AddRandomizerToTitle(engine); AddCropGuideBoxesToFileSelect(engine); FixHelmetheadBossRoom(engine); - FullItemShuffle(engine, GetNonSideviewItemLocations()); + FullItemShuffle(engine); rom.DontCountExpDuringTalking(engine); rom.FixElevatorPositionInFallRooms(engine); rom.AllowForChangingDoorYPosition(engine); @@ -3964,41 +3816,31 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando UpdateTexts(engine, texts); } - //This entire town location shuffle structure is awful if this method needs to exist. - private Location GetTownLocation(Town town) + private void AssignPalaceLocations() { - if(westHyrule.locationAtMido.ActualTown == town) - { - return westHyrule.locationAtMido; - } - if (westHyrule.locationAtRauru.ActualTown == town) + List palaceLocations = [westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, westHyrule.locationAtPalace3, + mazeIsland.locationAtPalace4, eastHyrule.locationAtPalace5, eastHyrule.locationAtPalace6]; + if (props.PalacesCanSwapContinent) { - return westHyrule.locationAtRauru; - } - if (westHyrule.locationAtRuto.ActualTown == town) - { - return westHyrule.locationAtRuto; - } - if (westHyrule.locationAtSariaNorth.ActualTown == town) - { - return westHyrule.locationAtSariaNorth; - } - if (eastHyrule.townAtDarunia.ActualTown == town) - { - return eastHyrule.townAtDarunia; - } - if (eastHyrule.townAtNabooru.ActualTown == town) - { - return eastHyrule.townAtNabooru; + if (props.P7shuffle) + { + palaceLocations.Add(eastHyrule.locationAtGP); + palaceLocations.FisherYatesShuffle(r); + } + else + { + palaceLocations.FisherYatesShuffle(r); + palaceLocations.Add(eastHyrule.locationAtGP); + } } - if (eastHyrule.townAtNewKasuto.ActualTown == town) + else { - return eastHyrule.townAtNewKasuto; + palaceLocations.Add(eastHyrule.locationAtGP); } - if (eastHyrule.townAtOldKasuto.ActualTown == town) + for (int i = 0; i < 7; i++) { - return eastHyrule.townAtOldKasuto; + palaceLocations[i].Palace = palaces[i]; } - throw new Exception("Unrecognized town in GetTownLocation, was this extended to non-standard wizards?"); + palaceLocations.ForEach(i => i.AccessRequirements = Requirements.DEFAULT_PALACE_REQUIREMENTS); } } diff --git a/RandomizerCore/Music.cs b/RandomizerCore/Music.cs index 7dd572ba1..7c6e2d37c 100644 --- a/RandomizerCore/Music.cs +++ b/RandomizerCore/Music.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Text; using Z2Randomizer.RandomizerCore.Overworld; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -348,7 +349,7 @@ SongMap CreateAreaSongMap(UsesSongs usesSongs) usage = Usage.Town; else if (loc.TerrainType == Terrain.TOWN) { - if (loc.ActualTown == Town.OLD_KASUTO // ?? + if (loc.Town?.Type == TownType.OLD_KASUTO // ?? /*|| loc.ActualTown == Town.SARIA_SOUTH*/) continue; @@ -356,10 +357,10 @@ SongMap CreateAreaSongMap(UsesSongs usesSongs) } else if (loc.TerrainType == Terrain.PALACE) { - if (loc.PalaceNumber == null) + if (loc.Palace?.Number == null) continue; // North palace - usage = (loc.PalaceNumber < 7) + usage = (loc.Palace.Number < 7) ? Usage.Palace : Usage.GreatPalace; } @@ -383,7 +384,7 @@ SongMap CreateAreaSongMap(UsesSongs usesSongs) Func GetSongIdx = usage switch { // One song per palace - Usage.Palace => (loc => (int)loc.PalaceNumber! - 1), + Usage.Palace => (loc => (int)loc.Palace!.Number! - 1), Usage.GreatPalace => (loc => 0), // One song per continent _ => (loc => (int)(loc.VanillaContinent ?? loc.Continent)), diff --git a/RandomizerCore/Overworld/DeathMountain.cs b/RandomizerCore/Overworld/DeathMountain.cs index c2bcbdeb2..d00f67537 100644 --- a/RandomizerCore/Overworld/DeathMountain.cs +++ b/RandomizerCore/Overworld/DeathMountain.cs @@ -193,7 +193,7 @@ .. rom.LoadLocations(LocationID.DM_SPEC_ROCK, 1, terrains), climate = Climates.Create(props.DmClimate); climate.SeedTerrainCount = Math.Min(climate.SeedTerrainCount, biome.SeedTerrainLimit()); - SetVanillaCollectables(props.ReplaceFireWithDash); + //SetVanillaCollectables(props.ReplaceFireWithDash); } private void RemoveLocationsDM(ICollection locations) @@ -1114,7 +1114,7 @@ public override void UpdateVisit(List requireables) if (location.AccessRequirements.AreSatisfiedBy(requireables)) { location.Reachable = true; - if (connectionsDM.ContainsKey(location) && location.ConnectionRequirements.AreSatisfiedBy(requireables)) + if (connectionsDM.ContainsKey(location) && (location.Town == null || location.Town.CanBeTraversed(requireables))) { foreach (Location connectedLocation in connectionsDM[location]) { @@ -1215,18 +1215,20 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool return requiredLocations.Where(i => i != null); } + /* protected override void SetVanillaCollectables(bool useDash) { hammerCave.VanillaCollectable = Collectable.HAMMER; specRock.VanillaCollectable = Collectable.MAGIC_CONTAINER; } + */ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("DEATH MOUNTAIN: "); - sb.AppendLine("\tHammer Cave: " + hammerCave.Collectables[0].EnglishText()); - sb.AppendLine("\tSpec Rock: " + specRock.Collectables[0].EnglishText()); + sb.AppendLine("\tHammer Cave: " + hammerCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tSpec Rock: " + specRock.GetAllCollectables()[0].EnglishText()); sb.AppendLine(); return sb.ToString(); @@ -1234,17 +1236,25 @@ public override string GenerateSpoiler() public override void DisableDisallowedPassthroughs() { - foreach (Location location in Locations[Terrain.CAVE]) - { - location.IsPassthrough = false; - } - foreach (Location location in Locations[Terrain.TOWN]) - { - location.IsPassthrough = false; - } - foreach (Location location in Locations[Terrain.PALACE]) - { - location.IsPassthrough = false; - } + foreach (Location location in Locations[Terrain.CAVE]) + { + location.IsPassthrough = false; + } + foreach (Location location in Locations[Terrain.TOWN]) + { + location.IsPassthrough = false; + } + foreach (Location location in Locations[Terrain.PALACE]) + { + location.IsPassthrough = false; + } + } + + public override void ResetCollectables(RandomizerProperties props) + { + hammerCave.SetCollectables([Collectable.HAMMER]); + hammerCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + specRock.SetCollectables([Collectable.MAGIC_CONTAINER]); + specRock.CollectablesAreShufflable = props.ShuffleOverworldItems; } } diff --git a/RandomizerCore/Overworld/EastHyrule.cs b/RandomizerCore/Overworld/EastHyrule.cs index 5a8b46a66..a0c74b13d 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -5,6 +5,7 @@ using System.Text; using NLog; using Z2Randomizer.RandomizerCore.Enemy; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore.Overworld; @@ -12,7 +13,6 @@ namespace Z2Randomizer.RandomizerCore.Overworld; //6A35 - address in memory of palace 6 y coord public sealed class EastHyrule : World { - //private int bridgeCount; int debug = 0; private static readonly new Logger logger = LogManager.GetCurrentClassLogger(); @@ -65,16 +65,12 @@ public sealed class EastHyrule : World public Location locationAtPalace5; public Location locationAtPalace6; - public Location fountain; public Location waterTile; public Location desertTile; - public Location townAtDarunia; - public Location daruniaRoof; - public Location townAtNewKasuto; - public Location spellTower; - public Location newKasutoBasement; - public Location townAtNabooru; - public Location townAtOldKasuto; + public Location darunia; + public Location newKasuto; + public Location nabooru; + public Location oldKasuto; public Location locationAtGP; public Location pbagCave1; public Location pbagCave2; @@ -140,43 +136,28 @@ .. rom.LoadLocations(LocationID.EAST_TOWN_OLD_KASUTO, 4, terrains), //Palaces locationAtPalace5 = GetLocation(LocationID.EAST_PALACE5); - locationAtPalace5.PalaceNumber = 5; - locationAtPalace5.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; - locationAtPalace6 = GetLocation(LocationID.EAST_PALACE6); - locationAtPalace6.PalaceNumber = 6; - locationAtPalace6.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; - locationAtGP = GetLocation(LocationID.EAST_GREAT_PALACE); - locationAtGP.PalaceNumber = 7; - locationAtGP.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; // the same as this gets shuffled with regular palaces - locationAtGP.VanillaCollectable = Collectable.DO_NOT_USE; - locationAtGP.Collectables = []; //Towns - townAtNabooru = GetLocation(LocationID.EAST_TOWN_NABOORU); - townAtNabooru.VanillaCollectable = props.ReplaceFireWithDash ? Collectable.DASH_SPELL : Collectable.FIRE_SPELL; - townAtNabooru.Collectables = props.ReplaceFireWithDash ? [Collectable.DASH_SPELL] : [Collectable.FIRE_SPELL]; - townAtNabooru.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.WATER]) - : new Requirements([], [[RequirementType.WATER, RequirementType.FIVE_CONTAINERS]]); - - townAtDarunia = GetLocation(LocationID.EAST_TOWN_DARUNIA); - townAtDarunia.VanillaCollectable = Collectable.REFLECT_SPELL; - townAtDarunia.Collectables = [Collectable.REFLECT_SPELL]; - townAtDarunia.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.CHILD]) - : new Requirements([], [[RequirementType.CHILD, RequirementType.SIX_CONTAINERS]]); - - townAtNewKasuto = GetLocation(LocationID.EAST_TOWN_NEW_KASUTO); - townAtNewKasuto.VanillaCollectable = Collectable.SPELL_SPELL; - townAtNewKasuto.Collectables = [Collectable.SPELL_SPELL]; - townAtNewKasuto.CollectableRequirements = props.DisableMagicRecs ? Requirements.NONE : new Requirements([RequirementType.SEVEN_CONTAINERS]); - - townAtOldKasuto = GetLocation(LocationID.EAST_TOWN_OLD_KASUTO); - townAtOldKasuto.VanillaCollectable = Collectable.THUNDER_SPELL; - townAtOldKasuto.Collectables = [Collectable.THUNDER_SPELL]; - townAtOldKasuto.CollectableRequirements = props.DisableMagicRecs ? Requirements.NONE : new Requirements([RequirementType.EIGHT_CONTAINERS]); + nabooru = GetLocation(LocationID.EAST_TOWN_NABOORU); + nabooru.Town = Towns.LoadVanillaTown(rom, TownType.NABOORU, props.DisableMagicRecs); + + darunia = GetLocation(LocationID.EAST_TOWN_DARUNIA); + darunia.Town = Towns.LoadVanillaTown(rom, TownType.DARUNIA, props.DisableMagicRecs); + + newKasuto = GetLocation(LocationID.EAST_TOWN_NEW_KASUTO); + newKasuto.Town = Towns.LoadVanillaTown(rom, TownType.NEW_KASUTO, props.DisableMagicRecs); + newKasuto.Town.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!.AccessRequirements = props.NewKasutoBasementRequirement switch + { + 5 => new Requirements([RequirementType.FIVE_CONTAINERS]), + 6 => new Requirements([RequirementType.SIX_CONTAINERS]), + 7 => new Requirements([RequirementType.SEVEN_CONTAINERS]), + _ => throw new Exception($"Unsupported New Kasuto basement container count: {props.NewKasutoBasementRequirement}") + }; + + oldKasuto = GetLocation(LocationID.EAST_TOWN_OLD_KASUTO); + oldKasuto.Town = Towns.LoadVanillaTown(rom, TownType.OLD_KASUTO, props.DisableMagicRecs); waterTile = GetLocation(LocationID.EAST_WATER); waterTile.AccessRequirements = new Requirements([RequirementType.BOOTS]); @@ -228,52 +209,6 @@ .. rom.LoadLocations(LocationID.EAST_TOWN_OLD_KASUTO, 4, terrains), pbagCave2 = GetLocation(LocationID.EAST_CAVE_PBAG2); VANILLA_MAP_ADDR = 0x9056; - //Fake locations that dont correspond to anywhere on the map, but still hold logic and items - spellTower = new Location(townAtNewKasuto); - spellTower.VanillaCollectable = Collectable.MAGIC_KEY; - spellTower.Collectables = [Collectable.MAGIC_KEY]; - spellTower.CollectableRequirements = new Requirements([RequirementType.SPELL]); - spellTower.Name = "Spell Tower"; - spellTower.CanShuffle = false; - spellTower.ActualTown = null; - townAtNewKasuto.Children.Add(spellTower); - AddLocation(spellTower); - - newKasutoBasement = new Location(townAtNewKasuto); - newKasutoBasement.VanillaCollectable = Collectable.MAGIC_CONTAINER; - newKasutoBasement.Collectables = [Collectable.MAGIC_CONTAINER]; - newKasutoBasement.CollectableRequirements = props.NewKasutoBasementRequirement switch - { - 5 => new Requirements([RequirementType.FIVE_CONTAINERS]), - 6 => new Requirements([RequirementType.SIX_CONTAINERS]), - 7 => new Requirements([RequirementType.SEVEN_CONTAINERS]), - _ => throw new Exception($"Unsupported New Kasuto basement container count: {props.NewKasutoBasementRequirement}") - }; - newKasutoBasement.Name = "Granny's basement"; - newKasutoBasement.CanShuffle = false; - newKasutoBasement.ActualTown = null; - townAtNewKasuto.Children.Add(newKasutoBasement); - AddLocation(newKasutoBasement); - - fountain = new Location(townAtNabooru); - fountain.VanillaCollectable = Collectable.WATER; - fountain.Collectables = [Collectable.WATER]; - fountain.Name = "Water Fountain"; - fountain.ActualTown = Town.NABOORU_FOUNTAIN; - fountain.CanShuffle = false; - townAtNabooru.Children.Add(fountain); - AddLocation(fountain); - - daruniaRoof = new Location(townAtDarunia); - daruniaRoof.VanillaCollectable = Collectable.UPSTAB; - daruniaRoof.Collectables = [Collectable.UPSTAB]; - daruniaRoof.CollectableRequirements = new Requirements([RequirementType.FAIRY, RequirementType.JUMP]); - daruniaRoof.Name = "Darunia Roof"; - daruniaRoof.ActualTown = Town.DARUNIA_ROOF; - daruniaRoof.CanShuffle = false; - townAtDarunia.Children.Add(daruniaRoof); - AddLocation(daruniaRoof); - walkableTerrains = [Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, Terrain.SWAMP, Terrain.GRAVE]; randomTerrainFilter = [Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, Terrain.SWAMP, Terrain.GRAVE, Terrain.MOUNTAIN, Terrain.WALKABLEWATER]; @@ -357,17 +292,15 @@ .. rom.LoadLocations(LocationID.EAST_TOWN_OLD_KASUTO, 4, terrains), { (0x66, 0x2D), "south" }, { (0x49, 0x04), "gp" } }; - townAtNewKasuto.IsExternalWorld = true; + newKasuto.IsExternalWorld = true; locationAtPalace6.IsExternalWorld = true; hiddenPalaceLocation = locationAtPalace6; - hiddenKasutoLocation = townAtNewKasuto; + hiddenKasutoLocation = newKasuto; //Climate filtering climate = Climates.Create(props.EastClimate); climate.SeedTerrainCount = Math.Min(climate.SeedTerrainCount, biome.SeedTerrainLimit()); climate.DisallowTerrain(props.CanWalkOnWaterWithBoots ? Terrain.WATER : Terrain.WALKABLEWATER); - //climate.DisallowTerrain(Terrain.LAVA); - SetVanillaCollectables(props.ReplaceFireWithDash); } public override bool Terraform(RandomizerProperties props, ROM rom) @@ -435,7 +368,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) if (!props.ShuffleHidden) { - townAtNewKasuto.CanShuffle = false; + newKasuto.CanShuffle = false; locationAtPalace6.CanShuffle = false; } ShuffleLocations(AllLocations); @@ -806,7 +739,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) if(props.RiverDevilBlockerOption == RiverDevilBlockerOption.SIEGE) { //Iterate the towns in a random order - Location[] towns = [townAtNabooru, townAtDarunia, townAtNewKasuto, townAtOldKasuto]; + Location[] towns = [nabooru, darunia, newKasuto, oldKasuto]; RNG.Shuffle(towns); bool placed = false; foreach (Location location in towns) @@ -863,17 +796,13 @@ public override bool Terraform(RandomizerProperties props, ROM rom) if (props.HiddenPalace) { - rom.UpdateHiddenPalaceSpot(biome, hiddenPalaceCoords, hiddenPalaceLocation, - townAtNewKasuto, spellTower, !props.LegacyVanillaShuffledLocations); - hiddenPalaceLocation.AccessRequirements = hiddenPalaceLocation.AccessRequirements.WithHardRequirement(RequirementType.FLUTE); - hiddenPalaceLocation.Children.ForEach(i => i.AccessRequirements = i.AccessRequirements.WithHardRequirement(RequirementType.FLUTE)); - } + rom.UpdateHiddenPalaceSpot(biome, hiddenPalaceCoords, hiddenPalaceLocation, !props.LegacyVanillaShuffledLocations); + hiddenPalaceLocation.AccessRequirements = hiddenPalaceLocation.AccessRequirements.WithHardRequirement(RequirementType.FLUTE); } if (props.HiddenKasuto) { - rom.UpdateKasuto(hiddenKasutoLocation, townAtNewKasuto, spellTower, biome, + rom.UpdateKasuto(hiddenKasutoLocation, biome, baseAddr, terrains[hiddenKasutoLocation.ID], !props.LegacyVanillaShuffledLocations); hiddenKasutoLocation.AccessRequirements = hiddenPalaceLocation.AccessRequirements.WithHardRequirement(RequirementType.HAMMER); - hiddenKasutoLocation.Children.ForEach(i => i.AccessRequirements = i.AccessRequirements.WithHardRequirement(RequirementType.HAMMER)); } WriteMapToRom(rom, true, MAP_ADDR, MAP_SIZE_BYTES, hiddenPalaceLocation.Y, hiddenPalaceLocation.Xpos, props.HiddenPalace, props.HiddenKasuto); @@ -1558,7 +1487,7 @@ private void RandomizeHiddenKasuto(bool shuffleHidden) } else { - hiddenKasutoLocation = townAtNewKasuto; + hiddenKasutoLocation = newKasuto; } hiddenKasutoLocation.TerrainType = Terrain.FOREST; hiddenKasutoLocation.AccessRequirements = hiddenKasutoLocation.AccessRequirements.WithHardRequirement(RequirementType.HAMMER); @@ -1662,10 +1591,6 @@ private bool RandomizeHiddenPalace(ROM rom, bool shuffleHidden, bool hiddenKasut if (!AllReached) { base.UpdateAllReached(); - if (!hiddenPalaceLocation.Reachable || !hiddenKasutoLocation.Reachable || !spellTower.Reachable) - { - AllReached = false; - } } } @@ -1680,7 +1605,7 @@ public override void UpdateVisit(List requireables) if(location.AccessRequirements.AreSatisfiedBy(requireables)) { location.Reachable = true; - if (connections.ContainsKey(location) && location.ConnectionRequirements.AreSatisfiedBy(requireables)) + if (connections.ContainsKey(location) && (location.Town == null || location.Town.CanBeTraversed(requireables))) { Location connectedLocation = connections[location]; connectedLocation.Reachable = true; @@ -1947,8 +1872,6 @@ protected override List GetPathingStarts() public bool ValidateBasicRouting() { List unreachedLocations = RequiredLocations(false, false).ToList(); - //SpellTower's connection logic isn't implemented here, nor do we care since we're assuming you have everything. - unreachedLocations.Remove(spellTower); bool[,] visitedCoordinates = new bool[MapRows, MapColumns]; List<(int, int)> pendingCoordinates = new(); @@ -2000,15 +1923,11 @@ public bool ValidateBasicRouting() } } while (pendingCoordinates.Count > 0); - return !unreachedLocations.Any(); + return unreachedLocations.Count == 0; } protected override void OnUpdateReachableTrigger() { - foreach(Location parentLocation in AllLocations.Where(i => i.Children != null && i.ActualTown != null)) - { - parentLocation.Children.ForEach(child => child.Reachable = parentLocation.Reachable); - } } public override string GetName() @@ -2024,11 +1943,10 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool locationAtPalace6, waterTile, desertTile, - townAtDarunia, - townAtNewKasuto, - spellTower, - townAtNabooru, - townAtOldKasuto, + darunia, + newKasuto, + nabooru, + oldKasuto, locationAtGP, pbagCave1, pbagCave2, @@ -2059,52 +1977,50 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool return requiredLocations.Where(i => i != null); } + /* protected override void SetVanillaCollectables(bool useDash) { locationAtPalace5.VanillaCollectable = Collectable.FLUTE; locationAtPalace6.VanillaCollectable = Collectable.CROSS; - townAtNabooru.VanillaCollectable = useDash ? Collectable.DASH_SPELL : Collectable.FIRE_SPELL; + nabooru.VanillaCollectable = useDash ? Collectable.DASH_SPELL : Collectable.FIRE_SPELL; fountain.VanillaCollectable = Collectable.WATER; - townAtDarunia.VanillaCollectable = Collectable.REFLECT_SPELL; + darunia.VanillaCollectable = Collectable.REFLECT_SPELL; daruniaRoof.VanillaCollectable = Collectable.UPSTAB; newKasutoBasement.VanillaCollectable = Collectable.MAGIC_CONTAINER; - townAtNewKasuto.VanillaCollectable = Collectable.SPELL_SPELL; + newKasuto.VanillaCollectable = Collectable.SPELL_SPELL; spellTower.VanillaCollectable = Collectable.MAGIC_KEY; - townAtOldKasuto.VanillaCollectable = Collectable.THUNDER_SPELL; + oldKasuto.VanillaCollectable = Collectable.THUNDER_SPELL; waterTile.VanillaCollectable = Collectable.HEART_CONTAINER; desertTile.VanillaCollectable = Collectable.HEART_CONTAINER; pbagCave1.VanillaCollectable = Collectable.XL_BAG; pbagCave2.VanillaCollectable = Collectable.XL_BAG; } + */ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("EAST: "); - sb.AppendLine("\tNabooru: " + AllLocations.First(i => i.ActualTown == Town.NABOORU).Collectables[0].EnglishText()); - sb.AppendLine("\tFountain: " + fountain.Collectables[0].EnglishText()); - sb.AppendLine("\tDarunia: " + AllLocations.First(i => i.ActualTown == Town.DARUNIA_WEST).Collectables[0].EnglishText()); - sb.AppendLine("\tUpstab Guy: " + daruniaRoof.Collectables[0].EnglishText()); - sb.AppendLine("\tNew Kasuto: " + AllLocations.First(i => i.ActualTown == Town.NEW_KASUTO).Collectables[0].EnglishText()); - sb.AppendLine("\tGranny's Basement: " + newKasutoBasement.Collectables[0].EnglishText()); - sb.AppendLine("\tSpell Tower: " + spellTower.Collectables[0].EnglishText()); - sb.AppendLine("\tOld Kasuto: " + AllLocations.First(i => i.ActualTown == Town.OLD_KASUTO).Collectables[0].EnglishText()); + sb.AppendLine("Nabooru: \n" + nabooru.Town!.GenerateSpoiler()); + sb.AppendLine("Darunia: \n" + nabooru.Town!.GenerateSpoiler()); + sb.AppendLine("New Kasuto: \n" + nabooru.Town!.GenerateSpoiler()); + sb.AppendLine("Old Kasuto: \n" + nabooru.Town!.GenerateSpoiler()); - sb.AppendLine("\tRisen Pbag Cave: " + pbagCave2.Collectables[0].EnglishText()); - sb.AppendLine("\tSunken Pbag Cave: " + pbagCave1.Collectables[0].EnglishText()); - sb.AppendLine("\tWater Tile: " + waterTile.Collectables[0].EnglishText()); - sb.AppendLine("\tDesert tile: " + desertTile.Collectables[0].EnglishText()); + sb.AppendLine("\tRisen Pbag Cave: " + pbagCave2.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tSunken Pbag Cave: " + pbagCave1.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tWater Tile: " + waterTile.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tDesert tile: " + desertTile.GetAllCollectables()[0].EnglishText()); - sb.Append("\tPalace 5 (" + locationAtPalace5.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace5.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace5.Collectables.Select(c => c.EnglishText()))); + sb.Append("\tPalace 5 (" + locationAtPalace5.Palace!.Number + "): "); + sb.AppendLine(locationAtPalace5.GetAllCollectables().Count == 0 ? "No Items" : string.Join(", ", locationAtPalace5.GetAllCollectables().Select(c => c.EnglishText()))); - sb.Append("\tPalace 6 (" + locationAtPalace6.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace6.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace6.Collectables.Select(c => c.EnglishText()))); + sb.Append("\tPalace 6 (" + locationAtPalace6.Palace!.Number + "): "); + sb.AppendLine(locationAtPalace6.GetAllCollectables().Count == 0 ? "No Items" : string.Join(", ", locationAtPalace6.GetAllCollectables().Select(c => c.EnglishText()))); - sb.Append("\tPalace 7 (" + locationAtGP.PalaceNumber + "): "); - sb.AppendLine(locationAtGP.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtGP.Collectables.Select(c => c.EnglishText()))); + sb.Append("\tPalace 7 (" + locationAtGP.Palace!.Number + "): "); + sb.AppendLine(locationAtGP.GetAllCollectables().Count == 0 ? "No Items" : string.Join(", ", locationAtGP.GetAllCollectables().Select(c => c.EnglishText()))); sb.AppendLine(); return sb.ToString(); @@ -2161,4 +2077,41 @@ public override void DisableDisallowedPassthroughs() swamp.AccessRequirements = swamp.AccessRequirements.Without(RequirementType.FAIRY); } } + + public override void ResetCollectables(RandomizerProperties props) + { + nabooru.Town!.GetWizard()!.Collectable = props.ReplaceFireWithDash ? Collectable.DASH_SPELL : Collectable.FAIRY_SPELL; + nabooru.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + nabooru.Town!.GetTownMap(VanillaTownMap.NABOORU_MID)!.Collectable = Collectable.WATER; + nabooru.Town!.GetTownMap(VanillaTownMap.NABOORU_MID)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + + darunia.Town!.GetWizard()!.Collectable = Collectable.REFLECT_SPELL; + darunia.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + darunia.Town!.GetTownMap(VanillaTownMap.DARUNIA_TRAINER)!.Collectable = Collectable.UPSTAB; + darunia.Town!.GetTownMap(VanillaTownMap.DARUNIA_TRAINER)!.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + + newKasuto.Town!.GetWizard()!.Collectable = Collectable.SPELL_SPELL; + newKasuto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + newKasuto.Town!.GetTownMap(VanillaTownMap.SPELL_TOWER_INTERIOR)!.Collectable = Collectable.MAGIC_KEY; + newKasuto.Town!.GetTownMap(VanillaTownMap.SPELL_TOWER_INTERIOR)!.CollectableIsShufflable = props.ShuffleOverworldItems; + newKasuto.Town!.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!.Collectable = Collectable.MAGIC_CONTAINER; + newKasuto.Town!.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!.CollectableIsShufflable = props.ShuffleOverworldItems; + + oldKasuto.Town!.GetWizard()!.Collectable = Collectable.THUNDER_SPELL; + oldKasuto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + + desertTile.SetCollectables([Collectable.HEART_CONTAINER]); + desertTile.CollectablesAreShufflable = props.ShuffleOverworldItems; + waterTile.SetCollectables([Collectable.HEART_CONTAINER]); + waterTile.CollectablesAreShufflable = props.ShuffleOverworldItems; + pbagCave1.SetCollectables([Collectable.XL_BAG]); + pbagCave1.CollectablesAreShufflable = props.ShuffleOverworldItems && props.PbagItemShuffle; + pbagCave2.SetCollectables([Collectable.XL_BAG]); + pbagCave2.CollectablesAreShufflable = props.ShuffleOverworldItems && props.PbagItemShuffle; + + + locationAtPalace5.SetCollectables(locationAtPalace5.Palace!.GetVanillaCollectables()); + locationAtPalace6.SetCollectables(locationAtPalace6.Palace!.GetVanillaCollectables()); + locationAtGP.SetCollectables(locationAtGP.Palace!.GetVanillaCollectables()); + } } \ No newline at end of file diff --git a/RandomizerCore/Overworld/Location.cs b/RandomizerCore/Overworld/Location.cs index 23536853e..142bd9b74 100644 --- a/RandomizerCore/Overworld/Location.cs +++ b/RandomizerCore/Overworld/Location.cs @@ -3,6 +3,9 @@ using System.Diagnostics; using System.Collections.Generic; using System.Linq; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; +using DynamicData; namespace Z2Randomizer.RandomizerCore.Overworld; @@ -13,13 +16,14 @@ public class Location public LocationID ID { get; set; } - public List Collectables { get; set; } - public Collectable VanillaCollectable { get; set; } + private List NonPalaceTownCollectables { get; set; } + public bool CollectablesAreShufflable { get; set; } public bool AppearsOnMap { get; set; } public bool Reachable { get; set; } - public int? PalaceNumber { get; set; } - public Town? ActualTown { get; set; } + public Town? Town { get; set; } + public Palace? Palace { get; set; } + //public TownEnum? ActualTown { get; set; } public Continent Continent { get; set; } public Continent? VanillaContinent { get; set; } public Continent? ConnectedContinent { get; set; } @@ -27,7 +31,6 @@ public class Location public Terrain TerrainType { get; set; } //public byte[] LocationBytes { get; set; } - public List Children { get; set; } = []; public int MemAddress { get; set; } @@ -81,11 +84,6 @@ public int Xpos //List of requirements to access this location. Required before the other requirements are evaluated, //and to pass through the location on the map, even if the collectable is not gettable. public Requirements AccessRequirements; - //Requirements to get the thing(s) here. For palaces, this does not (yet) consider the layout of the palace or the rooms in it - //only the general "need fairy or key" requirements for palaces generally - public Requirements CollectableRequirements; - //If this location is a connector, these are the requirements for the other side of the connection to be reached - public Requirements ConnectionRequirements; //This does 2 things and should only do 1. It both tracks whether the location is a location that should be possible to shuffle, //and tracks whether this location has actually been shuffled already. This persistence doesn't always get reset properly, @@ -151,20 +149,21 @@ public Location(LocationID lid, int yPos, int xPos, int map, Continent continent MemAddress = lid.GetRomOffset(); EntranceNumber = 0; CanShuffle = true; - Collectables = []; + NonPalaceTownCollectables = []; Reachable = false; - PalaceNumber = null; + Palace = null; Continent = continent; VanillaContinent = lid.GetContinent(); ConnectedContinent = null; AccessRequirements = Requirements.NONE; - CollectableRequirements = Requirements.NONE; - ConnectionRequirements = Requirements.NONE; IsPassthrough = isPassthrough; WasPassthrough = isPassthrough; Name = ID.GetName() ?? "Unknown (" + Continent.GetName(Continent) + ")"; - ActualTown = ID.GetTown(); + if (Town != null) + { + Town.Type = ID.GetTown(); + } if (Name.StartsWith("Unknown") && Xpos != 0 && YRaw != 0) { @@ -212,7 +211,7 @@ public string GetDebuggerDisplay() + " " + Name + " (" + (Y) + "," + (Xpos) + ") _" + (Reachable ? "Reachable " : "Unreachable ") - + '[' + string.Join(", ", Collectables.Select(i => i.ToString())) + ']'; + + '[' + string.Join(", ", GetAllCollectables().Select(i => i.ToString())) + ']'; } public void ResetCoords() @@ -223,7 +222,7 @@ public void ResetCoords() public int GetWorld() { //Towns reference their banks - if (ActualTown != null) + if (Town?.Type != null) { return Continent == Continent.WEST ? 4 : 10; } @@ -246,12 +245,10 @@ public int GetWorld() } //Palaces... have world numbers that kind... of... make sense? //This is what they are. - if (PalaceNumber != null) + if (Palace != null) { - return PalaceNumber switch + return Palace.Number switch { - //North palace - null => 0, 1 => 0b00001100 + (int)Continent, //12, 2 => 0b00001100 + (int)Continent, //12, 3 => 0b00010000 + (int)Continent, //16, @@ -277,4 +274,75 @@ public void Clear() EntranceNumber = 0; CanShuffle = false; } + + public List GetAllCollectables() + { + return GetGettableItems(RequirementTypeExtensions.ALL); + } + + public List GetShufflableCollectables() + { + return GetGettableItems(RequirementTypeExtensions.ALL, ForceEnterRight ? Direction.EAST : Direction.WEST, true); + } + + public List GetGettableItems(List requireables) + { + return GetGettableItems(requireables, ForceEnterRight ? Direction.EAST : Direction.WEST); + } + + public List GetGettableItems(List requireables, Direction entranceDirection, bool shufflableItemsOnly = false) + { + if (Town != null) + { + return Town.GetGettableItems(entranceDirection, requireables, shufflableItemsOnly); + } + if (Palace != null) + { + return Palace.GetGettableItems(requireables, shufflableItemsOnly); + } + if(shufflableItemsOnly && !CollectablesAreShufflable) + { + return []; + } + return AccessRequirements.AreSatisfiedBy(requireables) ? NonPalaceTownCollectables : []; + } + + public void SetCollectables(IEnumerable collectables) + { + Debug.Assert(Town == null || Palace == null); + if (Town != null) + { + Town.SetCollectables(collectables); + } + else if (Palace != null) + { + Palace.SetCollectables(collectables); + } + else NonPalaceTownCollectables = [.. collectables]; + } + + /// + /// Replaces the first instance of a given collectable at a location with the indicated collectable. + /// Intended to be used to replace unique items, and "first" is currently undefined in order so be careful (or fix this) + /// + /// + /// + public bool ReplaceCollectable(Collectable toReplace, Collectable replacement) + { + Debug.Assert(Town == null || Palace == null); + if (Town != null) + { + return Town.ReplaceCollectable(toReplace, replacement); + } + else if (Palace != null) + { + return Palace.ReplaceCollectable(toReplace, replacement); + } + else if (NonPalaceTownCollectables.Contains(toReplace)) + { + NonPalaceTownCollectables.Replace(toReplace, replacement); + return true; + } + return false; + } } diff --git a/RandomizerCore/Overworld/LocationID.cs b/RandomizerCore/Overworld/LocationID.cs index 2216f094a..7636772ea 100644 --- a/RandomizerCore/Overworld/LocationID.cs +++ b/RandomizerCore/Overworld/LocationID.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore.Overworld; @@ -276,18 +277,18 @@ public static IEnumerable EnumerateNamed(LocationID startLid, int li public static class LocationIDExtensions { - static readonly Dictionary townMap = new() + static readonly Dictionary townMap = new() { - [LocationID.WEST_TOWN_RAURO] = Town.RAURU, - [LocationID.WEST_TOWN_RUTO] = Town.RUTO, - [LocationID.WEST_TOWN_SARIA_NORTH] = Town.SARIA_NORTH, - [LocationID.WEST_TOWN_SARIA_SOUTH] = Town.SARIA_SOUTH, - [LocationID.WEST_BAGU_HOUSE] = Town.BAGU, - [LocationID.WEST_TOWN_MIDO] = Town.MIDO_WEST, - [LocationID.EAST_TOWN_NABOORU] = Town.NABOORU, - [LocationID.EAST_TOWN_DARUNIA] = Town.DARUNIA_WEST, - [LocationID.EAST_TOWN_OLD_KASUTO] = Town.OLD_KASUTO, - [LocationID.EAST_TOWN_NEW_KASUTO] = Town.NEW_KASUTO, + [LocationID.WEST_TOWN_RAURO] = TownType.RAURU, + [LocationID.WEST_TOWN_RUTO] = TownType.RUTO, + [LocationID.WEST_TOWN_SARIA_NORTH] = TownType.SARIA, + [LocationID.WEST_TOWN_SARIA_SOUTH] = TownType.SARIA, + [LocationID.WEST_BAGU_HOUSE] = TownType.BAGU, + [LocationID.WEST_TOWN_MIDO] = TownType.MIDO, + [LocationID.EAST_TOWN_NABOORU] = TownType.NABOORU, + [LocationID.EAST_TOWN_DARUNIA] = TownType.DARUNIA, + [LocationID.EAST_TOWN_OLD_KASUTO] = TownType.OLD_KASUTO, + [LocationID.EAST_TOWN_NEW_KASUTO] = TownType.NEW_KASUTO, }; public static bool IsNamed(this LocationID lid) @@ -338,9 +339,9 @@ public static int GetRomOffset(this LocationID lid, int byteIdx = 0) /// /// /// - public static Town? GetTown(this LocationID lid) + public static TownType? GetTown(this LocationID lid) { - if (townMap.TryGetValue(lid, out Town town)) + if (townMap.TryGetValue(lid, out TownType town)) return town; return null; diff --git a/RandomizerCore/Overworld/MazeIsland.cs b/RandomizerCore/Overworld/MazeIsland.cs index a55458a1d..8f9934706 100644 --- a/RandomizerCore/Overworld/MazeIsland.cs +++ b/RandomizerCore/Overworld/MazeIsland.cs @@ -75,8 +75,6 @@ .. rom.LoadLocations(LocationID.MI_CHILD_DROP, 1, terrains), childDrop = GetLocation(LocationID.MI_CHILD_DROP); magicContainerDrop = GetLocation(LocationID.MI_MAGIC_CONTAINER_DROP); locationAtPalace4 = GetLocation(LocationID.MI_PALACE4); - locationAtPalace4.PalaceNumber = 4; - locationAtPalace4.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; continentId = Continent.MAZE; baseAddr = RomMap.ContinentLocationBases[continentId]; @@ -111,7 +109,7 @@ .. rom.LoadLocations(LocationID.MI_CHILD_DROP, 1, terrains), trapLocations.Remove(removeLoc); } } - SetVanillaCollectables(props.ReplaceFireWithDash); + //SetVanillaCollectables(props.ReplaceFireWithDash); } public override bool Terraform(RandomizerProperties props, ROM rom) @@ -961,6 +959,7 @@ public override void UpdateVisit(List requireables) } } + //DebugVisitation(); foreach (Location location in AllLocations) { if (visitation[location.Y, location.Xpos]) @@ -1004,22 +1003,25 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool return requiredLocations.Where(i => i != null); } + /* protected override void SetVanillaCollectables(bool useDash) { locationAtPalace4.VanillaCollectable = Collectable.BOOTS; childDrop.VanillaCollectable = Collectable.CHILD; magicContainerDrop.VanillaCollectable = Collectable.MAGIC_CONTAINER; } + */ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("MAZE ISLAND: "); - sb.AppendLine("\tMagic Container Drop: " + magicContainerDrop.Collectables[0].EnglishText()); - sb.AppendLine("\tChild Drop: " + childDrop.Collectables[0].EnglishText()); + sb.AppendLine("\tMagic Container Drop: " + magicContainerDrop.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tChild Drop: " + childDrop.GetAllCollectables()[0].EnglishText()); - sb.Append("\tPalace 4 (" + locationAtPalace4.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace4.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace4.Collectables.Select(c => c.EnglishText()))); + sb.Append("\tPalace 4 (" + locationAtPalace4.Palace!.Number + "): "); + List palaceCollectables = locationAtPalace4.GetAllCollectables(); + sb.AppendLine(palaceCollectables.Count == 0 ? "No Items" : string.Join(", ", palaceCollectables.Select(c => c.EnglishText()))); sb.AppendLine(); return sb.ToString(); @@ -1046,4 +1048,14 @@ public override void DisableDisallowedPassthroughs() magicContainerDrop.IsPassthrough = false; childDrop.IsPassthrough = false; } + + public override void ResetCollectables(RandomizerProperties props) + { + childDrop.SetCollectables([Collectable.CHILD]); + childDrop.CollectablesAreShufflable = props.ShuffleOverworldItems; + magicContainerDrop.SetCollectables([Collectable.MAGIC_CONTAINER]); + magicContainerDrop.CollectablesAreShufflable = props.ShuffleOverworldItems; + + locationAtPalace4.SetCollectables(locationAtPalace4.Palace!.GetVanillaCollectables()); + } } diff --git a/RandomizerCore/Overworld/WestHyrule.cs b/RandomizerCore/Overworld/WestHyrule.cs index 2d206da32..56c5380d3 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -5,6 +5,7 @@ using System.Text; using NLog; using Z2Randomizer.RandomizerCore.Enemy; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore.Overworld; @@ -13,11 +14,12 @@ public sealed class WestHyrule : World private readonly new Logger logger = LogManager.GetCurrentClassLogger(); public Location northPalace; - public Location locationAtMido; + public Location rauru; + public Location ruto; + public Location sariaNorth; + public Location sariaSouth; + public Location mido; public Location bagu; - public Location mirrorTable; - public Location midoChurch; - public Location locationAtRuto; public Location medicineCave; public Location trophyCave; public Location locationAtPalace1; @@ -26,9 +28,6 @@ public sealed class WestHyrule : World public Location magicContainerCave; public Location grassTile; public Location heartContainerCave; - public Location locationAtSariaNorth; - public Location locationAtSariaSouth; - public Location locationAtRauru; private Location bridge1; private Location bridge2; @@ -121,9 +120,6 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), northPalace = GetLocation(LocationID.WEST_NORTH_PALACE); //0x462f medicineCave = GetLocation(LocationID.WEST_CAVE_MEDICINE); //0x463e - bagu = GetLocation(LocationID.WEST_BAGU_HOUSE); //0x4661 - bagu.ActualTown = Town.BAGU; - bagu.Collectables = [Collectable.BAGUS_NOTE]; trophyCave = GetLocation(LocationID.WEST_CAVE_TROPHY); //0x00004630 magicContainerCave = GetLocation(LocationID.WEST_CAVE_MAGIC_CONTAINER); grassTile = GetLocation(LocationID.WEST_GRASS); @@ -131,46 +127,33 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), pbagCave = GetLocation(LocationID.WEST_CAVE_PBAG); //Towns - locationAtRauru = GetLocation(LocationID.WEST_TOWN_RAURO); - locationAtRauru.Collectables = [Collectable.SHIELD_SPELL]; - locationAtRauru.CollectableRequirements = props.DisableMagicRecs ? Requirements.NONE : new Requirements([RequirementType.ONE_CONTAINER]); - - locationAtRuto = GetLocation(LocationID.WEST_TOWN_RUTO); //0x465e - locationAtRuto.Collectables = [Collectable.JUMP_SPELL]; - locationAtRuto.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.TROPHY]) - : new Requirements([], [[RequirementType.TROPHY, RequirementType.TWO_CONTAINERS]]); - - locationAtSariaNorth = GetLocation(LocationID.WEST_TOWN_SARIA_NORTH); //0x00004660 - locationAtSariaNorth.Collectables = [Collectable.LIFE_SPELL]; - locationAtSariaNorth.ConnectionRequirements = new Requirements([RequirementType.FAIRY, RequirementType.BAGU_LETTER], [[RequirementType.JUMP, RequirementType.DASH]]); - locationAtSariaNorth.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.MIRROR]) - : new Requirements([], [[RequirementType.MIRROR, RequirementType.THREE_CONTAINERS]]); - - locationAtSariaSouth = GetLocation(LocationID.WEST_TOWN_SARIA_SOUTH); //0x0000465f - locationAtSariaSouth.ConnectionRequirements = locationAtSariaNorth.ConnectionRequirements; - - locationAtMido = GetLocation(LocationID.WEST_TOWN_MIDO); //0x00004662 - locationAtMido.Collectables = [Collectable.FAIRY_SPELL]; - locationAtMido.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.MEDICINE]) - : new Requirements([], [[RequirementType.MEDICINE, RequirementType.FOUR_CONTAINERS]]); + bagu = GetLocation(LocationID.WEST_BAGU_HOUSE); //0x4661 + bagu.Town = Towns.LoadVanillaTown(rom, TownType.BAGU, props.DisableMagicRecs); + + rauru = GetLocation(LocationID.WEST_TOWN_RAURO); + rauru.Town = Towns.LoadVanillaTown(rom, TownType.RAURU, props.DisableMagicRecs); + + ruto = GetLocation(LocationID.WEST_TOWN_RUTO); //0x465e + ruto.Town = Towns.LoadVanillaTown(rom, TownType.RUTO, props.DisableMagicRecs); + + sariaNorth = GetLocation(LocationID.WEST_TOWN_SARIA_NORTH); //0x00004660 + Town sariaTown = Towns.LoadVanillaTown(rom, TownType.SARIA, props.DisableMagicRecs); + sariaNorth.Town = sariaTown; + + sariaSouth = GetLocation(LocationID.WEST_TOWN_SARIA_SOUTH); //0x0000465f + sariaSouth.Town = sariaTown; + + mido = GetLocation(LocationID.WEST_TOWN_MIDO); //0x00004662 + mido.Town = Towns.LoadVanillaTown(rom, TownType.MIDO, props.DisableMagicRecs); //Palaces locationAtPalace1 = GetLocation(LocationID.WEST_PALACE1); - locationAtPalace1.PalaceNumber = 1; - locationAtPalace1.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; locationAtPalace2 = GetLocation(LocationID.WEST_PALACE2); - locationAtPalace2.PalaceNumber = 2; - locationAtPalace2.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; locationAtPalace3 = GetLocation(LocationID.WEST_PALACE3); - locationAtPalace3.PalaceNumber = 3; - locationAtPalace3.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; //Connectors fairyCave = GetLocation(LocationID.WEST_FAIRY_CAVE_DROP); //0x4640 - fairyCave.ConnectionRequirements = new Requirements([RequirementType.FAIRY]); + fairyCave.AccessRequirements = new Requirements([RequirementType.FAIRY]); jumpCave = GetLocation(LocationID.WEST_CAVE_JUMP_NORTH); //0x463b jumpCave.AccessRequirements = new Requirements([RequirementType.JUMP, RequirementType.FAIRY]); @@ -184,24 +167,6 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), bridge1 = GetLocation(LocationID.WEST_BRIDGE_AFTER_DM_WEST); //0x4644 bridge2 = GetLocation(LocationID.WEST_BRIDGE_AFTER_DM_EAST); //0x4645 - //Fake locations that dont correspond to anywhere on the map, but still hold logic and items - mirrorTable = new Location(locationAtSariaNorth); - mirrorTable.Collectables = [Collectable.MIRROR]; - mirrorTable.ActualTown = Town.SARIA_TABLE; - mirrorTable.Name = "Saria Mirror Table"; - mirrorTable.CanShuffle = false; - locationAtSariaNorth.Children.Add(mirrorTable); - AddLocation(mirrorTable); - - midoChurch = new Location(locationAtMido); - midoChurch.Collectables = [Collectable.DOWNSTAB]; - midoChurch.CollectableRequirements = new Requirements([RequirementType.JUMP, RequirementType.FAIRY]); - midoChurch.ActualTown = Town.MIDO_CHURCH; - midoChurch.Name = "Mido Church"; - midoChurch.CanShuffle = false; - locationAtMido.Children.Add(midoChurch); - AddLocation(midoChurch); - caveConnections = []; // (left,right) caveConnections.Add((parapaCave1, parapaCave2)); caveConnections.Add((jumpCave, jumpCave2)); @@ -360,7 +325,7 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), GetLocation(LocationID.WEST_BAGU_WOODS5), GetLocation(LocationID.WEST_BAGU_WOODS1) ]; - SetVanillaCollectables(props.ReplaceFireWithDash); + //SetVanillaCollectables(props.ReplaceFireWithDash); } public override bool Terraform(RandomizerProperties props, ROM rom) @@ -430,8 +395,8 @@ public override bool Terraform(RandomizerProperties props, ROM rom) { AllLocations.ForEach(i => i.CanShuffle = true); Terrain riverTerrain = Terrain.MOUNTAIN; - locationAtSariaSouth.CanShuffle = false; - locationAtSariaNorth.CanShuffle = false; + sariaSouth.CanShuffle = false; + sariaNorth.CanShuffle = false; map = new Terrain[MapRows, MapColumns]; @@ -501,8 +466,8 @@ public override bool Terraform(RandomizerProperties props, ROM rom) rows--; } } - locationAtSariaSouth.CanShuffle = false; - locationAtSariaNorth.CanShuffle = false; + sariaSouth.CanShuffle = false; + sariaNorth.CanShuffle = false; walkableTerrains = [Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, Terrain.SWAMP, Terrain.GRAVE]; //Islands specifically seeds preplaced water instead of normally expandable water to make wetlands less insane randomTerrainFilter = new List { Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, @@ -591,8 +556,8 @@ public override bool Terraform(RandomizerProperties props, ROM rom) rows--; } } - locationAtSariaSouth.CanShuffle = false; - locationAtSariaNorth.CanShuffle = false; + sariaSouth.CanShuffle = false; + sariaNorth.CanShuffle = false; break; default: walkableTerrains = [Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, Terrain.SWAMP, Terrain.GRAVE]; @@ -1397,7 +1362,7 @@ public override void UpdateVisit(List requireables) if (location.AccessRequirements.AreSatisfiedBy(requireables)) { location.Reachable = true; - if (connections.ContainsKey(location) && location.ConnectionRequirements.AreSatisfiedBy(requireables)) + if (connections.ContainsKey(location) && (location.Town == null || location.Town.CanBeTraversed(requireables))) { Location connectedLocation = connections[location]; connectedLocation.Reachable = true; @@ -1488,11 +1453,11 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool HashSet requiredLocations = [ northPalace, - locationAtRauru, - locationAtMido, - locationAtRuto, - locationAtSariaNorth, - locationAtSariaSouth, + rauru, + mido, + ruto, + sariaNorth, + sariaSouth, bagu, medicineCave, @@ -1517,16 +1482,17 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool return requiredLocations.Where(i => i != null); } + /* protected override void SetVanillaCollectables(bool useFire) { locationAtPalace1.VanillaCollectable = Collectable.CANDLE; locationAtPalace2.VanillaCollectable = Collectable.GLOVE; locationAtPalace3.VanillaCollectable = Collectable.RAFT; - locationAtRauru.VanillaCollectable = Collectable.SHIELD_SPELL; - locationAtRuto.VanillaCollectable = Collectable.JUMP_SPELL; - locationAtSariaNorth.VanillaCollectable = Collectable.LIFE_SPELL; - locationAtMido.VanillaCollectable = Collectable.FAIRY_SPELL; + rauru.VanillaCollectable = Collectable.SHIELD_SPELL; + ruto.VanillaCollectable = Collectable.JUMP_SPELL; + sariaNorth.VanillaCollectable = Collectable.LIFE_SPELL; + mido.VanillaCollectable = Collectable.FAIRY_SPELL; midoChurch.VanillaCollectable = Collectable.DOWNSTAB; bagu.VanillaCollectable = Collectable.BAGUS_NOTE; mirrorTable.VanillaCollectable = Collectable.MIRROR; @@ -1538,34 +1504,36 @@ protected override void SetVanillaCollectables(bool useFire) trophyCave.VanillaCollectable = Collectable.TROPHY; medicineCave.VanillaCollectable = Collectable.MEDICINE; } + */ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("WEST: "); - sb.AppendLine("\tRauru: " + AllLocations.First(i => i.ActualTown == Town.RAURU).Collectables[0].EnglishText()); - sb.AppendLine("\tRuto: " + AllLocations.First(i => i.ActualTown == Town.RUTO).Collectables[0].EnglishText()); - sb.AppendLine("\tMirror Table: " + mirrorTable.Collectables[0].EnglishText()); - sb.AppendLine("\tSaria: " + AllLocations.First(i => i.ActualTown == Town.SARIA_NORTH).Collectables[0].EnglishText()); - sb.AppendLine("\tDownstab Guy: " + midoChurch.Collectables[0].EnglishText()); - sb.AppendLine("\tMido: " + AllLocations.First(i => i.ActualTown == Town.MIDO_WEST).Collectables[0].EnglishText()); - sb.AppendLine("\tBagu: " + bagu.Collectables[0].EnglishText()); - - sb.AppendLine("\tMagic Container Cave: " + magicContainerCave.Collectables[0].EnglishText()); - sb.AppendLine("\tTrophy Cave: " + trophyCave.Collectables[0].EnglishText()); - sb.AppendLine("\tGrass Tile: " + grassTile.Collectables[0].EnglishText()); - sb.AppendLine("\tHeart Container Cave: " + heartContainerCave.Collectables[0].EnglishText()); - sb.AppendLine("\tPillar Pbag Cave: " + pbagCave.Collectables[0].EnglishText()); - sb.AppendLine("\tMedicine Cave: " + medicineCave.Collectables[0].EnglishText()); - - sb.Append("\tPalace 1 (" + locationAtPalace1.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace1.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace1.Collectables.Select(c => c.EnglishText()))); - - sb.Append("\tPalace 2 (" + locationAtPalace2.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace2.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace2.Collectables.Select(c => c.EnglishText()))); - - sb.Append("\tPalace 3 (" + locationAtPalace3.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace3.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace3.Collectables.Select(c => c.EnglishText()))); + sb.AppendLine("Rauru: " + rauru.Town!.GenerateSpoiler()); + sb.AppendLine("Ruto: " + ruto.Town!.GenerateSpoiler()); + sb.AppendLine("Saria: " + sariaNorth.Town!.GenerateSpoiler()); + sb.AppendLine("Mido: " + mido.Town!.GenerateSpoiler()); + sb.AppendLine("Bagu: " + bagu.Town!.GenerateSpoiler()); + + sb.AppendLine("\tMagic Container Cave: " + magicContainerCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tTrophy Cave: " + trophyCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tGrass Tile: " + grassTile.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tHeart Container Cave: " + heartContainerCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tPillar Pbag Cave: " + pbagCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tMedicine Cave: " + medicineCave.GetAllCollectables()[0].EnglishText()); + + List palaceCollectables = locationAtPalace1.GetAllCollectables(); + sb.Append("\tPalace 1 (" + locationAtPalace1.Palace!.Number + "): "); + sb.AppendLine(palaceCollectables.Count == 0 ? "No Items" : string.Join(", ", palaceCollectables.Select(c => c.EnglishText()))); + + palaceCollectables = locationAtPalace2.GetAllCollectables(); + sb.Append("\tPalace 2 (" + locationAtPalace2.Palace!.Number + "): "); + sb.AppendLine(palaceCollectables.Count == 0 ? "No Items" : string.Join(", ", palaceCollectables.Select(c => c.EnglishText()))); + + palaceCollectables = locationAtPalace3.GetAllCollectables(); + sb.Append("\tPalace 3 (" + locationAtPalace3.Palace!.Number + "): "); + sb.AppendLine(palaceCollectables.Count == 0 ? "No Items" : string.Join(", ", palaceCollectables.Select(c => c.EnglishText()))); sb.AppendLine(); return sb.ToString(); @@ -1573,10 +1541,6 @@ public override string GenerateSpoiler() protected override void OnUpdateReachableTrigger() { - if (AllLocations.Where(i => i.ActualTown == Town.SARIA_NORTH).FirstOrDefault()?.Reachable ?? false) - { - mirrorTable.Reachable = true; - } } public override void DisableDisallowedPassthroughs() @@ -1602,4 +1566,43 @@ public override void DisableDisallowedPassthroughs() fairyCave.IsPassthrough = false; //fairy cave } + + public override void ResetCollectables(RandomizerProperties props) + { + rauru.Town!.GetWizard()!.Collectable = Collectable.SHIELD_SPELL; + rauru.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + + ruto.Town!.GetWizard()!.Collectable = Collectable.JUMP_SPELL; + ruto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + + sariaNorth.Town!.GetWizard()!.Collectable = Collectable.LIFE_SPELL; + sariaNorth.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + sariaNorth.Town!.GetTownMap(VanillaTownMap.SARIA_TABLE)!.Collectable = Collectable.MIRROR; + sariaNorth.Town!.GetTownMap(VanillaTownMap.SARIA_TABLE)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + + mido.Town!.GetWizard()!.Collectable = Collectable.FAIRY_SPELL; + mido.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER)!.Collectable = Collectable.DOWNSTAB; + mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER)!.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + + bagu.Town!.GetTownMap(VanillaTownMap.BAGU_INDOORS)!.Collectable = Collectable.BAGUS_NOTE; + bagu.Town!.GetTownMap(VanillaTownMap.BAGU_INDOORS)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + + trophyCave.SetCollectables([Collectable.TROPHY]); + trophyCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + medicineCave.SetCollectables([Collectable.MEDICINE]); + medicineCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + heartContainerCave.SetCollectables([Collectable.HEART_CONTAINER]); + heartContainerCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + magicContainerCave.SetCollectables([Collectable.MAGIC_CONTAINER]); + magicContainerCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + pbagCave.SetCollectables([Collectable.LARGE_BAG]); + pbagCave.CollectablesAreShufflable = props.ShuffleOverworldItems && props.PbagItemShuffle; + grassTile.SetCollectables([Collectable.HEART_CONTAINER]); + grassTile.CollectablesAreShufflable = props.ShuffleOverworldItems; + + locationAtPalace1.SetCollectables(locationAtPalace1.Palace!.GetVanillaCollectables()); + locationAtPalace2.SetCollectables(locationAtPalace2.Palace!.GetVanillaCollectables()); + locationAtPalace3.SetCollectables(locationAtPalace3.Palace!.GetVanillaCollectables()); + } } diff --git a/RandomizerCore/Overworld/World.cs b/RandomizerCore/Overworld/World.cs index 853d35cc4..d9ef05d07 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -7,6 +7,7 @@ using System.Text; using NLog; using Z2Randomizer.RandomizerCore.Enemy; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; //using System.Runtime.InteropServices.WindowsRuntime; @@ -56,8 +57,6 @@ public abstract class World private const int MINIMUM_BRIDGE_LENGTH = 2; - public static readonly Requirements DEFAULT_PALACE_REQUIREMENTS = new Requirements([RequirementType.FAIRY, RequirementType.KEY]); - private static readonly Dictionary MAXIMUM_BRIDGE_LENGTH = new() { { Biome.ISLANDS, 10 }, @@ -195,20 +194,6 @@ protected void Swap(Location l1, Location l2) (l2.Xpos, l1.Xpos) = (l1.Xpos, l2.Xpos); (l2.Y, l1.Y) = (l1.Y, l2.Y); (l2.IsPassthrough, l1.IsPassthrough) = (l1.IsPassthrough, l2.IsPassthrough); - - foreach (Location child in l1.Children) - { - child.Xpos = l1.Xpos; - child.Y = l1.Y; - child.IsPassthrough = l1.IsPassthrough; - } - - foreach (Location child in l2.Children) - { - child.Xpos = l2.Xpos; - child.Y = l2.Y; - child.IsPassthrough = l2.IsPassthrough; - } } protected void RemoveLocations(ICollection locationsToRemove) @@ -307,7 +292,7 @@ public void UpdateAllReached() //TODO: Doing this off terrain type is a very bad idea for stuff like vanilla shuffle no actual terrain. if (location.TerrainType == Terrain.PALACE || location.TerrainType == Terrain.TOWN - || location.Collectables.Any(i => !i.IsInternalUse())) + || location.GetAllCollectables().Any(i => !i.IsInternalUse())) { if (!location.Reachable) { @@ -440,8 +425,7 @@ protected bool PlaceLocations(Terrain riverTerrain, bool saneCaves, Location? hi location.Y = y; location.CanShuffle = false; } - else if (location.TerrainType != Terrain.TOWN || - (location.ActualTown > 0 && (location?.ActualTown?.AppearsOnMap() ?? false))) + else if (location.Town == null) { Terrain t; do @@ -461,15 +445,6 @@ protected bool PlaceLocations(Terrain riverTerrain, bool saneCaves, Location? hi location.CanShuffle = false; } } - - if(location!.TerrainType == Terrain.TOWN) - { - foreach (Location linkedLocation in AllLocations.Where( - loc => !loc.AppearsOnMap && loc.ActualTown?.GetMasterTown() == location.ActualTown)) - { - linkedLocation.Pos = location.Pos; - } - } } return true; } @@ -758,9 +733,7 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr //Any of the bridge locations that are being placed need to be reset so their vanilla locations aren't avoided. if(placeSaria) { - foreach (Location location in AllLocations.Where(i => i.ActualTown == Town.SARIA_NORTH - || i.ActualTown == Town.SARIA_SOUTH - || i.ActualTown == Town.SARIA_TABLE)) + foreach (Location location in AllLocations.Where(i => i.Town?.Type == TownType.SARIA)) { location.Xpos = 0; location.Y = 0; @@ -1960,6 +1933,8 @@ public void ResetVisitabilityState() } } + public abstract void ResetCollectables(RandomizerProperties props); + protected bool DrawBridge(Direction direction) { return DrawBridge(RNG, map, bridge, walkableTerrains, direction); @@ -2287,6 +2262,19 @@ public void VisitCave2() } } + public void DebugVisitation() + { + StringBuilder sb = new(); + for(int y = 0; y < MapRows; y++) + { + for(int x = 0; x < MapColumns; x++) + { + sb.Append(visitation[y, x] ? 'x' : ' '); + } + sb.AppendLine(); + } + Debug.WriteLine(sb.ToString()); + } public List GetContinentConnections() { return new List() { cave1!, cave2!, bridge!, raft! }.Where(i => i != null).ToList(); @@ -3183,42 +3171,11 @@ public bool ValidateCaves() return true; } - /* - public bool PassthroughsIntersectRaftCoordinates(IEnumerable<(int, int)> raftCoordinates) - { - foreach (Location location in AllLocations.Where(i => i.PassThrough != 0)) - { - if (raftCoordinates.Any(i => location.Xpos == i.Item2 && location.YRaw == i.Item1)) - { - return true; - } - } - return false; - } - */ - - //Short term fix, right now linked locations aren't shuffled, and they are counted as reachable when - //their parent location is reachable, but the original location remains in the location list, creating - //a phantom reachability spot at the vanilla location. - //What _should_ happen is there should be a unified interface for updating locations that automicatically - //updates the coordinates, map, linked locations, etc. But that's more work than I want to do, so here's this lazy hack - public void SynchronizeLinkedLocations() - { - foreach (Location parent in AllLocations) - { - foreach (Location child in parent.Children) - { - child.Xpos = parent.Xpos; - child.Y = parent.Y; - } - } - } - public abstract void UpdateVisit(List requireables); public abstract IEnumerable RequiredLocations(bool hiddenPalace, bool hiddenKasuto); - protected abstract void SetVanillaCollectables(bool useDash); + //protected abstract void SetVanillaCollectables(bool useDash); public abstract string GenerateSpoiler(); } \ No newline at end of file diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 4c345ea36..5f94ff58b 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -1,15 +1,18 @@ -using System; +using js65; +using NLog; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; -using js65; -using NLog; +using System.Xml.Linq; using Z2Randomizer.RandomizerCore.Enemy; using Z2Randomizer.RandomizerCore.Overworld; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -156,6 +159,8 @@ public class ROM public static readonly int[] ZeldaFacePaletteAddr = { 0x4023, 0x8023, 0x14047, 0x140c9 }; public static readonly int[] ZeldaDressPaletteAddr = { 0x4024, 0x8024, 0x14048, 0x140c8 }; + public static readonly byte[] DOOR_CONNECTION_BLACKLIST = [0x00, 0xFC, 0xFD, 0xFE, 0xFF]; + public byte[] rawdata { get; } public ROM(string filename, bool expandRom = false) @@ -2357,6 +2362,62 @@ public Location LoadLocation(LocationID lid, Terrain terrain, bool? passthroughO }; } + public Town LoadTown(int startMapNum, int numMaps) + { + List townMaps = []; + Dictionary connections = []; + Dictionary doorConnections = []; + for (int currentMapNum = startMapNum; currentMapNum - startMapNum < numMaps; currentMapNum++) + { + townMaps.Add(LoadTownMap(currentMapNum, false)); + + var roomDoorConnectionsRomAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x8817 + currentMapNum * 4); + var roomDoorConnections = GetBytes(roomDoorConnectionsRomAddr, 4); + + for(int connectionNumber = 0; connectionNumber < 4; connectionNumber++) + { + byte internalMapNumber = (byte)((roomDoorConnections[connectionNumber] & 0xFC) >> 2); + if(!DOOR_CONNECTION_BLACKLIST.Contains(internalMapNumber) && !townMaps.Any(i => i.Map == internalMapNumber)) + { + townMaps.Add(LoadTownMap(internalMapNumber, true)); + } + } + } + + foreach(TownMap townMap in townMaps) + { + + var romAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x871B + townMap.Map * 4); + var connectionsRaw = GetBytes(romAddr, 4); + connections.Add(townMap.Map, connectionsRaw); + romAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x8817 + townMap.Map * 4); + var doorConnectionsRaw = GetBytes(romAddr, 4); + doorConnections.Add(townMap.Map, doorConnectionsRaw); + } + + + return new Town(townMaps, connections, doorConnections); + } + + public TownMap LoadTownMap(int currentMapNum, bool isInternalMap) + { + var romPtr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x8523 + 2 * currentMapNum); + var nesAddr = GetShort(romPtr + 1, romPtr); + var romAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, nesAddr); + var length = GetByte(romAddr); + var sideviewRaw = GetBytes(romAddr, length); + + romPtr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x85a1 + 2 * currentMapNum); + //Enemy pointer points to the ram location base enemy data is copied from + //18A0 is the offset to reach the base enemy data + nesAddr = GetShort(romPtr + 1, romPtr) + 0x18A0; + romAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, nesAddr); + length = GetByte(romAddr); + byte[] enemiesRaw = GetBytes(romAddr, length); + + return new TownMap(currentMapNum, sideviewRaw, enemiesRaw, isInternalMap); + } + public void RemoveUnusedConnectors(World world) { if (world.raft == null) @@ -2381,8 +2442,7 @@ public void RemoveUnusedConnectors(World world) } //This was refactored out of EastHyrule. The signature/timing/structure needs work. - public void UpdateHiddenPalaceSpot(Biome biome, (int, int) hiddenPalaceCoords, Location hiddenPalaceLocation, - Location townAtNewKasuto, Location spellTower, bool vanillaShuffleUsesActualTerrain) + public void UpdateHiddenPalaceSpot(Biome biome, (int, int) hiddenPalaceCoords, Location hiddenPalaceLocation, bool vanillaShuffleUsesActualTerrain) { if (!biome.UsesVanillaMap()) { @@ -2396,10 +2456,6 @@ public void UpdateHiddenPalaceSpot(Biome biome, (int, int) hiddenPalaceCoords, L Put(0x1ccc0, (byte)pos); int connection = hiddenPalaceLocation.MemAddress - 0x862F; Put(0x1df76, (byte)connection); - if (hiddenPalaceLocation == spellTower) - { - throw new Exception("Child locations shouldn't be able to be hidden spots"); - } if (vanillaShuffleUsesActualTerrain || biome != Biome.VANILLA_SHUFFLE) { Put(0x1df74, (byte)hiddenPalaceLocation.TerrainType); @@ -2508,7 +2564,7 @@ public void UpdateHiddenPalaceSpot(Biome biome, (int, int) hiddenPalaceCoords, L } - public void UpdateKasuto(Location hiddenKasutoLocation, Location townAtNewKasuto, Location spellTower, Biome biome, + public void UpdateKasuto(Location hiddenKasutoLocation, Biome biome, int baseAddr, Terrain hiddenKasutoTerrain, bool vanillaShuffleUsesActualTerrain) { Put(0x1df79, (byte)(hiddenKasutoLocation.YRaw + (hiddenKasutoLocation.IsExternalWorld ? 0x80 : 0))); @@ -2518,10 +2574,6 @@ public void UpdateKasuto(Location hiddenKasutoLocation, Location townAtNewKasuto Put(0x1ccdb, (byte)hiddenKasutoLocation.YRaw); int connection = hiddenKasutoLocation.MemAddress - baseAddr; Put(0x1df77, (byte)connection); - if (hiddenKasutoLocation == spellTower) - { - throw new Exception("Child locations shouldn't be able to be hidden spots"); - } if (vanillaShuffleUsesActualTerrain || biome != Biome.VANILLA_SHUFFLE) { //Terrain t = terrains[hiddenKasutoLocation.MemAddress]; diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index bff83eb61..b062ff65b 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -9,7 +9,7 @@ using System.Runtime.CompilerServices; using NLog; using Z2Randomizer.RandomizerCore.Flags; -using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore; diff --git a/RandomizerCore/RandomizerProperties.cs b/RandomizerCore/RandomizerProperties.cs index d947cf240..866aadc62 100644 --- a/RandomizerCore/RandomizerProperties.cs +++ b/RandomizerCore/RandomizerProperties.cs @@ -246,7 +246,7 @@ public class RandomizerProperties [Key] public int Id { get; set; } - public bool StartsWithCollectable(Collectable collectable) + public bool StartsWithCollectable(Collectable? collectable) { return collectable switch { diff --git a/RandomizerCore/RequirementType.cs b/RandomizerCore/RequirementType.cs index 1a515eb2d..4b9109a63 100644 --- a/RandomizerCore/RequirementType.cs +++ b/RandomizerCore/RequirementType.cs @@ -1,4 +1,5 @@  +using System; using System.Collections.Generic; namespace Z2Randomizer.RandomizerCore; @@ -35,6 +36,7 @@ public enum RequirementType public static class RequirementTypeExtensions { + public static readonly List ALL = [..Enum.GetValues()]; /// Mirror of Collectable.AsRequirement() public static Collectable? AsCollectable(this RequirementType requirementType) { diff --git a/RandomizerCore/Requirements.cs b/RandomizerCore/Requirements.cs index 54534f6d1..7a2a1e65e 100644 --- a/RandomizerCore/Requirements.cs +++ b/RandomizerCore/Requirements.cs @@ -6,12 +6,15 @@ using System.Text.Json; using System.Text.Json.Serialization; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore; public class Requirements { public static readonly Requirements NONE = new(); + public static readonly Requirements DEFAULT_PALACE_REQUIREMENTS = new([RequirementType.FAIRY, RequirementType.KEY]); + private static readonly Dictionary ImplicitRequirements = new() { diff --git a/RandomizerCore/RomMap.cs b/RandomizerCore/RomMap.cs index c653641fe..79f6e831f 100644 --- a/RandomizerCore/RomMap.cs +++ b/RandomizerCore/RomMap.cs @@ -134,4 +134,5 @@ class RomMap public const int PALACE_PALETTE_TABLE_ENTRANCES = 0x10480; public const int PALACE_PALETTE_TABLE_PER_PALACE = 0x13f10; public const int GP_PALETTE_TABLE_MAJOR = 0x1401e; + } diff --git a/RandomizerCore/Shuffler.cs b/RandomizerCore/Shuffler.cs index 7f70b4d3a..e545e9871 100644 --- a/RandomizerCore/Shuffler.cs +++ b/RandomizerCore/Shuffler.cs @@ -3,7 +3,7 @@ using System.Linq; using js65; using NLog; -using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore; diff --git a/RandomizerCore/Sidescroll/ByEntranceDirectionItemRoomSelectionStrategy.cs b/RandomizerCore/Sidescroll/Palace/ByEntranceDirectionItemRoomSelectionStrategy.cs similarity index 96% rename from RandomizerCore/Sidescroll/ByEntranceDirectionItemRoomSelectionStrategy.cs rename to RandomizerCore/Sidescroll/Palace/ByEntranceDirectionItemRoomSelectionStrategy.cs index 33ecc7402..f967d6dc7 100644 --- a/RandomizerCore/Sidescroll/ByEntranceDirectionItemRoomSelectionStrategy.cs +++ b/RandomizerCore/Sidescroll/Palace/ByEntranceDirectionItemRoomSelectionStrategy.cs @@ -4,7 +4,7 @@ using System.Linq; using Z2Randomizer.RandomizerCore; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; internal class ByEntranceDirectionItemRoomSelectionStrategy : ItemRoomSelectionStrategy { diff --git a/RandomizerCore/Sidescroll/ByShapeItemRoomSelectionStrategy.cs b/RandomizerCore/Sidescroll/Palace/ByShapeItemRoomSelectionStrategy.cs similarity index 99% rename from RandomizerCore/Sidescroll/ByShapeItemRoomSelectionStrategy.cs rename to RandomizerCore/Sidescroll/Palace/ByShapeItemRoomSelectionStrategy.cs index e762d58f5..72daa27e2 100644 --- a/RandomizerCore/Sidescroll/ByShapeItemRoomSelectionStrategy.cs +++ b/RandomizerCore/Sidescroll/Palace/ByShapeItemRoomSelectionStrategy.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; internal class ByShapeItemRoomSelectionStrategy : ItemRoomSelectionStrategy, IItemRoomInShapeSelectionStrategy { diff --git a/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/ChaosPalaceGenerator.cs similarity index 98% rename from RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/ChaosPalaceGenerator.cs index 57c6545ce..429f87804 100644 --- a/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/ChaosPalaceGenerator.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; internal class ChaosPalaceGenerator : PalaceGenerator { protected static readonly Logger logger = LogManager.GetCurrentClassLogger(); @@ -18,7 +18,7 @@ internal override async Task GeneratePalace(RandomizerProperties props, debug++; bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); RoomPool roomPool = new(rooms); - Palace palace = new(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); var palaceGroup = Util.AsPalaceGrouping(palaceNumber); palace.Entrance = new(roomPool.Entrances[r.Next(roomPool.Entrances.Count)]) diff --git a/RandomizerCore/Sidescroll/CoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/CoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/CoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/CoordinatePalaceGenerator.cs index 0d615b8b8..7a4e675c4 100644 --- a/RandomizerCore/Sidescroll/CoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/CoordinatePalaceGenerator.cs @@ -4,7 +4,7 @@ using System.Linq; using NLog; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public abstract class CoordinatePalaceGenerator : PalaceGenerator { diff --git a/RandomizerCore/Sidescroll/ItemRoomSelectionStrategy.cs b/RandomizerCore/Sidescroll/Palace/ItemRoomSelectionStrategy.cs similarity index 90% rename from RandomizerCore/Sidescroll/ItemRoomSelectionStrategy.cs rename to RandomizerCore/Sidescroll/Palace/ItemRoomSelectionStrategy.cs index e9cb8eeda..d54a452a9 100644 --- a/RandomizerCore/Sidescroll/ItemRoomSelectionStrategy.cs +++ b/RandomizerCore/Sidescroll/Palace/ItemRoomSelectionStrategy.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public abstract class ItemRoomSelectionStrategy { diff --git a/RandomizerCore/Sidescroll/Palace.cs b/RandomizerCore/Sidescroll/Palace/Palace.cs similarity index 96% rename from RandomizerCore/Sidescroll/Palace.cs rename to RandomizerCore/Sidescroll/Palace/Palace.cs index 26647f65b..82859220f 100644 --- a/RandomizerCore/Sidescroll/Palace.cs +++ b/RandomizerCore/Sidescroll/Palace/Palace.cs @@ -1,24 +1,25 @@ -using System; +using NLog; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using NLog; using Z2Randomizer.RandomizerCore.Enemy; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; using static Z2Randomizer.RandomizerCore.Util; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; -public partial class Palace +public partial class Palace(int number, bool palaceItemsAreShufflable) { private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private const double DROPS_CAN_LEAD_TO_BOSS_CHANCE = 0.4; //private const bool DROPS_ARE_BLOCKERS = false; private const byte OUTSIDE_ROOM_EXIT = 0b11111100; - private readonly SortedDictionary> rooms; + private readonly SortedDictionary> rooms = []; internal bool IsValid { get; set; } = false; public static readonly ReadOnlyCollection SHUFFLABLE_SMALL_ITEMS = [ @@ -32,12 +33,13 @@ public partial class Palace Collectable.ONEUP ]; - public List AllRooms { get; private set; } + public List AllRooms { get; private set; } = []; - public Room? Entrance { get; set; } - public List ItemRooms { get; set; } + public Room? Entrance { get; set; } = null; + public List ItemRooms { get; set; } = []; + public bool PalaceItemsAreShufflable { get; set; } = palaceItemsAreShufflable; public Room? BossRoom { get; set; } - public int Number { get; set; } + public int Number { get; set; } = number; internal Room? TbirdRoom { get; set; } //DEBUG @@ -45,22 +47,6 @@ public partial class Palace public PalaceGrouping PalaceGroup => Util.AsPalaceGrouping(Number) ?? throw new Exception("Palace number out of range"); - public Palace(int number) - { - Number = number; - Entrance = null; - rooms = []; - AllRooms = []; - ItemRooms = []; - } - - /* - public void UpdateRomItem(Collectable collectable, ROM ROMData) - { - ItemRoom?.UpdateRomItem(collectable, ROMData); - } - */ - public bool RequiresThunderbird() { CheckSpecialPaths(Entrance!); @@ -961,40 +947,6 @@ public void RandomizeSmallItems(Random r, bool extraKeys) } } } - - /* - public List CheckBlocks() - { - return CheckBlocksHelper([], [], Entrance!); - } - - private List CheckBlocksHelper(List c, List blockers, Room r) - { - if (c.Contains(ItemRoom!)) - { - return c; - } - c.Add(r); - if (r.Up != null && !c.Contains(r.Up)) - { - CheckBlocksHelper(c, blockers, r.Up); - } - if (r.Down != null && !c.Contains(r.Down)) - { - CheckBlocksHelper(c, blockers, r.Down); - } - if (r.Left != null && !c.Contains(r.Left)) - { - CheckBlocksHelper(c, blockers, r.Left); - } - if (r.Right != null && !c.Contains(r.Right)) - { - CheckBlocksHelper(c, blockers, r.Right); - } - return c; - } - */ - public void ResetRooms() { foreach (Room r in AllRooms) @@ -1175,8 +1127,12 @@ public bool CanReachAnItemRoom(IEnumerable requireables) return false; } - public List GetGettableItems(IEnumerable initialRequireables) + public List GetGettableItems(IEnumerable initialRequireables, bool shufflableItemsOnly) { + if(shufflableItemsOnly && !PalaceItemsAreShufflable) + { + return []; + } List requireables = []; requireables.AddRange(initialRequireables); List pendingRooms = new() { AllRooms.First(i => i.IsEntrance) }; @@ -1235,6 +1191,27 @@ public List GetGettableItems(IEnumerable initialRe return gettableItems; } + public void SetCollectables(IEnumerable collectables) + { + Debug.Assert(collectables.Count() == ItemRooms.Count); + int i = 0; + foreach (Collectable collectable in collectables) + { + ItemRooms[i++].Collectable = collectable; + } + } + + public bool ReplaceCollectable(Collectable toReplace, Collectable replacement) + { + Room? room = ItemRooms.FirstOrDefault(i => i.Collectable == toReplace); + if (room == null) + { + return false; + } + room.Collectable = replacement; + return true; + } + [Conditional("DEBUG")] public void AssertItemRoomsAreUnique(ROM rom) { @@ -1398,22 +1375,31 @@ public void ReplaceRoom(Room roomToReplace, Room newRoom) AllRooms.Add(newRoom); } + public Collectable GetVanillaCollectable() + { + return Number switch + { + 7 => Collectable.DO_NOT_USE, + _ => GetVanillaCollectables()[0] + }; + } - public static Collectable GetVanillaCollectable(int? palaceNum) + public List GetVanillaCollectables() { - Collectable[] vanillaCollectables = [ - Collectable.CANDLE, - Collectable.GLOVE, - Collectable.RAFT, - Collectable.BOOTS, - Collectable.FLUTE, - Collectable.CROSS + List[] vanillaCollectables = [ + [Collectable.CANDLE], + [Collectable.GLOVE], + [Collectable.RAFT], + [Collectable.BOOTS], + [Collectable.FLUTE], + [Collectable.CROSS], + [] ]; - if (palaceNum == null || (int)palaceNum > 6 || (int)palaceNum < 1) + if (Number > 7 || Number < 1) { throw new Exception("Invalid palace number"); } - return vanillaCollectables[(int)palaceNum - 1]; + return vanillaCollectables[Number - 1]; } public RoomExitType GetCoordinateRoomShape(int x, int y) diff --git a/RandomizerCore/Sidescroll/PalaceColors.cs b/RandomizerCore/Sidescroll/Palace/PalaceColors.cs similarity index 99% rename from RandomizerCore/Sidescroll/PalaceColors.cs rename to RandomizerCore/Sidescroll/Palace/PalaceColors.cs index 922a87286..91dc9437c 100644 --- a/RandomizerCore/Sidescroll/PalaceColors.cs +++ b/RandomizerCore/Sidescroll/Palace/PalaceColors.cs @@ -1,5 +1,4 @@ - -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; /// /// Color combinations that may be chosen for palace bricks and diff --git a/RandomizerCore/Sidescroll/PalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/PalaceGenerator.cs similarity index 98% rename from RandomizerCore/Sidescroll/PalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/PalaceGenerator.cs index 707ba44c2..153c21aab 100644 --- a/RandomizerCore/Sidescroll/PalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/PalaceGenerator.cs @@ -5,7 +5,7 @@ using System.Numerics; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public abstract class PalaceGenerator { diff --git a/RandomizerCore/Sidescroll/PalaceRooms.cs b/RandomizerCore/Sidescroll/Palace/PalaceRooms.cs similarity index 99% rename from RandomizerCore/Sidescroll/PalaceRooms.cs rename to RandomizerCore/Sidescroll/Palace/PalaceRooms.cs index 1f3b3de5c..5a30791cf 100644 --- a/RandomizerCore/Sidescroll/PalaceRooms.cs +++ b/RandomizerCore/Sidescroll/Palace/PalaceRooms.cs @@ -5,7 +5,7 @@ using System.Text.Json; using System.Text.RegularExpressions; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public partial class PalaceRooms { diff --git a/RandomizerCore/Sidescroll/Palaces.cs b/RandomizerCore/Sidescroll/Palace/Palaces.cs similarity index 99% rename from RandomizerCore/Sidescroll/Palaces.cs rename to RandomizerCore/Sidescroll/Palace/Palaces.cs index 91de75a27..54516b7c9 100644 --- a/RandomizerCore/Sidescroll/Palaces.cs +++ b/RandomizerCore/Sidescroll/Palace/Palaces.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using NLog; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public enum PalaceGrouping { @@ -156,6 +156,7 @@ public async Task> CreatePalaces(Random r, RandomizerProperties pro } } } + return palaces; } diff --git a/RandomizerCore/Sidescroll/RandomItemRoomSelectionStrategy.cs b/RandomizerCore/Sidescroll/Palace/RandomItemRoomSelectionStrategy.cs similarity index 98% rename from RandomizerCore/Sidescroll/RandomItemRoomSelectionStrategy.cs rename to RandomizerCore/Sidescroll/Palace/RandomItemRoomSelectionStrategy.cs index fd2a86d61..f3c4baa9d 100644 --- a/RandomizerCore/Sidescroll/RandomItemRoomSelectionStrategy.cs +++ b/RandomizerCore/Sidescroll/Palace/RandomItemRoomSelectionStrategy.cs @@ -3,7 +3,7 @@ using System.Linq; using YamlDotNet.Core.Tokens; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class RandomItemRoomSelectionStrategy : ItemRoomSelectionStrategy { diff --git a/RandomizerCore/Sidescroll/RandomWalkCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/RandomWalkCoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/RandomWalkCoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/RandomWalkCoordinatePalaceGenerator.cs index e5f522994..bea281eca 100644 --- a/RandomizerCore/Sidescroll/RandomWalkCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/RandomWalkCoordinatePalaceGenerator.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class RandomWalkCoordinatePalaceGenerator : ShapeFirstCoordinatePalaceGenerator { diff --git a/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/ReconstructedLoopyPalaceGenerator.cs similarity index 96% rename from RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/ReconstructedLoopyPalaceGenerator.cs index 16fc9749e..1f34987a9 100644 --- a/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/ReconstructedLoopyPalaceGenerator.cs @@ -4,7 +4,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class ReconstructedLoopyPalaceGenerator(CancellationToken ct) : ReconstructedPalaceGenerator(ct) { diff --git a/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/ReconstructedPalaceGenerator.cs similarity index 98% rename from RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/ReconstructedPalaceGenerator.cs index 4ad33d683..0880985c9 100644 --- a/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/ReconstructedPalaceGenerator.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class ReconstructedPalaceGenerator(CancellationToken ct) : PalaceGenerator { @@ -21,7 +21,7 @@ internal override async Task GeneratePalace(RandomizerProperties props, debug++; bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); // var palaceGroup = Util.AsPalaceGrouping(palaceNumber); - Palace palace = new(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); do // while (tries >= PALACE_SHUFFLE_ATTEMPT_LIMIT); { await Task.Yield(); @@ -37,7 +37,7 @@ internal override async Task GeneratePalace(RandomizerProperties props, //No longer a loop since the room pool can't (and would need to be) regenerated after each attempt. //Now we just fail and restart if we can't place enough rooms - palace = new(palaceNumber); + palace = new(palaceNumber, props.ShufflePalaceItems); palace.Entrance = new(roomPool.Entrances[r.Next(roomPool.Entrances.Count)]) { IsRoot = true, diff --git a/RandomizerCore/Sidescroll/Room.cs b/RandomizerCore/Sidescroll/Palace/Room.cs similarity index 99% rename from RandomizerCore/Sidescroll/Room.cs rename to RandomizerCore/Sidescroll/Palace/Room.cs index dd6fce0ec..58603c808 100644 --- a/RandomizerCore/Sidescroll/Room.cs +++ b/RandomizerCore/Sidescroll/Palace/Room.cs @@ -8,7 +8,7 @@ using js65; using NLog; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public record struct Coord(int X, int Y) : IComparable { diff --git a/RandomizerCore/Sidescroll/RoomExitType.cs b/RandomizerCore/Sidescroll/Palace/RoomExitType.cs similarity index 98% rename from RandomizerCore/Sidescroll/RoomExitType.cs rename to RandomizerCore/Sidescroll/Palace/RoomExitType.cs index 5abc96c52..93718562d 100644 --- a/RandomizerCore/Sidescroll/RoomExitType.cs +++ b/RandomizerCore/Sidescroll/Palace/RoomExitType.cs @@ -1,7 +1,7 @@ using System; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public enum RoomExitType { diff --git a/RandomizerCore/Sidescroll/Palace/RoomGroup.cs b/RandomizerCore/Sidescroll/Palace/RoomGroup.cs new file mode 100644 index 000000000..44769229c --- /dev/null +++ b/RandomizerCore/Sidescroll/Palace/RoomGroup.cs @@ -0,0 +1,6 @@ +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; + +public enum RoomGroup +{ + VANILLA, V4_0, V5_0, STUBS +} diff --git a/RandomizerCore/Sidescroll/RoomPool.cs b/RandomizerCore/Sidescroll/Palace/RoomPool.cs similarity index 99% rename from RandomizerCore/Sidescroll/RoomPool.cs rename to RandomizerCore/Sidescroll/Palace/RoomPool.cs index 2bddd9d39..e1f50ea2f 100644 --- a/RandomizerCore/Sidescroll/RoomPool.cs +++ b/RandomizerCore/Sidescroll/Palace/RoomPool.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class RoomPool { diff --git a/RandomizerCore/Sidescroll/SegmentConnectionType.cs b/RandomizerCore/Sidescroll/Palace/SegmentConnectionType.cs similarity index 95% rename from RandomizerCore/Sidescroll/SegmentConnectionType.cs rename to RandomizerCore/Sidescroll/Palace/SegmentConnectionType.cs index af0f9c4f1..2f6915966 100644 --- a/RandomizerCore/Sidescroll/SegmentConnectionType.cs +++ b/RandomizerCore/Sidescroll/Palace/SegmentConnectionType.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public enum SegmentConnectionType { diff --git a/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/SequentialPlacementCoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/SequentialPlacementCoordinatePalaceGenerator.cs index 2a4ee2304..7bd8008b8 100644 --- a/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/SequentialPlacementCoordinatePalaceGenerator.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class SequentialPlacementCoordinatePalaceGenerator : CoordinatePalaceGenerator { @@ -33,7 +33,7 @@ internal override async Task GeneratePalace(RandomizerProperties props, debug++; bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); - Palace palace = new(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); openCoords = new(); Dictionary> roomsByExitType; roomPool = new(rooms); diff --git a/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/ShapeFirstCoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/ShapeFirstCoordinatePalaceGenerator.cs index 28c1d7d43..2a9345a03 100644 --- a/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/ShapeFirstCoordinatePalaceGenerator.cs @@ -8,7 +8,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public abstract class ShapeFirstCoordinatePalaceGenerator() : CoordinatePalaceGenerator() { @@ -16,7 +16,7 @@ public abstract class ShapeFirstCoordinatePalaceGenerator() : CoordinatePalaceGe internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber) { bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); - Palace palace = new(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); RoomPool roomPool = new(rooms); var itemRoomSelector = GetItemRoomSelectionStrategy(); // var palaceGroup = Util.AsPalaceGrouping(palaceNumber); diff --git a/RandomizerCore/Sidescroll/TowerCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/TowerCoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/TowerCoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/TowerCoordinatePalaceGenerator.cs index 61affa3b4..528ada4a4 100644 --- a/RandomizerCore/Sidescroll/TowerCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/TowerCoordinatePalaceGenerator.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class TowerCoordinatePalaceGenerator : ShapeFirstCoordinatePalaceGenerator { diff --git a/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/VanillaPalaceGenerator.cs similarity index 94% rename from RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/VanillaPalaceGenerator.cs index 113f9b6e1..e4d393293 100644 --- a/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/VanillaPalaceGenerator.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using NLog; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class VanillaPalaceGenerator() : PalaceGenerator { @@ -18,14 +18,10 @@ internal override async Task GeneratePalace(RandomizerProperties props, { throw new Exception("Invalid vanilla palace room pool"); } - Palace palace = new(palaceNumber); - - // var palaceGroup = Util.AsPalaceGrouping(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); palace.Entrance = new(roomPool.Entrances.First()); - // palace.Entrance.PalaceGroup = palaceGroup; palace.BossRoom = new(roomPool.BossRooms.First()); - // palace.BossRoom.PalaceGroup = palaceGroup; palace.AllRooms.Add(palace.Entrance); palace.ItemRooms = []; diff --git a/RandomizerCore/Sidescroll/VanillaRoomPool.cs b/RandomizerCore/Sidescroll/Palace/VanillaRoomPool.cs similarity index 97% rename from RandomizerCore/Sidescroll/VanillaRoomPool.cs rename to RandomizerCore/Sidescroll/Palace/VanillaRoomPool.cs index d7ae45881..9aee72ea3 100644 --- a/RandomizerCore/Sidescroll/VanillaRoomPool.cs +++ b/RandomizerCore/Sidescroll/Palace/VanillaRoomPool.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; internal class VanillaRoomPool : RoomPool { diff --git a/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/VanillaShufflePalaceGenerator.cs similarity index 95% rename from RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/VanillaShufflePalaceGenerator.cs index 6e192bc1e..bd451f02c 100644 --- a/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/VanillaShufflePalaceGenerator.cs @@ -1,7 +1,7 @@ using System; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class VanillaShufflePalaceGenerator() : VanillaPalaceGenerator() { diff --git a/RandomizerCore/Sidescroll/VanillaWeightedPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/VanillaWeightedPalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/VanillaWeightedPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/VanillaWeightedPalaceGenerator.cs index 59f253e20..2f7240269 100644 --- a/RandomizerCore/Sidescroll/VanillaWeightedPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/VanillaWeightedPalaceGenerator.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class VanillaWeightedPalaceGenerator : RandomWalkCoordinatePalaceGenerator { diff --git a/RandomizerCore/Sidescroll/RoomGroup.cs b/RandomizerCore/Sidescroll/RoomGroup.cs deleted file mode 100644 index d45f44752..000000000 --- a/RandomizerCore/Sidescroll/RoomGroup.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Z2Randomizer.RandomizerCore.Sidescroll; - -public enum RoomGroup -{ - VANILLA, V4_0, V5_0, STUBS -} diff --git a/RandomizerCore/Sidescroll/SideviewEnums.cs b/RandomizerCore/Sidescroll/SideviewEnums.cs index fd22bb8e9..a4c5e1a1d 100644 --- a/RandomizerCore/Sidescroll/SideviewEnums.cs +++ b/RandomizerCore/Sidescroll/SideviewEnums.cs @@ -177,6 +177,39 @@ public enum GreatPalaceObject PILLAR = 0xF0 } +public enum TownObject +{ + CLOSED_DOOR = 0x00, + OPEN_DOOR1 = 0x01, + OPEN_DOOR2 = 0x02, + OPEN_DOOR3 = 0x03, + STONEHENGE = 0x04, //??? + CHAIR = 0x05, + LARGE_CLOUD = 0x06, + SMALL_CLOUD_07 = 0x07, + TABLE = 0x08, + BENCH = 0x09, + CROSS = 0x0A, + CHIMNEY_ENTRANCE = 0x0B, + CHIMNEY_NON_ENTRANCE = 0x0C, + CHURCH_CROSS = 0x0D, + SMALL_CLOUD_OE = 0x0E, + COLLECTABLE = 0x0F, + CROSS_HASH_ROOF = 0x20, + GREY_ROOF = 0x30, + GREEN_ROOF = 0x40, + BRICK_BUILDING = 0x50, + WHITE_BRICK_BUILDING = 0x60, + GRASS_COLOR_BUILDING = 0x70, + WHITE_WALL = 0x80, + LOG_BUILDING = 0x90, + SQUARE_WINDOW = 0xA0, + GRAY_WINDOW = 0xB0, + BROWN_WINDOWS = 0xC0, + UNDERGROUND_BRICKS = 0xD0, + PILLAR = 0xE0, +} + public static class ForestObjectExtensions { public static int Width(SideviewMapCommand command) diff --git a/RandomizerCore/Sidescroll/Town/Town.cs b/RandomizerCore/Sidescroll/Town/Town.cs new file mode 100644 index 000000000..262898cbf --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/Town.cs @@ -0,0 +1,213 @@ +using DynamicData; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO.MemoryMappedFiles; +using System.Linq; +using System.Text; +using Z2Randomizer.RandomizerCore.Enemy; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +public class Town +{ + + public string Name { get; set; } = ""; + public TownType? Type { get; set; } + + public List TownMaps { get; } = []; + + //When custom towns happen, make this real + //public Town(string townJson) + + public Town(List townMaps, Dictionary connections, Dictionary doorConnections) + { + Debug.Assert(connections.Count == doorConnections.Count); + TownMaps = townMaps; + foreach (TownMap townMap in townMaps.OrderBy(i => i.Map).Where(i => !i.IsInternalLocation || (i.Map == 57 && Type == TownType.DARUNIA))) + { + if(connections.TryGetValue(townMap.Map, out var connectionsRaw)) + { + byte connectionMap = (byte)((connectionsRaw[0] & 0xFC) >> 2); + townMap.Left = townMaps.FirstOrDefault(i => i.Map == connectionMap); + connectionMap = (byte)((connectionsRaw[3] & 0xFC) >> 2); + townMap.Right = townMaps.FirstOrDefault(i => i.Map == connectionMap); + } + + if (doorConnections.TryGetValue(townMap.Map, out var doorConnectionsRaw)) + { + byte connectionMap = (byte)((doorConnectionsRaw[0] & 0xFC) >> 2); + townMap.Door0 = townMaps.FirstOrDefault(i => i.Map == connectionMap); + connectionMap = (byte)((doorConnectionsRaw[1] & 0xFC) >> 2); + townMap.Door1 = townMaps.FirstOrDefault(i => i.Map == connectionMap); + connectionMap = (byte)((doorConnectionsRaw[2] & 0xFC) >> 2); + townMap.Door2 = townMaps.FirstOrDefault(i => i.Map == connectionMap); + connectionMap = (byte)((doorConnectionsRaw[3] & 0xFC) >> 2); + townMap.Door3 = townMaps.FirstOrDefault(i => i.Map == connectionMap); + } + } + } + + //Determine what items you can get from this town with these reqireables from this entrance direction + public List GetGettableItems(Direction entranceDirection, List requireables, bool shufflableItemsOnly = false) + { + TownMap startMap = entranceDirection switch + { + Direction.WEST => TownMaps.First(i => i.Left == null), + Direction.EAST => TownMaps.First(i => i.Right == null), + _ => throw new ArgumentException("Invalid entrance direction. Towns can only enter from the west/east") + }; + + List collectables = []; + + List checkedMaps = []; + List mapsToCheck = [startMap]; + TownMap currentMap; + do + { + currentMap = mapsToCheck[0]; + if (currentMap.AccessRequirements.AreSatisfiedBy(requireables)) + { + if (currentMap.Door0 != null && !checkedMaps.Contains(currentMap.Door0) && !mapsToCheck.Contains(currentMap.Door0)) + { + mapsToCheck.Add(currentMap.Door0); + } + if (currentMap.Door1 != null && !checkedMaps.Contains(currentMap.Door1) && !mapsToCheck.Contains(currentMap.Door1)) + { + mapsToCheck.Add(currentMap.Door1); + } + if (currentMap.Door2 != null && !checkedMaps.Contains(currentMap.Door2) && !mapsToCheck.Contains(currentMap.Door2)) + { + mapsToCheck.Add(currentMap.Door2); + } + if (currentMap.Door3 != null && !checkedMaps.Contains(currentMap.Door3) && !mapsToCheck.Contains(currentMap.Door3)) + { + mapsToCheck.Add(currentMap.Door3); + } + if (entranceDirection == Direction.WEST + //This is a hacky fix for the darunia upstab guy intermediary room, which is the only vanilla room + //after shortenWizards that connects to another internal room. Normal internal rooms specifically need + //to not consider their connections because there's often multiple rooms in the same map, + //but this specific one does so it's special cased here. This will need to be fixed for custom towns. + && (!currentMap.IsInternalLocation || (currentMap.Map == 57 && Type == TownType.DARUNIA)) + && currentMap.Right != null + && !checkedMaps.Contains(currentMap.Right) + && !mapsToCheck.Contains(currentMap.Right)) + { + mapsToCheck.Add(currentMap.Right); + } + if (entranceDirection == Direction.EAST + && !currentMap.IsInternalLocation + && currentMap.Left != null + && !checkedMaps.Contains(currentMap.Left) + && !mapsToCheck.Contains(currentMap.Left)) + { + mapsToCheck.Add(currentMap.Left); + } + if (currentMap.Collectable != null && (currentMap.CollectableIsShufflable || !shufflableItemsOnly)) + { + collectables.Add((Collectable)currentMap.Collectable!); + } + } + checkedMaps.Add(currentMap); + mapsToCheck.Remove(currentMap); + } + while (mapsToCheck.Count > 0); + + return collectables; + } + + public bool CanBeTraversed(List requireables) + { + TownMap leftMap = TownMaps.First(i => i.Left == null && !i.IsInternalLocation); + + //This currently only checks left-right because all towns are either bi-directionally traversable or oneway deadending right (new kasuto) + //This means it will perform weirdly if you somehow create a monodirectional town entered from the right that you also + //for some reason set as a connection. Don't do that. + List coveredMaps = []; + TownMap currentMap = leftMap; + while(coveredMaps.Count < 63) + { + if(coveredMaps.Contains(currentMap)) + { + throw new Exception($"Loop detected in town connection routing. Check your town connections for town {Name}."); + } + if(!currentMap.AccessRequirements.AreSatisfiedBy(requireables)) + { + return false; + } + if (currentMap.Right == null) + { + return false; + } + if(currentMap.RightExitIsOutside) + { + return true; + } + coveredMaps.Add(currentMap); + currentMap = currentMap.Right; + } + throw new ImpossibleException("Too many non-looping maps in town traversal check"); + } + + public void SetCollectables(IEnumerable collectables) + { + List collectableMaps = TownMaps.Where(i => i.Collectable != null).ToList(); + Debug.Assert(collectables.Count() == collectableMaps.Count); + int i = 0; + foreach(Collectable collectable in collectables) + { + collectableMaps[i++].Collectable = collectable; + } + } + + public bool ReplaceCollectable(Collectable toReplace, Collectable replacement) + { + TownMap? map = TownMaps.FirstOrDefault(i => i.Collectable == toReplace); + if(map == null) + { + return false; + } + map.Collectable = replacement; + return true; + } + + public TownMap? GetWizard() + { + if(Type == null) return null; + + VanillaTownMap map = Type switch + { + TownType.RAURU => VanillaTownMap.RAURU_WIZARD, + TownType.RUTO => VanillaTownMap.RUTO_WIZARD, + TownType.SARIA => VanillaTownMap.SARIA_WIZARD, + TownType.MIDO => VanillaTownMap.MIDO_WIZARD, + TownType.NABOORU => VanillaTownMap.NABOORU_WIZARD, + TownType.DARUNIA => VanillaTownMap.DARUNIA_WIZARD, + TownType.NEW_KASUTO => VanillaTownMap.NEW_KASUTO_WIZARD, + TownType.OLD_KASUTO => VanillaTownMap.OLD_KASUTO_WIZARD, + _ => throw new ImpossibleException("Unrecognized town type in GetWizard()") + }; + return GetTownMap(map); + } + + public TownMap? GetTownMap(int mapNumber) + { + return TownMaps.FirstOrDefault(i => i.Map == mapNumber); + } + + public TownMap? GetTownMap(VanillaTownMap map) + { + return GetTownMap((int)map); + } + + public bool HasShufflableItems() + { + return TownMaps.Any(i => i.CollectableIsShufflable); + } + + public string GenerateSpoiler() + { + return string.Join("\n", TownMaps.Where(i => i.Collectable != null).Select(i => $"\t{i.Name} : {i.Collectable?.EnglishText() ?? ""}")); + } +} diff --git a/RandomizerCore/Sidescroll/Town/TownMap.cs b/RandomizerCore/Sidescroll/Town/TownMap.cs new file mode 100644 index 000000000..f87d5ed84 --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/TownMap.cs @@ -0,0 +1,49 @@ +using System.Diagnostics; +using Z2Randomizer.RandomizerCore.Enemy; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +[DebuggerDisplay("{" + nameof(DebugString) + "(),nq}")] +public class TownMap +{ + public string Name { get; set; } = ""; + public int Map { get; set; } + public bool IsInternalLocation { get; } + public SideviewEditable Sideview { get; } + + public Collectable? Collectable { get; set; } + public bool CollectableIsShufflable { get; set; } + public TownMap? Left { get; set; } + public TownMap? Right { get; set; } + public TownMap? Door0 { get; set; } + public TownMap? Door1 { get; set; } + public TownMap? Door2 { get; set; } + public TownMap? Door3 { get; set; } + + //WTF should the script look like + + //For now, all screens that have collectables don't have separate requirements between access and collection. + //This may change in the future, which will require a bit of refactoring + public Requirements AccessRequirements { get; set; } = Requirements.NONE; + public EnemiesEditable Enemies { get; } + + //Some way to store what the Left/Right exit connection should be if it connects to the overworld. + //I'm fairly certain FC vs FD matters for saria, but I need to test it, if it doesn't matter it can just always be FC + //For now i'm assuming it doesn't matter and any null left/right connection is an external connection FC + //We can just test it when we have writing working and modify if needed + public bool LeftExitIsOutside { get; set; } + public bool RightExitIsOutside { get; set; } + + public TownMap(int mapNumber, byte[] sideviewsRaw, byte[] enemiesRaw, bool isInternalLocation) + { + Sideview = new(sideviewsRaw); + Enemies = new(enemiesRaw); + Map = mapNumber; + IsInternalLocation = isInternalLocation; + } + + public string DebugString() + { + return $"{Map} - {Name}"; + } +} \ No newline at end of file diff --git a/RandomizerCore/Sidescroll/Town/TownType.cs b/RandomizerCore/Sidescroll/Town/TownType.cs new file mode 100644 index 000000000..6233964c4 --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/TownType.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +public enum TownType +{ + INVALID = 0, + RAURU = 1, + RUTO = 2, + SARIA = 3, + MIDO = 4, + NABOORU = 5, + DARUNIA = 6, + NEW_KASUTO = 7, + OLD_KASUTO = 8, + BAGU = 9, +} + +public static class TownExtensions +{ + public const int SPELL_GET_START_ADDRESS = 0x17AF7; + public static int VanillaTownOrder(this TownType town) + { + return town switch + { + TownType.INVALID => 99, + TownType.RAURU => 1, + TownType.RUTO => 2, + TownType.SARIA => 3, + TownType.MIDO => 4, + TownType.NABOORU => 5, + TownType.DARUNIA => 6, + TownType.NEW_KASUTO => 7, + TownType.OLD_KASUTO => 8, + _ => 0 + }; + } + + public static string HintName(this TownType town) + { + return town switch + { + TownType.RAURU => "RAURU", + TownType.RUTO => "RUTO", + TownType.SARIA => "SARIA", + TownType.MIDO => "MIDO", + TownType.NABOORU => "NABOORU", + TownType.DARUNIA => "DARUNIA", + TownType.NEW_KASUTO => "NEW KASUTO", + TownType.OLD_KASUTO => "OLD KASUTO", + _ => "UNKNOWN" + }; + } +} + diff --git a/RandomizerCore/Sidescroll/Town/Towns.cs b/RandomizerCore/Sidescroll/Town/Towns.cs new file mode 100644 index 000000000..8f2721a7d --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/Towns.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +public class Towns +{ + + //There's almost certainly way cleaner way to do this but this works for now. + public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableContainerRequirements) + { + Town town; + TownMap map; + switch(townType) + { + case TownType.RAURU: + town = rom.LoadTown(1, 2); + town.Type = TownType.RAURU; + town.Name = "Rauru"; + map = town.GetTownMap(VanillaTownMap.RAURU_LEFT)!; + map.Name = "Rauru Left"; + map = town.GetTownMap(VanillaTownMap.RAURU_RIGHT)!; + map.Name = "Rauru Right"; + map = town.GetTownMap(VanillaTownMap.RAURU_WIZARD)!; + map.Name = "Rauru Wizard"; + map.Collectable = Collectable.SHIELD_SPELL; + map.AccessRequirements = disableContainerRequirements ? Requirements.NONE : new Requirements([RequirementType.ONE_CONTAINER]); + break; + case TownType.RUTO: + town = rom.LoadTown(4, 2); + town.Type = TownType.RUTO; + town.Name = "Ruto"; + map = town.GetTownMap(VanillaTownMap.RUTO_LEFT)!; + map.Name = "Ruto Left"; + map = town.GetTownMap(VanillaTownMap.RUTO_RIGHT)!; + map.Name = "Ruto Right"; + map = town.GetTownMap(VanillaTownMap.RUTO_WIZARD)!; + map.Name = "Ruto Wizard"; + map.Collectable = Collectable.JUMP_SPELL; + map.AccessRequirements = disableContainerRequirements ? new Requirements([RequirementType.TROPHY]) + : new Requirements([], [[RequirementType.TROPHY, RequirementType.TWO_CONTAINERS]]); + break; + case TownType.SARIA: + town = rom.LoadTown(6, 3); + town.Type = TownType.SARIA; + town.Name = "Saria"; + map = town.GetTownMap(VanillaTownMap.SARIA_LEFT)!; + map.Name = "Saria Moat"; + map.AccessRequirements = new Requirements([RequirementType.FAIRY, RequirementType.BAGU_LETTER], [[RequirementType.JUMP, RequirementType.DASH]]); + map = town.GetTownMap(VanillaTownMap.SARIA_MID)!; + map.Name = "Saria Middle"; + map = town.GetTownMap(VanillaTownMap.SARIA_RIGHT)!; + map.Name = "Saria Right"; + map = town.GetTownMap(VanillaTownMap.SARIA_WIZARD)!; + map.Name = "Saria Wizard"; + map.Collectable = Collectable.LIFE_SPELL; + map.AccessRequirements = disableContainerRequirements ? + new Requirements([RequirementType.MIRROR]) : new Requirements([], [[RequirementType.MIRROR, RequirementType.THREE_CONTAINERS]]); + map = town.GetTownMap(VanillaTownMap.SARIA_TABLE)!; + map.Name = "Saria Table"; + map.Collectable = Collectable.MIRROR; + break; + case TownType.MIDO: + town = rom.LoadTown(9, 3); + town.Type = TownType.MIDO; + town.Name = "Mido"; + map = town.GetTownMap(VanillaTownMap.MIDO_LEFT)!; + map.Name = "Mido Left"; + map = town.GetTownMap(VanillaTownMap.MIDO_MIDDLE)!; + map.Name = "Mido Middle"; + map = town.GetTownMap(VanillaTownMap.MIDO_RIGHT)!; + map.Name = "Mido Right"; + map = town.GetTownMap(VanillaTownMap.MIDO_WIZARD)!; + map.Name = "Mido Wizard"; + map.Collectable = Collectable.FAIRY_SPELL; + map.AccessRequirements = disableContainerRequirements ? new Requirements([RequirementType.MEDICINE]) + : new Requirements([], [[RequirementType.MEDICINE, RequirementType.FOUR_CONTAINERS]]); + map = town.GetTownMap(VanillaTownMap.MIDO_TRAINER)!; + map.Name = "Downstab Guy"; + map.Collectable = Collectable.DOWNSTAB; + map.AccessRequirements = new Requirements([RequirementType.JUMP, RequirementType.FAIRY]); + break; + case TownType.NABOORU: + town = rom.LoadTown(12, 3); + town.Type = TownType.NABOORU; + town.Name = "Nabooru"; + map = town.GetTownMap(VanillaTownMap.NABOORU_LEFT)!; + map.Name = "Nabooru Left"; + map = town.GetTownMap(VanillaTownMap.NABOORU_MID)!; + map.Name = "Nabooru Fountain"; + map.Collectable = Collectable.WATER; + map = town.GetTownMap(VanillaTownMap.NABOORU_RIGHT)!; + map.Name = "Nabooru Right"; + map = town.GetTownMap(VanillaTownMap.NABOORU_WIZARD)!; + map.Name = "Nabooru Wizard"; + map.Collectable = Collectable.FIRE_SPELL; + map.AccessRequirements = disableContainerRequirements ? new Requirements([RequirementType.WATER]) + : new Requirements([], [[RequirementType.WATER, RequirementType.FIVE_CONTAINERS]]); + break; + case TownType.DARUNIA: + town = rom.LoadTown(15, 3); + town.Type = TownType.DARUNIA; + town.Name = "Darunia"; + map = town.GetTownMap(VanillaTownMap.DARUNIA_LEFT)!; + map.Name = "Darunia Left"; + map = town.GetTownMap(VanillaTownMap.DARUNIA_MID)!; + map.Name = "Darunia Middle"; + map = town.GetTownMap(VanillaTownMap.DARUNIA_RIGHT)!; + map.Name = "Darunia Right"; + map = town.GetTownMap(VanillaTownMap.DARUNIA_WIZARD)!; + map.Name = "Darunia Wizard"; + map.Collectable = Collectable.REFLECT_SPELL; + map.AccessRequirements = disableContainerRequirements ? new Requirements([RequirementType.CHILD]) + : new Requirements([], [[RequirementType.CHILD, RequirementType.SIX_CONTAINERS]]); + //Upstab guy is the only map in wizard-shortened vanilla where an internal map connects to another internal map + //so it didn't get loaded by LoadTown, so load/connect it here. + map = rom.LoadTownMap((int)VanillaTownMap.DARUNIA_TRAINER, true); + town.TownMaps.Add(map); + map.Left = town.GetTownMap(VanillaTownMap.DARUNIA_MID); + map.Name = "Upstab Guy"; + map.Collectable = Collectable.UPSTAB; + town.GetTownMap(VanillaTownMap.DARUNIA_UPSTAB_HOUSE)!.Right = map; + map.AccessRequirements = new Requirements([RequirementType.JUMP, RequirementType.FAIRY]); + + break; + case TownType.NEW_KASUTO: + town = rom.LoadTown(18, 3); + town.Type = TownType.NEW_KASUTO; + town.Name = "New Kasuto"; + map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_LEFT)!; + map.Name = "New Kasuto Left"; + map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_MID)!; + map.Name = "New Kasuto Middle"; + map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_RIGHT)!; + map.Name = "New Kasuto Right"; + map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_WIZARD)!; + map.Name = "New Kasuto Wizard"; + map.Collectable = Collectable.SPELL_SPELL; + map.AccessRequirements = disableContainerRequirements ? Requirements.NONE : new Requirements([RequirementType.SEVEN_CONTAINERS]); + map = town.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!; + map.Name = "Granny's Basement"; + map.AccessRequirements = new Requirements([RequirementType.SEVEN_CONTAINERS]); + map = town.GetTownMap(VanillaTownMap.SPELL_TOWER_INTERIOR)!; + map.Name = "Spell Tower" + + ""; + map.AccessRequirements = new Requirements([RequirementType.SPELL]); + break; + case TownType.OLD_KASUTO: + town = rom.LoadTown(21, 3); + town.Type = TownType.OLD_KASUTO; + town.Name = "Old Kasuto"; + map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_LEFT)!; + map.Name = "Old Kasuto Left"; + map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_MID)!; + map.Name = "Old Kasuto Left"; + map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_RIGHT)!; + map.Name = "Old Kasuto Right"; + map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_WIZARD)!; + map.Name = "Old Kasuto Wizard"; + map.Collectable = Collectable.THUNDER_SPELL; + map.AccessRequirements = disableContainerRequirements ? Requirements.NONE + : new Requirements([RequirementType.EIGHT_CONTAINERS]); + break; + case TownType.BAGU: + town = rom.LoadTown(24, 1); + town.Type = TownType.BAGU; + town.Name = "Bagu's House"; + map = town.GetTownMap(VanillaTownMap.BAGU_EXTERNAL)!; + map.Name = "Bagu Exterior"; + map = town.GetTownMap(VanillaTownMap.BAGU_INDOORS)!; + map.Name = "Bagu Interior"; + map.Collectable = Collectable.BAGUS_NOTE; + break; + case TownType.INVALID: + throw new Exception($"Unrecognized vanilla town: {townType.HintName()}"); + default: + throw new Exception($"Unrecognized vanilla town: {townType.HintName()}"); + } + return town; + } +} diff --git a/RandomizerCore/Sidescroll/Town/VanillaTownMap.cs b/RandomizerCore/Sidescroll/Town/VanillaTownMap.cs new file mode 100644 index 000000000..f600b137b --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/VanillaTownMap.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +//Map indexes from the towns bank +public enum VanillaTownMap +{ + GLITCH_TOWN = 0, + RAURU_LEFT = 1, + RAURU_RIGHT = 2, + GLITCH_RUTO = 3, + RUTO_LEFT = 4, + RUTO_RIGHT = 5, + SARIA_LEFT = 6, + SARIA_MID = 7, + SARIA_RIGHT = 8, + MIDO_LEFT = 9, + MIDO_MIDDLE = 10, + MIDO_RIGHT = 11, + NABOORU_LEFT = 12, + NABOORU_MID = 13, + NABOORU_RIGHT = 14, + DARUNIA_LEFT = 15, + DARUNIA_MID = 16, + DARUNIA_RIGHT = 17, + NEW_KASUTO_LEFT = 18, + NEW_KASUTO_MID = 19, + NEW_KASUTO_RIGHT = 20, + OLD_KASUTO_LEFT = 21, + OLD_KASUTO_MID = 22, + OLD_KASUTO_RIGHT = 23, + BAGU_EXTERNAL = 24, + WIZARD_HOUSE_UPSTAIRS = 26, + SARIA_TABLE = 30, + BAGU_INDOORS = 31, + RAURU_WIZARD = 36, + RUTO_WIZARD = 37, + SARIA_WIZARD = 38, + MIDO_WIZARD = 39, + NABOORU_WIZARD = 40, + DARUNIA_WIZARD = 41, + NEW_KASUTO_WIZARD = 42, + OLD_KASUTO_WIZARD = 43, + MIDO_TRAINER = 44, + DARUNIA_TRAINER = 45, + GRANNYS_BASEMENT = 46, + SPELL_TOWER_INTERIOR = 47, + DARUNIA_UPSTAB_HOUSE = 57 +} diff --git a/RandomizerCore/Text.cs b/RandomizerCore/Text.cs index fb50e84b9..9353fe4fa 100644 --- a/RandomizerCore/Text.cs +++ b/RandomizerCore/Text.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using Z2Randomizer.RandomizerCore.Overworld; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -79,55 +80,36 @@ public bool ValidateDialogText() return true; } - public static Text GenerateHelpfulHint(List allLocations, Location location, Collectable collectable, bool useTownSpecificHints) + public static Text GenerateHelpfulHint(Location location, Collectable collectable, bool useTownSpecificHints) { string? hint = null; - if (location.PalaceNumber == 1) + if (location.Palace?.Number == 1) { hint = "horsehead$neighs$with the$%%"; } - else if (location.PalaceNumber == 2) + else if (location.Palace?.Number == 2) { hint = "helmethead$guards the$%%"; } - else if (location.PalaceNumber == 3) + else if (location.Palace?.Number == 3) { hint = "rebonack$rides$with the$%%"; } - else if (location.PalaceNumber == 4) + else if (location.Palace?.Number == 4) { hint = "carock$disappears$with the$%%"; } - else if (location.PalaceNumber == 5) + else if (location.Palace?.Number == 5) { hint = "gooma sits$on the$%%"; } - else if (location.PalaceNumber == 6) + else if (location.Palace?.Number == 6) { hint = "barba$slithers$with the$%%"; } - else if (useTownSpecificHints) + else if (useTownSpecificHints && location.Town != null) { - //For now bagu is not a town for hints. Could change in the future. - if (location.ActualTown != null && location.ActualTown != Town.BAGU) - { - hint = $"{((Town)location.ActualTown).HintName()}$has the$%%"; - } - //Saria table - if(allLocations.First(i => i.ActualTown == Town.SARIA_NORTH).Children.Contains(location)) - { - hint = $"{Town.SARIA_NORTH.HintName()}$has the$%%"; - } - //Nabooru fountain - if (allLocations.First(i => i.ActualTown == Town.NABOORU).Children.Contains(location)) - { - hint = $"{Town.NABOORU.HintName()}$has the$%%"; - } - //Spell Tower / Granny's Basement - if (allLocations.First(i => i.ActualTown == Town.NEW_KASUTO).Children.Contains(location)) - { - hint = $"{Town.NEW_KASUTO.HintName()}$has the$%%"; - } + hint = $"{((TownType)location.Town.Type!).HintName()}$has the$%%"; } if(hint == null) { diff --git a/RandomizerCore/Town.cs b/RandomizerCore/Town.cs deleted file mode 100644 index 2ba6c9621..000000000 --- a/RandomizerCore/Town.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; - -namespace Z2Randomizer.RandomizerCore; - -public enum Town -{ - INVALID = 0, - RAURU = 1, - RUTO = 2, - SARIA_NORTH = 3, - SARIA_SOUTH = 4, - MIDO_WEST = 5, - MIDO_CHURCH = 6, - NABOORU = 7, - DARUNIA_WEST = 8, - DARUNIA_ROOF = 9, - NEW_KASUTO = 10, - OLD_KASUTO = 11, - SARIA_TABLE = 12, - BAGU = 13, - NABOORU_FOUNTAIN = 14, -} - -public static class TownExtensions -{ - public const int SPELL_GET_START_ADDRESS = 0x17AF7; - public static int VanillaTownOrder(this Town town) - { - return town switch - { - Town.INVALID => 99, - Town.RAURU => 1, - Town.RUTO => 2, - Town.SARIA_NORTH => 3, - Town.MIDO_WEST => 4, - Town.NABOORU => 5, - Town.DARUNIA_WEST => 6, - Town.NEW_KASUTO => 7, - Town.OLD_KASUTO => 8, - _ => 0 - }; - } - - public static bool IsWizardTown(this Town town) - { - return town switch - { - Town.RAURU => true, - Town.RUTO => true, - Town.SARIA_NORTH => true, - Town.MIDO_WEST => true, - Town.NABOORU => true, - Town.DARUNIA_WEST => true, - Town.NEW_KASUTO => true, - Town.OLD_KASUTO => true, - Town.MIDO_CHURCH => true, - Town.DARUNIA_ROOF => true, - _ => false - }; - } - - public static string HintName(this Town town) - { - return town switch - { - Town.RAURU => "RAURU", - Town.RUTO => "RUTO", - Town.SARIA_NORTH => "SARIA", - Town.MIDO_WEST => "MIDO", - Town.NABOORU => "NABOORU", - Town.DARUNIA_WEST => "DARUNIA", - Town.NEW_KASUTO => "NEW KASUTO", - Town.OLD_KASUTO => "OLD KASUTO", - Town.MIDO_CHURCH => "MIDO", - Town.DARUNIA_ROOF => "DARUNIA", - _ => "UNKNOWN" - }; - } - - public static bool AppearsOnMap(this Town town) - { - return town switch - { - Town.RAURU => true, - Town.RUTO => true, - Town.SARIA_NORTH => true, - Town.SARIA_SOUTH => true, - Town.MIDO_WEST => true, - Town.MIDO_CHURCH => false, - Town.NABOORU => true, - Town.DARUNIA_WEST => true, - Town.DARUNIA_ROOF => false, - Town.NEW_KASUTO => true, - Town.OLD_KASUTO => true, - Town.SARIA_TABLE => false, - Town.BAGU => true, - Town.NABOORU_FOUNTAIN => false, - _ => throw new Exception("Unrecognized town: " + town.ToString()) - }; - } - - public static Town GetMasterTown(this Town town) - { - return town switch - { - Town.SARIA_TABLE => Town.SARIA_NORTH, - Town.DARUNIA_ROOF => Town.DARUNIA_WEST, - Town.NABOORU_FOUNTAIN => Town.NABOORU, - Town.MIDO_CHURCH => Town.MIDO_WEST, - _ => town - }; - } - - public static bool IsUnderConsiderationForReachable(this Town town) - { - return town switch - { - Town.RAURU => true, - Town.RUTO => true, - Town.SARIA_NORTH => true, - Town.SARIA_SOUTH => true, - Town.MIDO_WEST => true, - Town.MIDO_CHURCH => false, - Town.NABOORU => true, - Town.DARUNIA_WEST => true, - Town.DARUNIA_ROOF => false, - Town.NEW_KASUTO => true, - Town.OLD_KASUTO => true, - Town.SARIA_TABLE => false, - Town.BAGU => true, - Town.NABOORU_FOUNTAIN => false, - _ => throw new Exception("Unrecognized town: " + town.ToString()) - }; - } -} diff --git a/RandomizerCore/TownEnum.cs b/RandomizerCore/TownEnum.cs new file mode 100644 index 000000000..f4dcb6b47 --- /dev/null +++ b/RandomizerCore/TownEnum.cs @@ -0,0 +1,137 @@ +/* +using System; + +namespace Z2Randomizer.RandomizerCore; + +public enum TownEnum +{ + INVALID = 0, + RAURU = 1, + RUTO = 2, + SARIA_NORTH = 3, + SARIA_SOUTH = 4, + MIDO_WEST = 5, + MIDO_CHURCH = 6, + NABOORU = 7, + DARUNIA_WEST = 8, + DARUNIA_ROOF = 9, + NEW_KASUTO = 10, + OLD_KASUTO = 11, + SARIA_TABLE = 12, + BAGU = 13, + NABOORU_FOUNTAIN = 14, +} + +public static class TownExtensions +{ + public const int SPELL_GET_START_ADDRESS = 0x17AF7; + public static int VanillaTownOrder(this TownEnum town) + { + return town switch + { + TownEnum.INVALID => 99, + TownEnum.RAURU => 1, + TownEnum.RUTO => 2, + TownEnum.SARIA_NORTH => 3, + TownEnum.MIDO_WEST => 4, + TownEnum.NABOORU => 5, + TownEnum.DARUNIA_WEST => 6, + TownEnum.NEW_KASUTO => 7, + TownEnum.OLD_KASUTO => 8, + _ => 0 + }; + } + + public static bool IsWizardTown(this TownEnum town) + { + return town switch + { + TownEnum.RAURU => true, + TownEnum.RUTO => true, + TownEnum.SARIA_NORTH => true, + TownEnum.MIDO_WEST => true, + TownEnum.NABOORU => true, + TownEnum.DARUNIA_WEST => true, + TownEnum.NEW_KASUTO => true, + TownEnum.OLD_KASUTO => true, + TownEnum.MIDO_CHURCH => true, + TownEnum.DARUNIA_ROOF => true, + _ => false + }; + } + + public static string HintName(this TownEnum town) + { + return town switch + { + TownEnum.RAURU => "RAURU", + TownEnum.RUTO => "RUTO", + TownEnum.SARIA_NORTH => "SARIA", + TownEnum.MIDO_WEST => "MIDO", + TownEnum.NABOORU => "NABOORU", + TownEnum.DARUNIA_WEST => "DARUNIA", + TownEnum.NEW_KASUTO => "NEW KASUTO", + TownEnum.OLD_KASUTO => "OLD KASUTO", + TownEnum.MIDO_CHURCH => "MIDO", + TownEnum.DARUNIA_ROOF => "DARUNIA", + _ => "UNKNOWN" + }; + } + + public static bool AppearsOnMap(this TownEnum town) + { + return town switch + { + TownEnum.RAURU => true, + TownEnum.RUTO => true, + TownEnum.SARIA_NORTH => true, + TownEnum.SARIA_SOUTH => true, + TownEnum.MIDO_WEST => true, + TownEnum.MIDO_CHURCH => false, + TownEnum.NABOORU => true, + TownEnum.DARUNIA_WEST => true, + TownEnum.DARUNIA_ROOF => false, + TownEnum.NEW_KASUTO => true, + TownEnum.OLD_KASUTO => true, + TownEnum.SARIA_TABLE => false, + TownEnum.BAGU => true, + TownEnum.NABOORU_FOUNTAIN => false, + _ => throw new Exception("Unrecognized town: " + town.ToString()) + }; + } + + public static TownEnum GetMasterTown(this TownEnum town) + { + return town switch + { + TownEnum.SARIA_TABLE => TownEnum.SARIA_NORTH, + TownEnum.DARUNIA_ROOF => TownEnum.DARUNIA_WEST, + TownEnum.NABOORU_FOUNTAIN => TownEnum.NABOORU, + TownEnum.MIDO_CHURCH => TownEnum.MIDO_WEST, + _ => town + }; + } + + public static bool IsUnderConsiderationForReachable(this TownEnum town) + { + return town switch + { + TownEnum.RAURU => true, + TownEnum.RUTO => true, + TownEnum.SARIA_NORTH => true, + TownEnum.SARIA_SOUTH => true, + TownEnum.MIDO_WEST => true, + TownEnum.MIDO_CHURCH => false, + TownEnum.NABOORU => true, + TownEnum.DARUNIA_WEST => true, + TownEnum.DARUNIA_ROOF => false, + TownEnum.NEW_KASUTO => true, + TownEnum.OLD_KASUTO => true, + TownEnum.SARIA_TABLE => false, + TownEnum.BAGU => true, + TownEnum.NABOORU_FOUNTAIN => false, + _ => throw new Exception("Unrecognized town: " + town.ToString()) + }; + } +} +*/ \ No newline at end of file diff --git a/RandomizerCore/Towns.cs b/RandomizerCore/Towns.cs deleted file mode 100644 index 88425b6f0..000000000 --- a/RandomizerCore/Towns.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; - -namespace Z2Randomizer.RandomizerCore; - -internal class Towns -{ - /* - public static readonly Dictionary townSpellAndItemRequirements = new() - { - { Town.RAURU, new Requirements() }, - { Town.RUTO, new Requirements([RequirementType.TROPHY]) }, - { Town.SARIA_NORTH, new Requirements([RequirementType.MIRROR]) }, - { Town.MIDO_WEST, new Requirements([RequirementType.MEDICINE]) }, - { Town.MIDO_CHURCH, new Requirements([RequirementType.JUMP, RequirementType.FAIRY]) }, - { Town.NABOORU, new Requirements([], [[RequirementType.WATER, RequirementType.FIVE_CONTAINERS]]) }, - { Town.DARUNIA_WEST, new Requirements([], [[RequirementType.CHILD, RequirementType.SIX_CONTAINERS]]) }, - { Town.DARUNIA_ROOF, new Requirements([RequirementType.JUMP, RequirementType.FAIRY]) }, - { Town.NEW_KASUTO, new Requirements([RequirementType.SEVEN_CONTAINERS]) }, - { Town.OLD_KASUTO, new Requirements([RequirementType.EIGHT_CONTAINERS]) }, - { Town.NABOORU_FOUNTAIN, new Requirements() }, - { Town.BAGU, new Requirements() }, - { Town.SARIA_TABLE, new Requirements() } - }; - */ - - public static readonly List STRICT_SPELL_LOCATIONS = [ Town.RAURU, Town.RUTO, Town.SARIA_NORTH, Town.MIDO_WEST, - Town.NABOORU, Town.DARUNIA_WEST, Town.NEW_KASUTO, Town.OLD_KASUTO ]; - - public static readonly List ITEM_LOCATION_TOWNS = [ Town.RAURU, Town.RUTO, Town.SARIA_NORTH, Town.MIDO_WEST, - Town.NABOORU, Town.DARUNIA_WEST, Town.NEW_KASUTO, Town.OLD_KASUTO, - Town.MIDO_CHURCH, Town.DARUNIA_ROOF, - Town.SARIA_TABLE, Town.NABOORU_FOUNTAIN, Town.BAGU]; -} diff --git a/RandomizerCore/Util.cs b/RandomizerCore/Util.cs index 7d97494be..c4564457d 100644 --- a/RandomizerCore/Util.cs +++ b/RandomizerCore/Util.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using NLog; using Z2Randomizer.RandomizerCore.Overworld; -using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore; @@ -98,17 +98,6 @@ public static string FromGameText(IEnumerable bytes) return output.ToString(); } - public static void Swap(Location p1, Location p2) - { - //(p2.Continent, p1.Continent) = (p1.Continent, p2.Continent); - (p2.Map, p1.Map) = (p1.Map, p2.Map); - (p2.PalaceNumber, p1.PalaceNumber) = (p1.PalaceNumber, p2.PalaceNumber); - - (p2.ActualTown, p1.ActualTown) = (p1.ActualTown, p2.ActualTown); - (p2.Collectables, p1.Collectables) = (p1.Collectables, p2.Collectables); - (p2.Name, p1.Name) = (p1.Name, p2.Name); - } - public static string ByteArrayToHexString(byte[] bytes) { string hex = BitConverter.ToString(bytes); diff --git a/Statistics/Result.cs b/Statistics/Result.cs index 9790d7dd9..793edde78 100644 --- a/Statistics/Result.cs +++ b/Statistics/Result.cs @@ -6,6 +6,7 @@ using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Overworld; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.Statistics; diff --git a/Statistics/Statistics.cs b/Statistics/Statistics.cs index 11d2b74d9..ee4e5f4d1 100644 --- a/Statistics/Statistics.cs +++ b/Statistics/Statistics.cs @@ -6,6 +6,7 @@ using Desktop.Common; using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.Statistics; diff --git a/Tests/FlagsTests.cs b/Tests/FlagsTests.cs index 2ef5017a9..ae94c908a 100644 --- a/Tests/FlagsTests.cs +++ b/Tests/FlagsTests.cs @@ -208,7 +208,7 @@ public void SharedSeedExportIgnoresDifficultyOnlySettings() EnemyXPDrops = XPEffectiveness.NONE }; - RandomizerProperties properties = config.Export(new Random(1234), includeDifficulty: false); + RandomizerProperties properties = config.Export(new RandomizerCore.Random(1234), includeDifficulty: false); Assert.IsFalse(properties.StartCandle); Assert.IsFalse(properties.StartCross); diff --git a/Tests/RoomSerializationTests.cs b/Tests/RoomSerializationTests.cs index 7a772db34..a154a39e0 100644 --- a/Tests/RoomSerializationTests.cs +++ b/Tests/RoomSerializationTests.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.Tests; diff --git a/Tests/Utils.cs b/Tests/Utils.cs index ff526a1cc..6a9f322c0 100644 --- a/Tests/Utils.cs +++ b/Tests/Utils.cs @@ -5,6 +5,7 @@ using System.Text.RegularExpressions; using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.Tests; diff --git a/ValidateRooms/Program.cs b/ValidateRooms/Program.cs index a005b7101..7273bff1d 100644 --- a/ValidateRooms/Program.cs +++ b/ValidateRooms/Program.cs @@ -6,6 +6,7 @@ using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Enemy; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; using static Z2Randomizer.RandomizerCore.Util; StringBuilder sb = new StringBuilder(""); diff --git a/Z2Randomizer.sln b/Z2Randomizer.sln index 22545c485..470f451a7 100644 --- a/Z2Randomizer.sln +++ b/Z2Randomizer.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.33530.505 +# Visual Studio Version 18 +VisualStudioVersion = 18.7.11911.148 stable MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RandomizerCore", "RandomizerCore\RandomizerCore.csproj", "{34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}" ProjectSection(ProjectDependencies) = postProject @@ -60,8 +60,8 @@ Global {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Release|Any CPU.Build.0 = Release|Any CPU {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Release|x64.ActiveCfg = Release|x64 {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Release|x64.Build.0 = Release|x64 - {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Unsafe Debug|Any CPU.ActiveCfg = Unsafe Debug|Any CPU - {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Unsafe Debug|Any CPU.Build.0 = Unsafe Debug|Any CPU + {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Unsafe Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Unsafe Debug|Any CPU.Build.0 = Debug|Any CPU {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Unsafe Debug|x64.ActiveCfg = Unsafe Debug|Any CPU {34CEBF26-BA9D-4B1F-83EA-9FA87EDC2328}.Unsafe Debug|x64.Build.0 = Unsafe Debug|Any CPU {45FE0E64-60F2-47CF-AE93-FD5A41FF8F52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -180,8 +180,8 @@ Global {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Release|Any CPU.Build.0 = Release|Any CPU {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Release|x64.ActiveCfg = Release|Any CPU {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Release|x64.Build.0 = Release|Any CPU - {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Unsafe Debug|Any CPU.ActiveCfg = Unsafe Debug|Any CPU - {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Unsafe Debug|Any CPU.Build.0 = Unsafe Debug|Any CPU + {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Unsafe Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Unsafe Debug|Any CPU.Build.0 = Debug|Any CPU {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Unsafe Debug|x64.ActiveCfg = Unsafe Debug|Any CPU {6C838F98-8DAC-4E99-A756-EFFBD4331FBA}.Unsafe Debug|x64.Build.0 = Unsafe Debug|Any CPU {27087DD8-40ED-60F9-0765-BA784D146BAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU From 59b5c65f255c540d022e9d350a69a7cd1e3132a2 Mon Sep 17 00:00:00 2001 From: Ellendar Date: Thu, 25 Jun 2026 16:56:55 -0400 Subject: [PATCH 07/10] Fixed custom text --- RandomizerCore/CustomTexts.cs | 1 + RandomizerCore/Hyrule.cs | 2 +- RandomizerCore/Sidescroll/Town/Town.cs | 1 + RandomizerCore/SpritePatcher.cs | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index 22a5c6e2e..939c805e4 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -124,6 +124,7 @@ public class CustomTexts { TownType.DARUNIA, 81 }, { TownType.NEW_KASUTO, 93 }, { TownType.OLD_KASUTO, 96 }, + { TownType.BAGU, baguTextIndex } }; private static readonly Dictionary wizardTextIndexesBySpell = new() diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 503dfee17..f9752bb80 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -2744,7 +2744,7 @@ private void UpdateRom() { // Use the old spell menu behavior where the spells are placed in the menu in the location it came from var wizardCollectables = AllLocations() - .Where(l => l.Town?.GetWizard() != null) + .Where(l => l.Town?.GetWizard() != null && l != westHyrule.sariaSouth && l != westHyrule.bagu) .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); for (int i = 0; i < magFunction.Length; i++) diff --git a/RandomizerCore/Sidescroll/Town/Town.cs b/RandomizerCore/Sidescroll/Town/Town.cs index 262898cbf..6c127908c 100644 --- a/RandomizerCore/Sidescroll/Town/Town.cs +++ b/RandomizerCore/Sidescroll/Town/Town.cs @@ -186,6 +186,7 @@ public bool ReplaceCollectable(Collectable toReplace, Collectable replacement) TownType.DARUNIA => VanillaTownMap.DARUNIA_WIZARD, TownType.NEW_KASUTO => VanillaTownMap.NEW_KASUTO_WIZARD, TownType.OLD_KASUTO => VanillaTownMap.OLD_KASUTO_WIZARD, + TownType.BAGU => VanillaTownMap.BAGU_INDOORS, _ => throw new ImpossibleException("Unrecognized town type in GetWizard()") }; return GetTownMap(map); diff --git a/RandomizerCore/SpritePatcher.cs b/RandomizerCore/SpritePatcher.cs index 3e4a4c5c4..8ce9d6a40 100644 --- a/RandomizerCore/SpritePatcher.cs +++ b/RandomizerCore/SpritePatcher.cs @@ -117,7 +117,7 @@ public static void PatchSpriteSanitized(byte[] romData, byte[] ipsData, bool exp else { byte oldValue = romData[tgtOffs + i]; - logger.Error($"Moderating IPS patch at address: 0x{addr.ToString("x5")} from 0x{oldValue.ToString("x2")} to 0x{newValue.ToString("x2")}"); + logger.Trace($"Moderating IPS patch at address: 0x{addr.ToString("x5")} from 0x{oldValue.ToString("x2")} to 0x{newValue.ToString("x2")}"); } } if (fillValue is null) From a1527f5fbe53d18e638a7ba10ed5677180bd116f Mon Sep 17 00:00:00 2001 From: Ellendar Date: Fri, 3 Jul 2026 18:53:48 -0400 Subject: [PATCH 08/10] Moderate rework of item shuffle handling. Almost certainly still buggy. --- RandomizerCore/CustomTexts.cs | 8 +- RandomizerCore/Hyrule.cs | 279 ++++++++------------- RandomizerCore/Overworld/EastHyrule.cs | 6 +- RandomizerCore/Overworld/Location.cs | 24 +- RandomizerCore/Overworld/LocationID.cs | 4 +- RandomizerCore/Overworld/WestHyrule.cs | 16 +- RandomizerCore/Overworld/World.cs | 14 +- RandomizerCore/ROM.cs | 56 ----- RandomizerCore/Sidescroll/Palace/Palace.cs | 3 +- RandomizerCore/Sidescroll/Town/Town.cs | 4 +- 10 files changed, 167 insertions(+), 247 deletions(-) diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index 939c805e4..a1804f790 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -506,7 +506,6 @@ public CustomTexts(List texts) public static List GenerateTexts( IEnumerable locations, - IEnumerable itemLocations, List texts, RandomizerProperties props, Random hashRNG) @@ -603,7 +602,7 @@ public static List GenerateTexts( if (props.HelpfulHints) { - List placedIndexes = GenerateHelpfulHints(texts, itemLocations, hashRNG, props); + List placedIndexes = GenerateHelpfulHints(texts, locations, hashRNG, props); GenerateKnowNothings(texts, placedIndexes, nonhashRNG, props.BagusWoods, props.UseCommunityText); } @@ -686,7 +685,10 @@ private static void GenerateTownNameHints(List texts, IEnumerable i.Town?.GetWizard() != null)) { - texts[TOWN_SIGN_INDEXES[(TownType)location.Town!.Type!]] = GenerateTownSignHint((Collectable)location.Town.GetWizard()!.Collectable!, linkedFire); + if(location?.Town?.Type != null && TOWN_SIGN_INDEXES.TryGetValue((TownType)location?.Town?.Type!, out int index)) + { + texts[index] = GenerateTownSignHint((Collectable)location.Town.GetWizard()!.Collectable!, linkedFire); + } } } private static Text GenerateTownSignHint(Collectable spell, bool linkedFire) diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index f9752bb80..c5b7774c1 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -6,9 +6,11 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; @@ -83,8 +85,6 @@ public class Hyrule public HashSet reachableAreas; //Which vanilla spell corresponds with which shuffled spell //private Dictionary WizardCollectables { get; set; } - //Locations that contain an item - private List itemLocs; //Locations that are pbags in vanilla that are turned into hearts because maxhearts - startinghearts > 4 private List pbagHearts; //Continent connectors @@ -121,7 +121,7 @@ public class Hyrule //DEBUG/STATS #pragma warning disable CS0414 // Field is assigned but its value is never used - private static int DEBUG_THRESHOLD = 1; + private static int DEBUG_THRESHOLD = 100; #pragma warning restore CS0414 // Field is assigned but its value is never used public DateTime startTime = DateTime.Now; // public DateTime startRandomizeStartingValuesTimestamp; @@ -363,7 +363,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC List? customSpellOrder = props.IncludeSpellsInShuffle ? null : AllLocations() - .Where(l => l.Town?.GetWizard() != null) + .Where(l => l.Town?.GetWizard() != null && l != westHyrule.bagu && l != westHyrule.sariaSouth) .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); ROMData.CombineFireSpell(assembler, customSpellOrder, r); } @@ -396,7 +396,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC if (ct.IsCancellationRequested) { return new RandomizerResult(false); } UpdateProgress(progress, 9); - List texts = CustomTexts.GenerateTexts(AllLocations(), itemLocs, ROMData.GetGameText(), props, r); + List texts = CustomTexts.GenerateTexts(AllLocations(), ROMData.GetGameText(), props, r); StatRandomizer randomizedStats = new(ROMData, props); randomizedStats.Randomize(r, skipDifficultyOnly: shareSeedAcrossDifficulty); @@ -645,7 +645,7 @@ private void ReplaceDifficultyStartingItemsInWorld(Random r) { if (!props.StartsWithCollectable(item)) { continue; } Collectable replacementItem = minorItems.Sample(r); - foreach (Location location in itemLocs) + foreach (Location location in AllLocations().Where(i => i.GetCollectableCount() > 0)) { if(location.ReplaceCollectable(item, replacementItem)) { @@ -677,6 +677,7 @@ private void ShufflePalaces() private void ShuffleItems() { List shufflableItems = []; + List globalShuffleLocations = []; List minorItems = [Collectable.BLUE_JAR, Collectable.RED_JAR, Collectable.SMALL_BAG, Collectable.MEDIUM_BAG, Collectable.LARGE_BAG, Collectable.XL_BAG, Collectable.ONEUP, Collectable.KEY]; @@ -694,6 +695,7 @@ private void ShuffleItems() collectable = (Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE); eastHyrule.pbagCave2.SetCollectables([collectable]); shufflableItems.Add(collectable); + globalShuffleLocations.Add([westHyrule.pbagCave, eastHyrule.pbagCave1, eastHyrule.pbagCave2]); } else { @@ -702,7 +704,7 @@ private void ShuffleItems() eastHyrule.pbagCave2.SetCollectables([Collectable.XL_BAG]); } - if(props.IncludeSpellsInShuffle) + if (props.IncludeSpellsInShuffle) { shufflableItems.Add(Collectable.SHIELD_SPELL); shufflableItems.Add(Collectable.JUMP_SPELL); @@ -712,37 +714,32 @@ private void ShuffleItems() shufflableItems.Add(Collectable.REFLECT_SPELL); shufflableItems.Add(Collectable.SPELL_SPELL); shufflableItems.Add(Collectable.THUNDER_SPELL); + globalShuffleLocations.Add([westHyrule.rauru, westHyrule.ruto, westHyrule.sariaNorth, westHyrule.mido, + eastHyrule.nabooru, eastHyrule.darunia, eastHyrule.newKasuto, eastHyrule.oldKasuto]); } - else if(props.ShuffleSpellLocations) + else if (props.ShuffleSpellLocations) { ShuffleSpells(); } - int mirrorIndex = 0, waterindex = 0; if (props.IncludeQuestItemsInShuffle) { shufflableItems.Add(Collectable.BAGUS_NOTE); - if (props.StartWithSpellItems) - { - shufflableItems.Add(minorItems[r.Next(minorItems.Count)]); - shufflableItems.Add(minorItems[r.Next(minorItems.Count)]); - } - else - { - mirrorIndex = shufflableItems.Count; - shufflableItems.Add(Collectable.MIRROR); - waterindex = shufflableItems.Count; - shufflableItems.Add(Collectable.WATER); - } + shufflableItems.Add(Collectable.MIRROR); + shufflableItems.Add(Collectable.WATER); + globalShuffleLocations.AddRange([westHyrule.bagu, westHyrule.sariaNorth, eastHyrule.nabooru]); } if (props.IncludeSwordTechsInShuffle) { - shufflableItems.Add(Collectable.UPSTAB); - shufflableItems.Add(Collectable.DOWNSTAB); + TownMap? townMap = westHyrule.mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER); + shufflableItems.Add(props.StartWithDownstab ? minorItems.Sample(r) : Collectable.DOWNSTAB); + globalShuffleLocations.Add(westHyrule.sariaNorth); + townMap = eastHyrule.darunia.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER); + shufflableItems.Add(props.StartWithUpstab ? minorItems.Sample(r) : Collectable.UPSTAB); + globalShuffleLocations.Add(eastHyrule.darunia); } - - else if(props.SwapUpAndDownStab) + else if (props.SwapUpAndDownStab) { SwapUpAndDownstab(); } @@ -750,22 +747,8 @@ private void ShuffleItems() int heartContainersInItemPool = props.MaxHearts - props.StartHearts; int magicContainersInItemPool = props.MaxMagicContainers - props.StartMagicContainers; - foreach (Collectable item in ItemGet.Keys.ToList()) - { - ItemGet[item] = false; - } - - //TODO: Refactor these puts out of this class - ROMData.Put(RomMap.START_CANDLE, props.StartCandle ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_GLOVE, props.StartGlove ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_RAFT, props.StartRaft ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_BOOTS, props.StartBoots ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_FLUTE, props.StartFlute ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_CROSS, props.StartCross ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_HAMMER, props.StartHammer ? (byte)1 : (byte)0); - ROMData.Put(RomMap.START_MAGICAL_KEY, props.StartKey ? (byte)1 : (byte)0); - foreach(Collectable collectable in ItemGet.Keys) + foreach (Collectable collectable in ItemGet.Keys) { ItemGet[collectable] = props.StartsWithCollectable(collectable); } @@ -783,7 +766,7 @@ private void ShuffleItems() List collectables = hcLocation.GetAllCollectables(); List order = Enumerable.Range(0, collectables.Count).ToList(); order.FisherYatesShuffle(r); - foreach(int index in order) + foreach (int index in order) { if (collectables[index] == Collectable.HEART_CONTAINER) { @@ -828,64 +811,35 @@ private void ShuffleItems() } } + //Spell Item handling if (props.StartWithSpellItems) { - Debug.Assert(shufflableItems[9] == Collectable.MEDICINE); - Debug.Assert(shufflableItems[10] == Collectable.TROPHY); - Debug.Assert(shufflableItems[17] == Collectable.CHILD); - shufflableItems[9] = minorItems[r.Next(minorItems.Count)]; - shufflableItems[10] = minorItems[r.Next(minorItems.Count)]; - shufflableItems[17] = minorItems[r.Next(minorItems.Count)]; - ItemGet[Collectable.TROPHY] = true; - ItemGet[Collectable.MEDICINE] = true; - ItemGet[Collectable.CHILD] = true; - ItemGet[Collectable.MIRROR] = true; - ItemGet[Collectable.WATER] = true; - } - else if(!props.IncludeSpellsInShuffle) - { - Collectable wizardCollectable; - //wizardCollectable = westHyrule.AllLocations.First(i => i.ActualTown == TownType.RUTO).Collectables[0]; - wizardCollectable = (Collectable)westHyrule.ruto.Town!.GetWizard()!.Collectable!; - if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) + Collectable[] spellItems = [Collectable.TROPHY, Collectable.MEDICINE, Collectable.CHILD, Collectable.MIRROR, Collectable.WATER]; + foreach (Collectable spellItem in spellItems) { - Debug.Assert(shufflableItems[10] == Collectable.TROPHY); - shufflableItems[10] = minorItems[r.Next(minorItems.Count)]; - ItemGet[Collectable.TROPHY] = true; - } - - wizardCollectable = (Collectable)westHyrule.mido.Town!.GetWizard()!.Collectable!; - if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) - { - Debug.Assert(shufflableItems[9] == Collectable.MEDICINE); - shufflableItems[9] = minorItems[r.Next(minorItems.Count)]; - ItemGet[Collectable.MEDICINE] = true; + int index = shufflableItems.IndexOf(spellItem); + if (index > -1) + { + shufflableItems[index] = minorItems.Sample(r); + } + ItemGet[spellItem] = true; } - - wizardCollectable = (Collectable)eastHyrule.darunia.Town!.GetWizard()!.Collectable!; - if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) + } + else if (!props.IncludeSpellsInShuffle) + { + List spellItemTowns = [westHyrule.ruto, westHyrule.mido, eastHyrule.darunia]; + if (props.IncludeQuestItemsInShuffle) { - Debug.Assert(shufflableItems[17] == Collectable.CHILD); - shufflableItems[17] = minorItems[r.Next(minorItems.Count)]; - ItemGet[Collectable.CHILD] = true; + spellItemTowns.Add([westHyrule.sariaNorth, eastHyrule.nabooru]); } - if (props.IncludeQuestItemsInShuffle) + foreach (Location location in spellItemTowns) { - wizardCollectable = (Collectable)westHyrule.sariaNorth.Town!.GetWizard()!.Collectable!; - if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) + Collectable wizardCollectable = (Collectable)location.Town!.GetWizard()!.Collectable!; + if (props.StartsWithCollectable(wizardCollectable)) { - Debug.Assert(shufflableItems[mirrorIndex] == Collectable.MIRROR); - shufflableItems[mirrorIndex] = minorItems[r.Next(minorItems.Count)]; - ItemGet[Collectable.MIRROR] = true; - } - - wizardCollectable = (Collectable)eastHyrule.nabooru.Town!.GetWizard()!.Collectable!; - if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) - { - Debug.Assert(shufflableItems[waterindex] == Collectable.WATER); - shufflableItems[waterindex] = minorItems[r.Next(minorItems.Count)]; - ItemGet[Collectable.WATER] = true; + ItemGet[wizardCollectable] = true; + location.Town!.GetWizard()!.Collectable = minorItems.Sample(r); } } } @@ -910,15 +864,19 @@ private void ShuffleItems() Collectable.SPELL_SPELL, Collectable.THUNDER_SPELL]; - foreach(Collectable item in possibleStartItems) + foreach (Collectable item in possibleStartItems) { - if(props.StartsWithCollectable(item)) + if (props.StartsWithCollectable(item)) { - shufflableItems[shufflableItems.IndexOf(item)] = minorItems.Sample(r); + int index = shufflableItems.IndexOf(item); + if (index > -1) + { + shufflableItems[index] = minorItems.Sample(r); + } } } - if(props.IncludeSpellsInShuffle) + if (props.IncludeSpellsInShuffle) { foreach (Collectable item in possibleStartSpells) { @@ -929,31 +887,7 @@ private void ShuffleItems() } } - TownMap? townMap = itemLocs.SelectMany(i => i.Town?.TownMaps ?? []).FirstOrDefault(i => i.Map == (int)VanillaTownMap.MIDO_TRAINER); - if(townMap != null) - { - townMap.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; - } - townMap = itemLocs.SelectMany(i => i.Town?.TownMaps ?? []).FirstOrDefault(i => i.Map == (int)VanillaTownMap.DARUNIA_TRAINER); - if (townMap != null) - { - townMap.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; - } - - if (props.IncludeSwordTechsInShuffle) - { - if (props.StartWithDownstab) - { - shufflableItems[shufflableItems.IndexOf(Collectable.DOWNSTAB)] = minorItems.Sample(r); - } - if (props.StartWithUpstab) - { - shufflableItems[shufflableItems.IndexOf(Collectable.UPSTAB)] = minorItems.Sample(r); - } - } - //Handle excess items - List excessItems = []; //Heart containers over 4 are excess @@ -961,19 +895,18 @@ private void ShuffleItems() { excessItems.Add(Collectable.HEART_CONTAINER); } - + //Magic containers over 4 are excess for (int i = 4; i < magicContainersInItemPool; i++) { excessItems.Add(Collectable.MAGIC_CONTAINER); } - int extraPalaceItemCount = props.PalaceItemRoomCounts.Select(c => Math.Max(c - 1, 0)).Sum(); + int extraPalaceItemCount = props.PalaceItemRoomCounts.Sum(c => Math.Max(c - 1, 0)); for (int i = 0; i < extraPalaceItemCount; i++) { shufflableItems.Add(minorItems.Sample(r)); } - List vanillaPalaceItems = [Collectable.CANDLE, Collectable.GLOVE, Collectable.RAFT, Collectable.BOOTS, Collectable.FLUTE, Collectable.CROSS]; //No palace items handling @@ -1023,7 +956,8 @@ private void ShuffleItems() for(int i = 0; i < overflowLocationsRequired; i++) { Location overflowLocation = overflowLocations.Sample(r)!; - itemLocs.Add(overflowLocation); + overflowLocation.CollectablesAreShufflable = true; + globalShuffleLocations.Add(overflowLocation); shufflableItems.Add(Collectable.SMALL_BAG); minorItemIndexes.Add(shufflableItems.Count - 1); overflowLocations.Remove(overflowLocation); @@ -1045,8 +979,9 @@ private void ShuffleItems() if (props.MixOverworldPalaceItems) { - duplicateItemPlacementCandidates.AddRange(itemLocs); - DoShuffle(shufflableItems, itemLocs); + + duplicateItemPlacementCandidates.AddRange(globalShuffleLocations); + DoShuffle(shufflableItems, globalShuffleLocations); } else { @@ -1057,11 +992,11 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location palaceLocation in itemLocs.Where(i => i.Palace != null)) + foreach (Location palaceLocation in AllLocations().Where(i => i.Palace != null)) { shufflableItemLocations.Add(palaceLocation); Collectable vanillaCollectable = palaceLocation.Palace!.GetVanillaCollectable(); - itemsToActuallyShuffle.Add(shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)); + itemsToActuallyShuffle.Add(props.StartsWithCollectable(vanillaCollectable) ? minorItems.Sample(r) : vanillaCollectable); for (int i = 1; i < props.PalaceItemRoomCounts[(int)palaceLocation.Palace.Number! - 1]; i++) { @@ -1073,7 +1008,7 @@ private void ShuffleItems() } else //no shuffle palace items { - foreach (Location palaceLocation in itemLocs.Where(i => i.Palace != null && i.Palace.Number < 7)) + foreach (Location palaceLocation in AllLocations().Where(i => i.Palace != null && i.Palace.Number < 7)) { int palaceNumber = palaceLocation.Palace!.Number; Collectable vanillaCollectable = palaceLocation.Palace!.GetVanillaCollectable(); @@ -1095,7 +1030,7 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location nonPalaceLocation in itemLocs.Where(i => i.Palace == null)) + foreach (Location nonPalaceLocation in AllLocations().Where(i => i.Palace == null)) { shufflableItemLocations.Add(nonPalaceLocation); foreach(Collectable collectable in nonPalaceLocation.GetAllCollectables()) @@ -1108,7 +1043,7 @@ private void ShuffleItems() } else { - foreach (Location nonPalaceLocation in itemLocs.Where(i => i.Palace == null)) + foreach (Location nonPalaceLocation in AllLocations().Where(i => i.Palace == null)) { List collectables = nonPalaceLocation.GetAllCollectables(); for(int i = 0; i < collectables.Count; i++) @@ -1200,10 +1135,9 @@ private void ShuffleItems() importantItemsToDuplicate = importantItemsToDuplicate.Where(shufflableItems.Contains).ToList(); - minorItemLocations = duplicateItemPlacementCandidates.Where(i => i.GetAllCollectables().Any(c => c.IsMinorItem())).ToList(); - int replaceableMinorItemCount = duplicateItemPlacementCandidates.Sum(l => l.GetAllCollectables().Count(c => c.IsMinorItem())); + minorItemLocations = duplicateItemPlacementCandidates.Where(i => i.GetShufflableCollectables().Any(c => c.IsMinorItem())).ToList(); + int replaceableMinorItemCount = duplicateItemPlacementCandidates.Sum(l => l.GetShufflableCollectables().Count(c => c.IsMinorItem())); importantItemsToDuplicate = importantItemsToDuplicate.GetRange(0, int.Min(replaceableMinorItemCount, importantItemsToDuplicate.Count)); - importantItemsToDuplicate.FisherYatesShuffle(r); minorItemLocations.FisherYatesShuffle(r); @@ -1211,7 +1145,7 @@ private void ShuffleItems() { Location minorItemLocation = minorItemLocations.Sample(r)!; List locationCollectables = minorItemLocation.GetAllCollectables(); - List collectableIndexes = Enumerable.Range(0, minorItemLocation.GetAllCollectables().Count()).ToList(); + List collectableIndexes = Enumerable.Range(0, minorItemLocation.GetCollectableCount()).ToList(); collectableIndexes.FisherYatesShuffle(r); foreach(int collectableIndex in collectableIndexes) { @@ -1277,7 +1211,8 @@ bool IsChainLocation(Location l) => private void DoShuffle(List itemsToShuffle, List itemShuffleLocations) { - int possibleItemPlacements = itemShuffleLocations.Sum(i => i.GetAllCollectables().Count); + itemShuffleLocations = itemShuffleLocations.Distinct().ToList(); + int possibleItemPlacements = itemShuffleLocations.Sum(i => i.GetCollectableCount(true)); if (itemsToShuffle.Count != possibleItemPlacements) { @@ -1290,10 +1225,10 @@ private void DoShuffle(List itemsToShuffle, List itemShuf int itemIndex = 0; foreach(Location location in itemShuffleLocations) { - int itemCount = location.GetAllCollectables().Count; - List collectables = itemsToShuffle.GetRange(itemIndex, itemCount); - itemIndex += itemCount; - location.SetCollectables(collectables); + int collectableCount = location.GetCollectableCount(true); + List newCollectables = itemsToShuffle.GetRange(itemIndex, collectableCount); + itemIndex += collectableCount; + location.SetCollectables(newCollectables); } } @@ -1461,22 +1396,22 @@ private bool IsEverythingReachable(Dictionary itemGet) foreach (World world in worlds) { - if (world.raft != null && CanGet(world.raft) && itemGet[Collectable.RAFT]) + if (world.raft != null && world.raft.Reachable && itemGet[Collectable.RAFT]) { worlds.ForEach(i => i.VisitRaft()); } - if (world.bridge != null && CanGet(world.bridge)) + if (world.bridge != null && world.bridge.Reachable) { worlds.ForEach(i => i.VisitBridge()); } - if (world.cave1 != null && CanGet(world.cave1)) + if (world.cave1 != null && world.cave1.Reachable) { worlds.ForEach(i => i.VisitCave1()); } - if (world.cave2 != null && CanGet(world.cave2)) + if (world.cave2 != null && world.cave2.Reachable) { worlds.ForEach(i => i.VisitCave2()); } @@ -1520,6 +1455,7 @@ private bool IsEverythingReachable(Dictionary itemGet) PrintRoutingDebug(reachableLocationsCount, wh, eh, dm, mi); //Debug.WriteLine(GenerateSpoiler()); //Debug.WriteLine(westHyrule.GetMapDebug()); + //XXX: return false; } //#endif @@ -1582,10 +1518,7 @@ private bool CanGet(IEnumerable locations) { return locations.All(i => i.Reachable); } - private bool CanGet(Location location) - { - return location.Reachable; - } + private void ShortenWizards() { //Change the exit leading to the intermedaite wizard room to go directly to the basement @@ -1642,7 +1575,7 @@ private int UpdateItemGets(RandomizerProperties props) accessibleMagicContainers = props.StartMagicContainers + magicContainerGetLocations.Count; requireables = GetRequireables(props); gottenItems = []; - foreach (Location location in AllLocations()) + foreach (Location location in AllLocations().Where(i => i.Reachable)) { List gettableItems = location.GetGettableItems(requireables); @@ -2075,7 +2008,6 @@ private async Task ProcessOverworld(Func progress, CancellationTok westHyrule.SetStart(); ShufflePalaces(); - LoadItemLocs(props.PalaceItemRoomCounts); ShuffleItems(); @@ -2192,8 +2124,9 @@ private bool AllContinentsAreConnected((Location, Location)[] connections) return continentsFound.Count == 4; } - //ItemLocs is specifically only those locations that contain shufflable items - //If you're looking for a global reference for which items are where... too bad it doesn't exist :( + //ItemLocs is no longer a global reference (and probably shouldn't have been to begin with). + //It's possible this makes a return at some point just for efficency but for now it's dead Jim + /* private List LoadItemLocs(int[] itemsPerPalaces) { List GetPalacesWithItems(IEnumerable palaces) => @@ -2275,6 +2208,7 @@ List GetPalacesWithItems(IEnumerable palaces) => return itemLocs; } + */ /// /// Shuffles which spells are in which towns. @@ -2347,6 +2281,7 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra shuffler.ShuffleBossDrop(rom, r, a); } + //neither of these starting item handlings really belong here, but this is good enough. if (props.StartWithSpellItems) { //ROMData.Put(0xF584, 0xA9); @@ -2359,6 +2294,14 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra rom.Put(0x17b17, 0x01); //Water rom.Put(0x17b18, 0x20); //Child } + rom.Put(RomMap.START_CANDLE, props.StartCandle ? (byte)1 : (byte)0); + rom.Put(RomMap.START_GLOVE, props.StartGlove ? (byte)1 : (byte)0); + rom.Put(RomMap.START_RAFT, props.StartRaft ? (byte)1 : (byte)0); + rom.Put(RomMap.START_BOOTS, props.StartBoots ? (byte)1 : (byte)0); + rom.Put(RomMap.START_FLUTE, props.StartFlute ? (byte)1 : (byte)0); + rom.Put(RomMap.START_CROSS, props.StartCross ? (byte)1 : (byte)0); + rom.Put(RomMap.START_HAMMER, props.StartHammer ? (byte)1 : (byte)0); + rom.Put(RomMap.START_MAGICAL_KEY, props.StartKey ? (byte)1 : (byte)0); rom.Put(ROM.ChrRomOffset + 0x1a000, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.item_sprites.chr")); //Linked fire/dash custom sprites replace fire's sprite. This lets us free up c7 for future use. @@ -3036,18 +2979,6 @@ public string GenerateSpoiler() //sb.AppendLine(); } - //sb.AppendLine("West:\n"); - //sb.AppendLine(westHyrule.GetReadableMap()); - - //sb.AppendLine("\nEast:\n"); - //sb.AppendLine(eastHyrule.GetReadableMap()); - - //sb.AppendLine("\nDeath Mountain:\n"); - //sb.AppendLine(deathMountain.GetReadableMap()); - - //sb.AppendLine("\nMaze Island:\n"); - //sb.AppendLine(mazeIsland.GetReadableMap()); - for(int i = 0; i < 6; i++) { sb.AppendLine($"\nPalace {i+1}:\n"); @@ -3141,19 +3072,31 @@ public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) sb.AppendLine("eh: " + eh + " / " + eastHyrule.AllLocations.Count); sb.AppendLine("dm: " + dm + " / " + deathMountain.AllLocations.Count); sb.AppendLine("mi: " + mi + " / " + mazeIsland.AllLocations.Count); - sb.AppendLine(""); + sb.AppendLine(); + sb.AppendLine("Missing Items:"); //Where(i => !i.ItemGet && !i.Collectable.IsInternalUse() - foreach (Location location in AllLocations()) + bool itemWasMissing = false; + foreach (Location location in AllLocations().Where(l => l.GetAllCollectables().Any(i => !i.IsMinorItem() && !ItemGet[i]))) { - foreach (Collectable collectable in location.GetAllCollectables().Where(i => !i.IsMinorItem() && !ItemGet[i])) + sb.AppendLine($"{location.Name} / [{String.Join(", ", location.GetAllCollectables() + .Where(i => !i.IsMinorItem() && !ItemGet[i]) + .Select(i => Enum.GetName(i)!))}]"); + itemWasMissing = true; + } + + if(!itemWasMissing) + { + sb.AppendLine("Missing Non-Item locations (this should have been caught by ValidateBasicRouting!"); + foreach (Location location in AllLocations().Where(l => !l.Reachable)) { - sb.AppendLine($"{location.Name} / [{String.Join(", ", location.GetAllCollectables().Select(i => Enum.GetName(i)!))}]"); + sb.AppendLine(location.Name); } } + sb.AppendLine(); + - sb.AppendLine(""); - logger.Error(sb.ToString()); +logger.Error(sb.ToString()); } private bool IsRaftAlwaysRequired(RandomizerProperties props) diff --git a/RandomizerCore/Overworld/EastHyrule.cs b/RandomizerCore/Overworld/EastHyrule.cs index a0c74b13d..b57430334 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -2004,9 +2004,9 @@ public override string GenerateSpoiler() StringBuilder sb = new(); sb.AppendLine("EAST: "); sb.AppendLine("Nabooru: \n" + nabooru.Town!.GenerateSpoiler()); - sb.AppendLine("Darunia: \n" + nabooru.Town!.GenerateSpoiler()); - sb.AppendLine("New Kasuto: \n" + nabooru.Town!.GenerateSpoiler()); - sb.AppendLine("Old Kasuto: \n" + nabooru.Town!.GenerateSpoiler()); + sb.AppendLine("Darunia: \n" + darunia.Town!.GenerateSpoiler()); + sb.AppendLine("New Kasuto: \n" + newKasuto.Town!.GenerateSpoiler()); + sb.AppendLine("Old Kasuto: \n" + oldKasuto.Town!.GenerateSpoiler()); sb.AppendLine("\tRisen Pbag Cave: " + pbagCave2.GetAllCollectables()[0].EnglishText()); sb.AppendLine("\tSunken Pbag Cave: " + pbagCave1.GetAllCollectables()[0].EnglishText()); diff --git a/RandomizerCore/Overworld/Location.cs b/RandomizerCore/Overworld/Location.cs index 142bd9b74..6ec3b596a 100644 --- a/RandomizerCore/Overworld/Location.cs +++ b/RandomizerCore/Overworld/Location.cs @@ -307,18 +307,21 @@ public List GetGettableItems(List requireables, Di return AccessRequirements.AreSatisfiedBy(requireables) ? NonPalaceTownCollectables : []; } - public void SetCollectables(IEnumerable collectables) + public void SetCollectables(IEnumerable collectables, bool shufflableOnly = false) { Debug.Assert(Town == null || Palace == null); if (Town != null) { - Town.SetCollectables(collectables); + Town.SetCollectables(collectables, shufflableOnly); } else if (Palace != null) { - Palace.SetCollectables(collectables); + Palace.SetCollectables(collectables, shufflableOnly); + } + else if (!shufflableOnly || CollectablesAreShufflable) + { + NonPalaceTownCollectables = [.. collectables]; } - else NonPalaceTownCollectables = [.. collectables]; } /// @@ -345,4 +348,17 @@ public bool ReplaceCollectable(Collectable toReplace, Collectable replacement) } return false; } + + public int GetCollectableCount(bool shufflableOnly = false) + { + if (Town != null) + { + return Town.TownMaps.Count(i => i.Collectable != null && (!shufflableOnly || i.CollectableIsShufflable)); + } + else if (Palace != null) + { + return Palace.ItemRooms.Count; + } + return NonPalaceTownCollectables.Count; + } } diff --git a/RandomizerCore/Overworld/LocationID.cs b/RandomizerCore/Overworld/LocationID.cs index 7636772ea..29489bcc2 100644 --- a/RandomizerCore/Overworld/LocationID.cs +++ b/RandomizerCore/Overworld/LocationID.cs @@ -50,7 +50,7 @@ public enum LocationID WEST_DM_ENTRANCE, // 42: (10, 95) WEST_DM_EXIT, // 43: (21, 96) WEST_KINGS_TOMB, // 44: (50, 88) - WEST_TOWN_RAURO, // 45: (46, 54) + WEST_TOWN_RAURU, // 45: (46, 54) WEST_TOWN_RUTO = 47, // 47: ( 2, 36) WEST_TOWN_SARIA_SOUTH, // 48: ( 8, 91) WEST_TOWN_SARIA_NORTH, // 49: ( 8, 89) @@ -279,7 +279,7 @@ public static class LocationIDExtensions { static readonly Dictionary townMap = new() { - [LocationID.WEST_TOWN_RAURO] = TownType.RAURU, + [LocationID.WEST_TOWN_RAURU] = TownType.RAURU, [LocationID.WEST_TOWN_RUTO] = TownType.RUTO, [LocationID.WEST_TOWN_SARIA_NORTH] = TownType.SARIA, [LocationID.WEST_TOWN_SARIA_SOUTH] = TownType.SARIA, diff --git a/RandomizerCore/Overworld/WestHyrule.cs b/RandomizerCore/Overworld/WestHyrule.cs index 56c5380d3..6e8fdf1a8 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -89,7 +89,7 @@ public sealed class WestHyrule : World //{ 0x4659, terrain.cave }, // Cave to DM //{ 0x465A, terrain.cave }, // Cave from DM { LocationID.WEST_KINGS_TOMB, Terrain.GRAVE }, //44: King's tomb - { LocationID.WEST_TOWN_RAURO, Terrain.TOWN }, + { LocationID.WEST_TOWN_RAURU, Terrain.TOWN }, { LocationID.WEST_TOWN_RUTO, Terrain.TOWN }, { LocationID.WEST_TOWN_SARIA_SOUTH, Terrain.TOWN }, { LocationID.WEST_TOWN_SARIA_NORTH, Terrain.TOWN }, @@ -130,7 +130,7 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), bagu = GetLocation(LocationID.WEST_BAGU_HOUSE); //0x4661 bagu.Town = Towns.LoadVanillaTown(rom, TownType.BAGU, props.DisableMagicRecs); - rauru = GetLocation(LocationID.WEST_TOWN_RAURO); + rauru = GetLocation(LocationID.WEST_TOWN_RAURU); rauru.Town = Towns.LoadVanillaTown(rom, TownType.RAURU, props.DisableMagicRecs); ruto = GetLocation(LocationID.WEST_TOWN_RUTO); //0x465e @@ -1409,6 +1409,8 @@ public bool ValidateBasicRouting() } visitedCoordinates[y, x] = true; //if there is a location at this coordinate + List locationsAtCoord = unreachedLocations.Where(location => location.Y == y && location.Xpos == x).ToList(); + Debug.Assert(locationsAtCoord.Count <= 1); Location? here = unreachedLocations.FirstOrDefault(location => location.Y == y && location.Xpos == x); if (here != null) { @@ -1510,11 +1512,11 @@ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("WEST: "); - sb.AppendLine("Rauru: " + rauru.Town!.GenerateSpoiler()); - sb.AppendLine("Ruto: " + ruto.Town!.GenerateSpoiler()); - sb.AppendLine("Saria: " + sariaNorth.Town!.GenerateSpoiler()); - sb.AppendLine("Mido: " + mido.Town!.GenerateSpoiler()); - sb.AppendLine("Bagu: " + bagu.Town!.GenerateSpoiler()); + sb.AppendLine("Rauru: \n" + rauru.Town!.GenerateSpoiler()); + sb.AppendLine("Ruto: \n" + ruto.Town!.GenerateSpoiler()); + sb.AppendLine("Saria: \n" + sariaNorth.Town!.GenerateSpoiler()); + sb.AppendLine("Mido: \n" + mido.Town!.GenerateSpoiler()); + sb.AppendLine("Bagu: \n" + bagu.Town!.GenerateSpoiler()); sb.AppendLine("\tMagic Container Cave: " + magicContainerCave.GetAllCollectables()[0].EnglishText()); sb.AppendLine("\tTrophy Cave: " + trophyCave.GetAllCollectables()[0].EnglishText()); diff --git a/RandomizerCore/Overworld/World.cs b/RandomizerCore/Overworld/World.cs index d9ef05d07..7e3999f0e 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -425,7 +425,7 @@ protected bool PlaceLocations(Terrain riverTerrain, bool saneCaves, Location? hi location.Y = y; location.CanShuffle = false; } - else if (location.Town == null) + else { Terrain t; do @@ -3178,4 +3178,16 @@ public bool ValidateCaves() //protected abstract void SetVanillaCollectables(bool useDash); public abstract string GenerateSpoiler(); + + public bool HasCollidingLocations() + { + foreach(Location location in AllLocations) + { + if(AllLocations.Any(i => location != i && location.CoordsY30Offset == i.CoordsY30Offset)) + { + return true; + } + } + return false; + } } \ No newline at end of file diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 5f94ff58b..0dda71918 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -16,39 +16,6 @@ namespace Z2Randomizer.RandomizerCore; -/* -Classes Needed: - -* Location - Represents entry from overworld - * Terrain type - * Requires Jump - * Requires Fairy - * Requires Hammer - * X Position - * Y Position - * Exit Location? - * Contains Item - * Enter from right - * Map Number - -* World - Represent a single map - * Location Tree - * Root of tree is starting position, children are areas directly accessible from position - * Lists of locations broken down by terrain - * Entry / Exit Points - -* Hyrule - * Does the randomization - * Checks for Sanity - * Contains links to all World objects - -* Room - Palace only? - -* Palace - * Pallette - * Enemies? - * Rooms -*/ public class ROM { public const int RomHdrSize = 0x10; @@ -576,29 +543,6 @@ public void ApplyIps(byte[] patch, bool expandRom = false) public void UpdateSprite(CharacterSprite charSprite, bool sanitize, bool changeItems) { - /* - * Dear future digshake, - * - * This method is garbage. - * - * The first entry in sprite info contains non-sprite data. - * My lazy ass just skips it. - * - * If you update the sprite structures in Graphics.cs, you could refactor this: - * - * Make lists of all the ROM locations for sprites - * Iterate through each sprite array, putting the data at the ROM locations - * - * -Past digshake (who is still too lazy to fix this method) - */ - /* - * Dear future Ellendar, - * This method is still garbage, but better. - * Eventually we need to create a data structure that represents the individual properies of things like sprites - * that we can save in external files so they can be imported from a simple format people can use to customize all - * the cosmetics. - */ - if (charSprite.Patch != null) { if (sanitize) { diff --git a/RandomizerCore/Sidescroll/Palace/Palace.cs b/RandomizerCore/Sidescroll/Palace/Palace.cs index 82859220f..66b6a6e5b 100644 --- a/RandomizerCore/Sidescroll/Palace/Palace.cs +++ b/RandomizerCore/Sidescroll/Palace/Palace.cs @@ -1191,8 +1191,9 @@ public List GetGettableItems(IEnumerable initialRe return gettableItems; } - public void SetCollectables(IEnumerable collectables) + public void SetCollectables(IEnumerable collectables, bool shufflableOnly = false) { + //Shufflable marking NYI for palaces (and probably not needed) so it's NOP for now Debug.Assert(collectables.Count() == ItemRooms.Count); int i = 0; foreach (Collectable collectable in collectables) diff --git a/RandomizerCore/Sidescroll/Town/Town.cs b/RandomizerCore/Sidescroll/Town/Town.cs index 6c127908c..b22533a11 100644 --- a/RandomizerCore/Sidescroll/Town/Town.cs +++ b/RandomizerCore/Sidescroll/Town/Town.cs @@ -150,9 +150,9 @@ public bool CanBeTraversed(List requireables) throw new ImpossibleException("Too many non-looping maps in town traversal check"); } - public void SetCollectables(IEnumerable collectables) + public void SetCollectables(IEnumerable collectables, bool collectableOnly = false) { - List collectableMaps = TownMaps.Where(i => i.Collectable != null).ToList(); + List collectableMaps = TownMaps.Where(i => i.Collectable != null && (!collectableOnly || i.CollectableIsShufflable)).ToList(); Debug.Assert(collectables.Count() == collectableMaps.Count); int i = 0; foreach(Collectable collectable in collectables) From d5ac2d27e5e6c80005adba37217a1fcfc975b48e Mon Sep 17 00:00:00 2001 From: Ellendar Date: Wed, 8 Jul 2026 14:58:31 -0400 Subject: [PATCH 09/10] Fixed a pile of item shuffle / hint generation issues. Included in this is a fix for an older bug that prevented a palace from having more than one of the same item. --- RandomizerCore/CustomTexts.cs | 35 ++--- RandomizerCore/Hyrule.cs | 142 +++++++++++--------- RandomizerCore/Overworld/EastHyrule.cs | 9 +- RandomizerCore/Overworld/MazeIsland.cs | 3 +- RandomizerCore/Overworld/WestHyrule.cs | 9 +- RandomizerCore/RandomizerConfiguration.cs | 2 + RandomizerCore/RandomizerProperties.cs | 2 +- RandomizerCore/Sidescroll/Palace/Palace.cs | 39 +++--- RandomizerCore/Sidescroll/Palace/Palaces.cs | 34 +++-- 9 files changed, 151 insertions(+), 124 deletions(-) diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index a1804f790..192d7323a 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -1,8 +1,9 @@ -using System; +using DynamicData; +using NLog; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using NLog; using Z2Randomizer.RandomizerCore.Overworld; using Z2Randomizer.RandomizerCore.Sidescroll.Town; @@ -396,7 +397,8 @@ public class CustomTexts { TownType.NABOORU, ["Do not$drink the$fountain$water here$...$trust me."] }, { TownType.DARUNIA, ["You saved$a kid$for this?", "Dont$forget to$get upstab"] }, { TownType.NEW_KASUTO, [] }, - { TownType.OLD_KASUTO, ["Sorry$about the$moas"] } + { TownType.OLD_KASUTO, ["Sorry$about the$moas"] }, + { TownType.BAGU, BAGU_TEXTS } }; public static readonly Dictionary WIZARD_SPELL_TEXTS_BY_COLLECTABLE = new() @@ -571,8 +573,6 @@ public static List GenerateTexts( if (props.SpellItemHints && props.IncludeSwordTechsInShuffle) { - var downstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.DOWNSTAB)); - var upstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.UPSTAB)); Text hint; if (props.StartWithDownstab) { @@ -581,22 +581,25 @@ public static List GenerateTexts( } else { + var downstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.DOWNSTAB)); hint = Text.GenerateHelpfulHint(downstabLoc, Collectable.DOWNSTAB, props.IncludeSpellsInShuffle); } texts[downstabClosedDoorTextIndex] = hint; - if (upstabLoc == null) + if (props.StartWithUpstab) { - hint = props.UseCommunityText ? new Text(STARTED_WITH_ITEM_TEXTS.Sample(nonhashRNG)!, Collectable.DOWNSTAB) - : new Text(NON_COMMUNITY_STARTED_WITH_TEXT, Collectable.DOWNSTAB); + hint = props.UseCommunityText ? new Text(STARTED_WITH_ITEM_TEXTS.Sample(nonhashRNG)!, Collectable.UPSTAB) + : new Text(NON_COMMUNITY_STARTED_WITH_TEXT, Collectable.UPSTAB); } else { + var upstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.UPSTAB)); hint = Text.GenerateHelpfulHint(upstabLoc, Collectable.UPSTAB, props.IncludeSpellsInShuffle); } texts[upstabClosedDoorTextIndex] = hint; if (props.SwapUpAndDownStab) { - (texts[upstabClosedDoorTextIndex], texts[downstabClosedDoorTextIndex]) = (texts[downstabClosedDoorTextIndex], texts[upstabClosedDoorTextIndex]); + (texts[upstabClosedDoorTextIndex], texts[downstabClosedDoorTextIndex]) + = (texts[downstabClosedDoorTextIndex], texts[upstabClosedDoorTextIndex]); } } @@ -717,9 +720,11 @@ private static Text GenerateWizardText(List texts, Random r, Location loca //If it's a spell, use the old behavior if(useCommunityText) { - List possibleWizardHints = GENERIC_WIZARD_TEXTS - .Union(WIZARD_SPELL_TEXTS_BY_TOWN[town]) - .Union(WIZARD_SPELL_TEXTS_BY_COLLECTABLE[collectable]).ToList(); + List possibleWizardHints = WIZARD_SPELL_TEXTS_BY_TOWN[town].Union(WIZARD_SPELL_TEXTS_BY_COLLECTABLE[collectable]).ToList(); + if(town != TownType.BAGU) + { + possibleWizardHints.Add(GENERIC_WIZARD_TEXTS); + } int selectedHintIndex = r.Next(possibleWizardHints.Count()); return new Text(possibleWizardHints[selectedHintIndex]); } @@ -859,7 +864,7 @@ private static List GenerateHelpfulHints(List hints, IEnumerable placedIndex = []; - List placedItems = []; + List placedItems = []; List placedTowns = []; @@ -908,7 +913,7 @@ private static List GenerateHelpfulHints(List hints, IEnumerable GenerateHelpfulHints(List hints, IEnumerable Randomize(byte[] vanillaRomData, RandomizerC { List? customSpellOrder = props.IncludeSpellsInShuffle ? null - : AllLocations() + : ItemLocations() .Where(l => l.Town?.GetWizard() != null && l != westHyrule.bagu && l != westHyrule.sariaSouth) .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); ROMData.CombineFireSpell(assembler, customSpellOrder, r); @@ -396,7 +396,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC if (ct.IsCancellationRequested) { return new RandomizerResult(false); } UpdateProgress(progress, 9); - List texts = CustomTexts.GenerateTexts(AllLocations(), ROMData.GetGameText(), props, r); + List texts = CustomTexts.GenerateTexts(ItemLocations(), ROMData.GetGameText(), props, r); StatRandomizer randomizedStats = new(ROMData, props); randomizedStats.Randomize(r, skipDifficultyOnly: shareSeedAcrossDifficulty); @@ -645,7 +645,7 @@ private void ReplaceDifficultyStartingItemsInWorld(Random r) { if (!props.StartsWithCollectable(item)) { continue; } Collectable replacementItem = minorItems.Sample(r); - foreach (Location location in AllLocations().Where(i => i.GetCollectableCount() > 0)) + foreach (Location location in ItemLocations()) { if(location.ReplaceCollectable(item, replacementItem)) { @@ -676,8 +676,16 @@ private void ShufflePalaces() private void ShuffleItems() { - List shufflableItems = []; - List globalShuffleLocations = []; + List possibleItemLocations = ItemLocations().ToList(); + IEnumerable palaceLocations = AllLocations().Where(i => i.Palace != null); + + //Non-pbag, non-wizard, non-palace items are inherently in the global shuffle pool + List shufflableItems = [Collectable.TROPHY, Collectable.MEDICINE, Collectable.HEART_CONTAINER, Collectable.MAGIC_CONTAINER, + Collectable.HEART_CONTAINER, Collectable.HAMMER, Collectable.MAGIC_CONTAINER, Collectable.HEART_CONTAINER, Collectable.HEART_CONTAINER, + Collectable.MAGIC_CONTAINER, Collectable.MAGIC_KEY, Collectable.MAGIC_CONTAINER, Collectable.CHILD]; + List globalShuffleLocations = [westHyrule.trophyCave, westHyrule.medicineCave, westHyrule.heartContainerCave, westHyrule.magicContainerCave, + westHyrule.grassTile, deathMountain.hammerCave, deathMountain.specRock, eastHyrule.desertTile, eastHyrule.waterTile, eastHyrule.newKasuto, + mazeIsland.childDrop, mazeIsland.magicContainerDrop]; List minorItems = [Collectable.BLUE_JAR, Collectable.RED_JAR, Collectable.SMALL_BAG, Collectable.MEDIUM_BAG, Collectable.LARGE_BAG, Collectable.XL_BAG, Collectable.ONEUP, Collectable.KEY]; @@ -747,14 +755,13 @@ private void ShuffleItems() int heartContainersInItemPool = props.MaxHearts - props.StartHearts; int magicContainersInItemPool = props.MaxMagicContainers - props.StartMagicContainers; - foreach (Collectable collectable in ItemGet.Keys) { ItemGet[collectable] = props.StartsWithCollectable(collectable); } pbagHearts = []; - List heartContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.HEART_CONTAINER)).ToList(); + List heartContainerLocations = possibleItemLocations.Where(i => i.GetAllCollectables().Contains(Collectable.HEART_CONTAINER)).ToList(); //Replace any unused heart containers with small items if (heartContainersInItemPool < 4) @@ -773,6 +780,8 @@ private void ShuffleItems() collectables[index] = minorItems.Sample(r); hcLocation.SetCollectables(collectables); heartContainersToRemove--; + shufflableItems.Remove(Collectable.HEART_CONTAINER); + shufflableItems.Add(collectables[index]); if (!collectables.Any(i => i == Collectable.HEART_CONTAINER)) { heartContainerLocations.Remove(hcLocation); @@ -783,7 +792,7 @@ private void ShuffleItems() } } - List magicContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.MAGIC_CONTAINER)).ToList(); + List magicContainerLocations = possibleItemLocations.Where(i => i.GetAllCollectables().Contains(Collectable.MAGIC_CONTAINER)).ToList(); //Replace any unused magic containers with small items if (magicContainersInItemPool < 4) { @@ -801,6 +810,8 @@ private void ShuffleItems() collectables[index] = minorItems.Sample(r); mcLocation.SetCollectables(collectables); magicContainersToRemove--; + shufflableItems.Remove(Collectable.MAGIC_CONTAINER); + shufflableItems.Add(collectables[index]); if (!collectables.Any(i => i == Collectable.MAGIC_CONTAINER)) { magicContainerLocations.Remove(mcLocation); @@ -822,6 +833,11 @@ private void ShuffleItems() { shufflableItems[index] = minorItems.Sample(r); } + else + { + Location? defaultLocation = possibleItemLocations.FirstOrDefault(i => i.GetAllCollectables().Contains(spellItem)); + defaultLocation?.ReplaceCollectable(spellItem, minorItems.Sample(r)); + } ItemGet[spellItem] = true; } } @@ -901,12 +917,6 @@ private void ShuffleItems() excessItems.Add(Collectable.MAGIC_CONTAINER); } - int extraPalaceItemCount = props.PalaceItemRoomCounts.Sum(c => Math.Max(c - 1, 0)); - for (int i = 0; i < extraPalaceItemCount; i++) - { - shufflableItems.Add(minorItems.Sample(r)); - } - List vanillaPalaceItems = [Collectable.CANDLE, Collectable.GLOVE, Collectable.RAFT, Collectable.BOOTS, Collectable.FLUTE, Collectable.CROSS]; //No palace items handling @@ -915,17 +925,12 @@ private void ShuffleItems() if (props.PalaceItemRoomCounts[i] == 0) { var vanillaPalaceItem = vanillaPalaceItems[i]; - if (props.StartsWithCollectable(vanillaPalaceItem)) - { - // since the palace item is replaced by a minor item, remove a minor item from the shuffle - var firstMinorItemIndex = shufflableItems.FindIndex(c => c.IsMinorItem()); - Debug.Assert(firstMinorItemIndex != -1); - shufflableItems.RemoveAt(firstMinorItemIndex); - } - else + + //If we didn't start with the item that should have been here, it still has to be somewhere, + //so add it to the excess items and it will eventually replace a minor item + if (!props.StartsWithCollectable(vanillaPalaceItem)) { excessItems.Add(vanillaPalaceItem); - shufflableItems.Remove(vanillaPalaceItem); } } } @@ -979,7 +984,12 @@ private void ShuffleItems() if (props.MixOverworldPalaceItems) { - + //This should probably always be true but safety + if(props.ShufflePalaceItems) + { + shufflableItems.Add(palaceLocations.SelectMany(i => i.GetAllCollectables())); + globalShuffleLocations.Add(palaceLocations); + } duplicateItemPlacementCandidates.AddRange(globalShuffleLocations); DoShuffle(shufflableItems, globalShuffleLocations); } @@ -992,37 +1002,21 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location palaceLocation in AllLocations().Where(i => i.Palace != null)) + foreach (Location palaceLocation in globalShuffleLocations.Where(i => i.Palace != null)) { shufflableItemLocations.Add(palaceLocation); - Collectable vanillaCollectable = palaceLocation.Palace!.GetVanillaCollectable(); - itemsToActuallyShuffle.Add(props.StartsWithCollectable(vanillaCollectable) ? minorItems.Sample(r) : vanillaCollectable); - - for (int i = 1; i < props.PalaceItemRoomCounts[(int)palaceLocation.Palace.Number! - 1]; i++) - { - itemsToActuallyShuffle.Add(minorItems.Sample(r)); - } + itemsToActuallyShuffle.Add(palaceLocation.GetAllCollectables()); } duplicateItemPlacementCandidates.AddRange(shufflableItemLocations); DoShuffle(itemsToActuallyShuffle, shufflableItemLocations); } - else //no shuffle palace items + else //vanilla palace items { - foreach (Location palaceLocation in AllLocations().Where(i => i.Palace != null && i.Palace.Number < 7)) + foreach (Location palaceLocation in possibleItemLocations.Where(i => i.Palace != null && i.Palace.Number < 7)) { int palaceNumber = palaceLocation.Palace!.Number; - Collectable vanillaCollectable = palaceLocation.Palace!.GetVanillaCollectable(); - List collectables = []; - collectables = [shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)]; - for (int i = 0; i < props.PalaceItemRoomCounts[(int)palaceNumber! - 1]; i++) - { - if (palaces[(int)palaceNumber! - 1].ItemRooms[i].Collectable == null) - { - Collectable smallItem = minorItems.Sample(r); - collectables.Add(smallItem); - } - } - palaceLocation.SetCollectables(collectables); + List vanillaCollectables = palaceLocation.Palace!.GetVanillaCollectables(palaceLocation.Palace!.ItemRooms.Count); + palaceLocation.SetCollectables(vanillaCollectables); } } @@ -1030,10 +1024,10 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location nonPalaceLocation in AllLocations().Where(i => i.Palace == null)) + foreach (Location nonPalaceLocation in possibleItemLocations.Where(i => i.Palace == null)) { shufflableItemLocations.Add(nonPalaceLocation); - foreach(Collectable collectable in nonPalaceLocation.GetAllCollectables()) + foreach(Collectable collectable in nonPalaceLocation.GetShufflableCollectables()) { itemsToActuallyShuffle.Add(shufflableItems.Contains(collectable) ? collectable : minorItems.Sample(r)); } @@ -1043,7 +1037,7 @@ private void ShuffleItems() } else { - foreach (Location nonPalaceLocation in AllLocations().Where(i => i.Palace == null)) + foreach (Location nonPalaceLocation in possibleItemLocations.Where(i => i.Palace == null)) { List collectables = nonPalaceLocation.GetAllCollectables(); for(int i = 0; i < collectables.Count; i++) @@ -1056,11 +1050,12 @@ private void ShuffleItems() } //Assigning unshuffled locations can make the number of containers wrong, so re-adjust them - heartContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.HEART_CONTAINER)).ToList(); + heartContainerLocations = possibleItemLocations.Where(i => i.GetAllCollectables().Contains(Collectable.HEART_CONTAINER)).ToList(); int heartContainerCount = heartContainerLocations.SelectMany(i => i.GetAllCollectables()).Count(i => i == Collectable.HEART_CONTAINER); while(heartContainerCount > heartContainersInItemPool) { Location location = heartContainerLocations.Sample(r)!; + Debug.Assert(location != null); Collectable minorItem = minorItems.Sample(r); if(!location.ReplaceCollectable(Collectable.HEART_CONTAINER, minorItem)) { @@ -1068,7 +1063,7 @@ private void ShuffleItems() } } - List minorItemLocations = AllLocations().Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); + List minorItemLocations = possibleItemLocations.Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); while (heartContainerCount < heartContainersInItemPool) { @@ -1086,19 +1081,25 @@ private void ShuffleItems() heartContainerCount++; } - magicContainerLocations = AllLocations().Where(i => i.GetAllCollectables().Contains(Collectable.MAGIC_CONTAINER)).ToList(); + magicContainerLocations = possibleItemLocations.Where(i => i.GetAllCollectables().Contains(Collectable.MAGIC_CONTAINER)).ToList(); int magicContainerCount = magicContainerLocations.SelectMany(i => i.GetAllCollectables()).Count(i => i == Collectable.MAGIC_CONTAINER); while (magicContainerCount > magicContainersInItemPool) { - Location location = magicContainerLocations.Sample(r)!; - Collectable minorItem = minorItems.Sample(r); - if (!location.ReplaceCollectable(Collectable.MAGIC_CONTAINER, minorItem)) + Location location = minorItemLocations.Sample(r)!; + Collectable[] locationMinorItems = location.GetAllCollectables().Where(i => i.IsMinorItem()).ToArray(); + if (locationMinorItems.Length == 0) + { + minorItemLocations.Remove(location); + continue; + } + if (!location.ReplaceCollectable(locationMinorItems.Sample(r), Collectable.MAGIC_CONTAINER)) { magicContainerLocations.Remove(location); } + magicContainerCount++; } - minorItemLocations = AllLocations().Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); + minorItemLocations = possibleItemLocations.Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); while (magicContainerCount < magicContainersInItemPool) { @@ -1216,19 +1217,20 @@ private void DoShuffle(List itemsToShuffle, List itemShuf if (itemsToShuffle.Count != possibleItemPlacements) { + itemShuffleLocations.ForEach(i => Debug.WriteLine($"{i.Name} - {i.GetShufflableCollectables().Count}")); + itemsToShuffle.ForEach(i => Debug.WriteLine($"{i}")); throw new Exception("Item locations must match number of items"); } itemsToShuffle.FisherYatesShuffle(r); - int itemIndex = 0; foreach(Location location in itemShuffleLocations) { int collectableCount = location.GetCollectableCount(true); List newCollectables = itemsToShuffle.GetRange(itemIndex, collectableCount); itemIndex += collectableCount; - location.SetCollectables(newCollectables); + location.SetCollectables(newCollectables, true); } } @@ -1456,7 +1458,7 @@ private bool IsEverythingReachable(Dictionary itemGet) //Debug.WriteLine(GenerateSpoiler()); //Debug.WriteLine(westHyrule.GetMapDebug()); //XXX: - return false; + return palaces[2].ItemRooms[0].Collectable == Collectable.GLOVE; } //#endif return false; @@ -1564,7 +1566,7 @@ private int UpdateItemGets(RandomizerProperties props) accessibleHeartContainers = props.StartHearts; Location newKasuto = eastHyrule.newKasuto; List gottenItems = [], lastIterationGottenItems = []; - List locations = AllLocations().ToList(); + List locations = ItemLocations().ToList(); List heartContainerGetLocations = [], magicContainerGetLocations = []; bool stalled = false; //No more delayed evaluation because too many locations could give you containers or require containers. @@ -1575,7 +1577,7 @@ private int UpdateItemGets(RandomizerProperties props) accessibleMagicContainers = props.StartMagicContainers + magicContainerGetLocations.Count; requireables = GetRequireables(props); gottenItems = []; - foreach (Location location in AllLocations().Where(i => i.Reachable)) + foreach (Location location in ItemLocations().Where(i => i.Reachable)) { List gettableItems = location.GetGettableItems(requireables); @@ -2686,7 +2688,7 @@ private void UpdateRom() else { // Use the old spell menu behavior where the spells are placed in the menu in the location it came from - var wizardCollectables = AllLocations() + var wizardCollectables = ItemLocations() .Where(l => l.Town?.GetWizard() != null && l != westHyrule.sariaSouth && l != westHyrule.bagu) .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); @@ -3063,6 +3065,14 @@ public IEnumerable AllLocations() return locations ?? []; } + public IEnumerable ItemLocations() + { + List possibleItemLocations = AllLocations().Where(i => i.GetCollectableCount() > 0).ToList(); + possibleItemLocations.Remove(westHyrule.sariaSouth); + return possibleItemLocations; + } + + public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) { StringBuilder sb = new StringBuilder(); @@ -3077,18 +3087,18 @@ public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) sb.AppendLine("Missing Items:"); //Where(i => !i.ItemGet && !i.Collectable.IsInternalUse() bool itemWasMissing = false; - foreach (Location location in AllLocations().Where(l => l.GetAllCollectables().Any(i => !i.IsMinorItem() && !ItemGet[i]))) + foreach (Location location in ItemLocations().Where(l => l.GetAllCollectables().Any(i => !i.IsMinorItem() && !ItemGet[i]))) { sb.AppendLine($"{location.Name} / [{String.Join(", ", location.GetAllCollectables() .Where(i => !i.IsMinorItem() && !ItemGet[i]) - .Select(i => Enum.GetName(i)!))}]"); + .Select(i => Enum.GetName(i)))}]"); itemWasMissing = true; } if(!itemWasMissing) { sb.AppendLine("Missing Non-Item locations (this should have been caught by ValidateBasicRouting!"); - foreach (Location location in AllLocations().Where(l => !l.Reachable)) + foreach (Location location in ItemLocations().Where(l => !l.Reachable)) { sb.AppendLine(location.Name); } diff --git a/RandomizerCore/Overworld/EastHyrule.cs b/RandomizerCore/Overworld/EastHyrule.cs index b57430334..7c43b9911 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -2110,8 +2110,11 @@ public override void ResetCollectables(RandomizerProperties props) pbagCave2.CollectablesAreShufflable = props.ShuffleOverworldItems && props.PbagItemShuffle; - locationAtPalace5.SetCollectables(locationAtPalace5.Palace!.GetVanillaCollectables()); - locationAtPalace6.SetCollectables(locationAtPalace6.Palace!.GetVanillaCollectables()); - locationAtGP.SetCollectables(locationAtGP.Palace!.GetVanillaCollectables()); + locationAtPalace5.SetCollectables(locationAtPalace5.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtPalace5.Palace!.Number - 1])); + locationAtPalace6.SetCollectables(locationAtPalace6.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtPalace6.Palace!.Number - 1])); + locationAtGP.SetCollectables(locationAtGP.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtGP.Palace!.Number - 1])); } } \ No newline at end of file diff --git a/RandomizerCore/Overworld/MazeIsland.cs b/RandomizerCore/Overworld/MazeIsland.cs index 8f9934706..9c5d40137 100644 --- a/RandomizerCore/Overworld/MazeIsland.cs +++ b/RandomizerCore/Overworld/MazeIsland.cs @@ -1056,6 +1056,7 @@ public override void ResetCollectables(RandomizerProperties props) magicContainerDrop.SetCollectables([Collectable.MAGIC_CONTAINER]); magicContainerDrop.CollectablesAreShufflable = props.ShuffleOverworldItems; - locationAtPalace4.SetCollectables(locationAtPalace4.Palace!.GetVanillaCollectables()); + locationAtPalace4.SetCollectables(locationAtPalace4.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtPalace4.Palace!.Number - 1])); } } diff --git a/RandomizerCore/Overworld/WestHyrule.cs b/RandomizerCore/Overworld/WestHyrule.cs index 6e8fdf1a8..411f60bf4 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -1603,8 +1603,11 @@ public override void ResetCollectables(RandomizerProperties props) grassTile.SetCollectables([Collectable.HEART_CONTAINER]); grassTile.CollectablesAreShufflable = props.ShuffleOverworldItems; - locationAtPalace1.SetCollectables(locationAtPalace1.Palace!.GetVanillaCollectables()); - locationAtPalace2.SetCollectables(locationAtPalace2.Palace!.GetVanillaCollectables()); - locationAtPalace3.SetCollectables(locationAtPalace3.Palace!.GetVanillaCollectables()); + locationAtPalace1.SetCollectables(locationAtPalace1.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtPalace1.Palace!.Number - 1])); + locationAtPalace2.SetCollectables(locationAtPalace2.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtPalace2.Palace!.Number - 1])); + locationAtPalace3.SetCollectables(locationAtPalace3.Palace! + .GetVanillaCollectables(props.PalaceItemRoomCounts[locationAtPalace3.Palace!.Number - 1])); } } diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index b062ff65b..94a561991 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -1642,6 +1642,8 @@ public void AssignPalaceItemCounts(RandomizerProperties properties, Random r) } } } + //for ease of some calculations, add a count in for GP that is always 0 + properties.PalaceItemRoomCounts[6] = 0; } /// Note: this value can be limited by other things, like the style being Vanilla diff --git a/RandomizerCore/RandomizerProperties.cs b/RandomizerCore/RandomizerProperties.cs index 866aadc62..15803d8da 100644 --- a/RandomizerCore/RandomizerProperties.cs +++ b/RandomizerCore/RandomizerProperties.cs @@ -135,7 +135,7 @@ public class RandomizerProperties public bool HardBosses { get; set; } public bool AggressiveTbird { get; set; } [NotMapped] - public int[] PalaceItemRoomCounts { get; set; } = new int[6]; + public int[] PalaceItemRoomCounts { get; set; } = new int[7]; public bool UsePalaceItemRoomCountIndicator { get; set; } public bool RevealWalkthroughWalls { get; set; } diff --git a/RandomizerCore/Sidescroll/Palace/Palace.cs b/RandomizerCore/Sidescroll/Palace/Palace.cs index 66b6a6e5b..4a2d02ebd 100644 --- a/RandomizerCore/Sidescroll/Palace/Palace.cs +++ b/RandomizerCore/Sidescroll/Palace/Palace.cs @@ -1146,6 +1146,11 @@ public List GetGettableItems(IEnumerable initialRe while (pendingRooms.Count > 0) { Room room = pendingRooms.First(); + if (coveredRooms.Contains(room)) + { + pendingRooms.Remove(room); + continue; + } if (ItemRooms.Contains(room)) { if (room.Collectable == null) @@ -1153,20 +1158,12 @@ public List GetGettableItems(IEnumerable initialRe throw new Exception("Unable to assess gettability of item room with no item"); } Collectable roomCollectable = room.Collectable ?? Collectable.DO_NOT_USE; - if(!gettableItems.Contains(roomCollectable)) + gettableItems.Add(roomCollectable); + if (roomCollectable.AsRequirement() != null) { - gettableItems.Add(roomCollectable); - if (roomCollectable.AsRequirement() != null) - { - requireables.Add(roomCollectable.AsRequirement() ?? RequirementType.JUMP); - } + requireables.Add(roomCollectable.AsRequirement() ?? RequirementType.JUMP); } } - if (coveredRooms.Contains(room)) - { - pendingRooms.Remove(room); - continue; - } coveredRooms.Add(room); pendingRooms.Remove(room); if (room.Left != null && room.Left.IsTraversable(requireables)) @@ -1376,17 +1373,12 @@ public void ReplaceRoom(Room roomToReplace, Room newRoom) AllRooms.Add(newRoom); } - public Collectable GetVanillaCollectable() + public List GetVanillaCollectables(int itemCount) { - return Number switch + if(itemCount == 0) { - 7 => Collectable.DO_NOT_USE, - _ => GetVanillaCollectables()[0] - }; - } - - public List GetVanillaCollectables() - { + return []; + } List[] vanillaCollectables = [ [Collectable.CANDLE], [Collectable.GLOVE], @@ -1400,7 +1392,12 @@ public List GetVanillaCollectables() { throw new Exception("Invalid palace number"); } - return vanillaCollectables[Number - 1]; + List collectables = vanillaCollectables[Number - 1]; + for(int i = 1; i < itemCount; i++) + { + collectables.Add(Collectable.LARGE_BAG); + } + return collectables; } public RoomExitType GetCoordinateRoomShape(int x, int y) diff --git a/RandomizerCore/Sidescroll/Palace/Palaces.cs b/RandomizerCore/Sidescroll/Palace/Palaces.cs index 54516b7c9..256af7050 100644 --- a/RandomizerCore/Sidescroll/Palace/Palaces.cs +++ b/RandomizerCore/Sidescroll/Palace/Palaces.cs @@ -294,7 +294,7 @@ private static bool ValidatePalaces(RandomizerProperties props, bool raftIsRequi return false; } } - return CanGetGlove(props, palaces[1]) + return CanGetGlove(props, palaces) && CanGetRaft(props, raftIsRequired, palaces[1], palaces[2]) && AtLeastOnePalaceCanHaveGlove(props, palaces); } @@ -326,29 +326,35 @@ private static bool AtLeastOnePalaceCanHaveGlove(RandomizerProperties props, Lis return false; } - private static bool CanGetGlove(RandomizerProperties props, Palace palace2) + private static bool CanGetGlove(RandomizerProperties props, IEnumerable palaces) { - if (!props.ShufflePalaceItems) + List requireables = [.. RequirementTypeExtensions.UpToXContainers(props.StartMagicContainers)]; + //If shuffle overworld items is on, we assume you can get all the items / spells + //as all progression items will eventually shuffle into spots that work + if (props.ShuffleOverworldItems) { - List requireables = [..RequirementTypeExtensions.UpToXContainers(props.StartMagicContainers)]; - //If shuffle overworld items is on, we assume you can get all the items / spells - //as all progression items will eventually shuffle into spots that work - if (props.ShuffleOverworldItems) - { - requireables = [RequirementType.KEY, ..RequirementTypeExtensions.UpToXContainers(8)]; + requireables = [RequirementType.KEY, .. RequirementTypeExtensions.UpToXContainers(8)]; + } + //Otherwise if it's vanilla items we can't get the magic key, because we could need glove for boots for flute to get to new kasuto + requireables.Add(props.SwapUpAndDownStab ? RequirementType.UPSTAB : RequirementType.DOWNSTAB); + requireables.Add(RequirementType.JUMP); + requireables.Add(RequirementType.FAIRY); - } - //Otherwise if it's vanilla items we can't get the magic key, because we could need glove for boots for flute to get to new kasuto - requireables.Add(props.SwapUpAndDownStab ? RequirementType.UPSTAB : RequirementType.DOWNSTAB); - requireables.Add(RequirementType.JUMP); - requireables.Add(RequirementType.FAIRY); + if (!props.ShufflePalaceItems) + { //If we can't clear 2 without the items available, we can never get the glove, so the palace is unbeatable + Palace palace2 = palaces.FirstOrDefault(i => i.Number == 2)!; if (!palace2.CanReachAnItemRoom(requireables)) { return false; } } + else if(!props.MixOverworldPalaceItems) + { + //If palace items are shuffled, but not mixed, we have to be able to get to at least one item room without the glove + return palaces.Any(palace => palace.CanReachAnItemRoom(requireables)); + } return true; } From e2956296a06914e06c4b068f979e5216d95bf429 Mon Sep 17 00:00:00 2001 From: Ellendar Date: Wed, 15 Jul 2026 20:45:23 -0400 Subject: [PATCH 10/10] Fixed a bunch more issues with shuffles / town routing --- RandomizerCore/Hyrule.cs | 58 +++++++++++++++-------- RandomizerCore/Overworld/Location.cs | 2 +- RandomizerCore/RandomizerConfiguration.cs | 2 +- RandomizerCore/Sidescroll/Town/Towns.cs | 17 +++++++ Statistics/Statistics.cs | 4 +- 5 files changed, 58 insertions(+), 25 deletions(-) diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 6b7863344..28bf7338d 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -106,6 +106,7 @@ public class Hyrule //private Character character; public Dictionary ItemGet { get; set; } + private List spellListOrder { get; set; } //private bool[] spellGet; public WestHyrule westHyrule; @@ -360,12 +361,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC if (props.CombineFire) { - List? customSpellOrder = props.IncludeSpellsInShuffle - ? null - : ItemLocations() - .Where(l => l.Town?.GetWizard() != null && l != westHyrule.bagu && l != westHyrule.sariaSouth) - .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); - ROMData.CombineFireSpell(assembler, customSpellOrder, r); + ROMData.CombineFireSpell(assembler, spellListOrder, r); } if (props.StartsWithCollectable(westHyrule.ruto.Town?.GetWizard()?.Collectable)) @@ -679,6 +675,10 @@ private void ShuffleItems() List possibleItemLocations = ItemLocations().ToList(); IEnumerable palaceLocations = AllLocations().Where(i => i.Palace != null); + spellListOrder = [Collectable.SHIELD_SPELL, Collectable.JUMP_SPELL, Collectable.LIFE_SPELL, Collectable.FAIRY_SPELL, + props.ReplaceFireWithDash ? Collectable.DASH_SPELL : Collectable.FIRE_SPELL, Collectable.SPELL_SPELL, Collectable.REFLECT_SPELL, + Collectable.THUNDER_SPELL]; + //Non-pbag, non-wizard, non-palace items are inherently in the global shuffle pool List shufflableItems = [Collectable.TROPHY, Collectable.MEDICINE, Collectable.HEART_CONTAINER, Collectable.MAGIC_CONTAINER, Collectable.HEART_CONTAINER, Collectable.HAMMER, Collectable.MAGIC_CONTAINER, Collectable.HEART_CONTAINER, Collectable.HEART_CONTAINER, @@ -712,6 +712,8 @@ private void ShuffleItems() eastHyrule.pbagCave2.SetCollectables([Collectable.XL_BAG]); } + //For now this is just for classic spell shuffle. Maybe this is extended later + bool canReplaceStartingSpellsWithMinorItems = !props.ShuffleSpellLocations; if (props.IncludeSpellsInShuffle) { shufflableItems.Add(Collectable.SHIELD_SPELL); @@ -728,8 +730,13 @@ private void ShuffleItems() else if (props.ShuffleSpellLocations) { ShuffleSpells(); + spellListOrder = possibleItemLocations.Where(i => i.Town != null && i?.Town?.Type != TownType.BAGU) + .OrderBy(i => (int)i.Town!.Type!) + .Select(i => (Collectable)i.Town!.GetWizard()!.Collectable!) + .ToList(); } + if (props.IncludeQuestItemsInShuffle) { shufflableItems.Add(Collectable.BAGUS_NOTE); @@ -882,7 +889,7 @@ private void ShuffleItems() foreach (Collectable item in possibleStartItems) { - if (props.StartsWithCollectable(item)) + if (props.StartsWithCollectable(item) && (canReplaceStartingSpellsWithMinorItems || !item.IsSpell())) { int index = shufflableItems.IndexOf(item); if (index > -1) @@ -1002,7 +1009,7 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location palaceLocation in globalShuffleLocations.Where(i => i.Palace != null)) + foreach (Location palaceLocation in possibleItemLocations.Where(i => i.Palace != null)) { shufflableItemLocations.Add(palaceLocation); itemsToActuallyShuffle.Add(palaceLocation.GetAllCollectables()); @@ -1015,8 +1022,13 @@ private void ShuffleItems() foreach (Location palaceLocation in possibleItemLocations.Where(i => i.Palace != null && i.Palace.Number < 7)) { int palaceNumber = palaceLocation.Palace!.Number; - List vanillaCollectables = palaceLocation.Palace!.GetVanillaCollectables(palaceLocation.Palace!.ItemRooms.Count); - palaceLocation.SetCollectables(vanillaCollectables); + List collectables = palaceLocation.Palace!.GetVanillaCollectables(palaceLocation.Palace!.ItemRooms.Count); + + for (int i = 0; i < collectables.Count; i++) + { + collectables[i] = props.StartsWithCollectable(collectables[i]) ? minorItems.Sample(r) : collectables[i]; + } + palaceLocation.SetCollectables(collectables); } } @@ -1029,7 +1041,9 @@ private void ShuffleItems() shufflableItemLocations.Add(nonPalaceLocation); foreach(Collectable collectable in nonPalaceLocation.GetShufflableCollectables()) { - itemsToActuallyShuffle.Add(shufflableItems.Contains(collectable) ? collectable : minorItems.Sample(r)); + itemsToActuallyShuffle.Add( + props.StartsWithCollectable(collectable) && (!collectable.IsSpell() || canReplaceStartingSpellsWithMinorItems) + ? minorItems.Sample(r) : collectable); } } duplicateItemPlacementCandidates.AddRange(shufflableItemLocations); @@ -1042,7 +1056,7 @@ private void ShuffleItems() List collectables = nonPalaceLocation.GetAllCollectables(); for(int i = 0; i < collectables.Count; i++) { - collectables[i] = shufflableItems.Contains(collectables[i]) ? collectables[i] : minorItems.Sample(r); + collectables[i] = props.StartsWithCollectable(collectables[i]) ? minorItems.Sample(r) : collectables[i]; } nonPalaceLocation.SetCollectables(collectables); } @@ -1458,7 +1472,7 @@ private bool IsEverythingReachable(Dictionary itemGet) //Debug.WriteLine(GenerateSpoiler()); //Debug.WriteLine(westHyrule.GetMapDebug()); //XXX: - return palaces[2].ItemRooms[0].Collectable == Collectable.GLOVE; + return false; } //#endif return false; @@ -2688,31 +2702,32 @@ private void UpdateRom() else { // Use the old spell menu behavior where the spells are placed in the menu in the location it came from - var wizardCollectables = ItemLocations() + /*var wizardCollectables = ItemLocations() .Where(l => l.Town?.GetWizard() != null && l != westHyrule.sariaSouth && l != westHyrule.bagu) .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); + */ for (int i = 0; i < magFunction.Length; i++) { - magFunction[i] = ROMData.GetByte(functionBase + wizardCollectables[i].VanillaSpellOrder()); + magFunction[i] = ROMData.GetByte(functionBase + spellListOrder[i].VanillaSpellOrder()); } for (int i = 0; i < magEffects.Length; i = i + 2) { - magEffects[i] = ROMData.GetByte(effectBase + wizardCollectables[i / 2].VanillaSpellOrder() * 2); - magEffects[i + 1] = ROMData.GetByte(effectBase + wizardCollectables[i / 2].VanillaSpellOrder() * 2 + 1); + magEffects[i] = ROMData.GetByte(effectBase + spellListOrder[i / 2].VanillaSpellOrder() * 2); + magEffects[i + 1] = ROMData.GetByte(effectBase + spellListOrder[i / 2].VanillaSpellOrder() * 2 + 1); } for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { - magLevels[i, j] = ROMData.GetByte(spellCostBase + (wizardCollectables[i].VanillaSpellOrder() * 8 + j)); + magLevels[i, j] = ROMData.GetByte(spellCostBase + (spellListOrder[i].VanillaSpellOrder() * 8 + j)); } for (int j = 0; j < 8; j++) { - magNames[i, j] = ROMData.GetByte(spellNameBase + (wizardCollectables[i].VanillaSpellOrder() * 0xe + j)); + magNames[i, j] = ROMData.GetByte(spellNameBase + (spellListOrder[i].VanillaSpellOrder() * 0xe + j)); } } @@ -2742,7 +2757,7 @@ private void UpdateRom() for (int i = 0; i < 8; i++) { - ROMData.Put(TownExtensions.SPELL_GET_START_ADDRESS + i, props.StartsWithCollectable(wizardCollectables[i]) ? (byte)1 : (byte)0); + ROMData.Put(TownExtensions.SPELL_GET_START_ADDRESS + i, props.StartsWithCollectable(spellListOrder[i]) ? (byte)1 : (byte)0); } } //fix for rope graphical glitch @@ -3089,7 +3104,8 @@ public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) bool itemWasMissing = false; foreach (Location location in ItemLocations().Where(l => l.GetAllCollectables().Any(i => !i.IsMinorItem() && !ItemGet[i]))) { - sb.AppendLine($"{location.Name} / [{String.Join(", ", location.GetAllCollectables() + string name = location.Name + (location.Palace == null ? String.Empty : $"({location?.Palace?.Number})"); + sb.AppendLine($"{name} / [{String.Join(", ", location!.GetAllCollectables() .Where(i => !i.IsMinorItem() && !ItemGet[i]) .Select(i => Enum.GetName(i)))}]"); itemWasMissing = true; diff --git a/RandomizerCore/Overworld/Location.cs b/RandomizerCore/Overworld/Location.cs index 6ec3b596a..cc9774e54 100644 --- a/RandomizerCore/Overworld/Location.cs +++ b/RandomizerCore/Overworld/Location.cs @@ -359,6 +359,6 @@ public int GetCollectableCount(bool shufflableOnly = false) { return Palace.ItemRooms.Count; } - return NonPalaceTownCollectables.Count; + return (CollectablesAreShufflable || !shufflableOnly) ? NonPalaceTownCollectables.Count : 0; } } diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index 94a561991..c2fe77eee 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -1615,7 +1615,7 @@ public void AssignPalaceItemCounts(RandomizerProperties properties, Random r) properties.UsePalaceItemRoomCountIndicator = true; break; default: - properties.PalaceItemRoomCounts = Enumerable.Repeat((int)palaceItemRoomCount, 6).ToArray(); + properties.PalaceItemRoomCounts = Enumerable.Repeat((int)palaceItemRoomCount, 7).ToArray(); properties.UsePalaceItemRoomCountIndicator = false; break; } diff --git a/RandomizerCore/Sidescroll/Town/Towns.cs b/RandomizerCore/Sidescroll/Town/Towns.cs index 8f2721a7d..16caaca88 100644 --- a/RandomizerCore/Sidescroll/Town/Towns.cs +++ b/RandomizerCore/Sidescroll/Town/Towns.cs @@ -20,8 +20,10 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Rauru"; map = town.GetTownMap(VanillaTownMap.RAURU_LEFT)!; map.Name = "Rauru Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.RAURU_RIGHT)!; map.Name = "Rauru Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.RAURU_WIZARD)!; map.Name = "Rauru Wizard"; map.Collectable = Collectable.SHIELD_SPELL; @@ -33,8 +35,10 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Ruto"; map = town.GetTownMap(VanillaTownMap.RUTO_LEFT)!; map.Name = "Ruto Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.RUTO_RIGHT)!; map.Name = "Ruto Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.RUTO_WIZARD)!; map.Name = "Ruto Wizard"; map.Collectable = Collectable.JUMP_SPELL; @@ -47,11 +51,13 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Saria"; map = town.GetTownMap(VanillaTownMap.SARIA_LEFT)!; map.Name = "Saria Moat"; + map.LeftExitIsOutside = true; map.AccessRequirements = new Requirements([RequirementType.FAIRY, RequirementType.BAGU_LETTER], [[RequirementType.JUMP, RequirementType.DASH]]); map = town.GetTownMap(VanillaTownMap.SARIA_MID)!; map.Name = "Saria Middle"; map = town.GetTownMap(VanillaTownMap.SARIA_RIGHT)!; map.Name = "Saria Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.SARIA_WIZARD)!; map.Name = "Saria Wizard"; map.Collectable = Collectable.LIFE_SPELL; @@ -67,10 +73,12 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Mido"; map = town.GetTownMap(VanillaTownMap.MIDO_LEFT)!; map.Name = "Mido Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.MIDO_MIDDLE)!; map.Name = "Mido Middle"; map = town.GetTownMap(VanillaTownMap.MIDO_RIGHT)!; map.Name = "Mido Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.MIDO_WIZARD)!; map.Name = "Mido Wizard"; map.Collectable = Collectable.FAIRY_SPELL; @@ -87,11 +95,13 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Nabooru"; map = town.GetTownMap(VanillaTownMap.NABOORU_LEFT)!; map.Name = "Nabooru Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.NABOORU_MID)!; map.Name = "Nabooru Fountain"; map.Collectable = Collectable.WATER; map = town.GetTownMap(VanillaTownMap.NABOORU_RIGHT)!; map.Name = "Nabooru Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.NABOORU_WIZARD)!; map.Name = "Nabooru Wizard"; map.Collectable = Collectable.FIRE_SPELL; @@ -104,10 +114,12 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Darunia"; map = town.GetTownMap(VanillaTownMap.DARUNIA_LEFT)!; map.Name = "Darunia Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.DARUNIA_MID)!; map.Name = "Darunia Middle"; map = town.GetTownMap(VanillaTownMap.DARUNIA_RIGHT)!; map.Name = "Darunia Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.DARUNIA_WIZARD)!; map.Name = "Darunia Wizard"; map.Collectable = Collectable.REFLECT_SPELL; @@ -130,10 +142,12 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "New Kasuto"; map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_LEFT)!; map.Name = "New Kasuto Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_MID)!; map.Name = "New Kasuto Middle"; map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_RIGHT)!; map.Name = "New Kasuto Right"; + map.RightExitIsOutside = false; map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_WIZARD)!; map.Name = "New Kasuto Wizard"; map.Collectable = Collectable.SPELL_SPELL; @@ -152,10 +166,12 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Old Kasuto"; map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_LEFT)!; map.Name = "Old Kasuto Left"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_MID)!; map.Name = "Old Kasuto Left"; map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_RIGHT)!; map.Name = "Old Kasuto Right"; + map.RightExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_WIZARD)!; map.Name = "Old Kasuto Wizard"; map.Collectable = Collectable.THUNDER_SPELL; @@ -168,6 +184,7 @@ public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableConta town.Name = "Bagu's House"; map = town.GetTownMap(VanillaTownMap.BAGU_EXTERNAL)!; map.Name = "Bagu Exterior"; + map.LeftExitIsOutside = true; map = town.GetTownMap(VanillaTownMap.BAGU_INDOORS)!; map.Name = "Bagu Interior"; map.Collectable = Collectable.BAGUS_NOTE; diff --git a/Statistics/Statistics.cs b/Statistics/Statistics.cs index ee4e5f4d1..823538445 100644 --- a/Statistics/Statistics.cs +++ b/Statistics/Statistics.cs @@ -20,9 +20,9 @@ namespace Z2Randomizer.Statistics; /// class Statistics { - //private static readonly string FLAGS = "hEAK0thCqbLyhAAL4XpGU+!5@W4xeWvdAALhA"; //Random% vanilla + private static readonly string FLAGS = "hEABexKyA3zW5qAw0LJid9wd2COH+0c4YUCfstWWdAALh+h"; //Random% //private static readonly string FLAGS = "AAAA2dJALw3kToaJESXL#o4++o6WFU9WaAkyAAWAoh"; //Standard Swiss - private static readonly string FLAGS = "hEABexKyA3zWoqAw0LJidsqd2COQ+56swoE@WXss7AAWA+h"; //test + //private static readonly string FLAGS = "hEABexKyA3zWoqAw0LJidsqd2COQ+56swoE@WXss7AAWA+h"; //test //private static readonly string FLAGS = "g7+y2SHCACtOJ3hAAAAACsWhtocY+#caVWVprWmCQAABVAbh"; //short/short //private static readonly string FLAGS = "g7+y2SHCACtOJ3hAAAAACsWhtocY+#caVWVprWmCQAABVFbh"; //long/long