From 0cc1c2e1a2cc08e4ebdc3323fda6438450824f44 Mon Sep 17 00:00:00 2001 From: Justin Olbrantz Date: Thu, 28 May 2026 17:45:02 -0700 Subject: [PATCH 01/56] Fix FT music overriding fire SFX --- RandomizerCore/Asm/z2rndft.ips | Bin 7459 -> 7510 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/RandomizerCore/Asm/z2rndft.ips b/RandomizerCore/Asm/z2rndft.ips index ea62c5fd607c16dbc3b7377133a4139779d32550..5bf2007e8dd51fe2e18b2d630e3a6f1edd832968 100644 GIT binary patch delta 69 zcmV-L0J{I9I@UU{>H{Qg00E2002WHH3eW{c?XU`9ObnK(0FC_zU`zy-0gK)M001(R b>jQBQmFoZtkOhqW2e7qFxQhU@TLj1$lSdj& delta 18 acmca+wb*LID`v*6llfQ*H}|s~mjD1w8wY#< From ac00bf4bf080fdd3b79c86d3a75a6ac4f9be013b Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 17 Jun 2026 09:30:31 -0400 Subject: [PATCH 02/56] Generate a With method to allow mixing configurations --- .../FlagsSerializeGenerator.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CoreSourceGenerator/FlagsSerializeGenerator.cs b/CoreSourceGenerator/FlagsSerializeGenerator.cs index 2decebcdf..55882e30d 100644 --- a/CoreSourceGenerator/FlagsSerializeGenerator.cs +++ b/CoreSourceGenerator/FlagsSerializeGenerator.cs @@ -277,6 +277,7 @@ private static string GenerateCode(ClassGenerationInfo classInfo) GenerateSerializeMethod(sb, classInfo.SerializedFields, indent); GenerateDeserializeMethod(sb, classInfo.SerializedFields, indent); + GenerateWithMethod(sb, classInfo.ClassName, classInfo.ReactiveFields, indent); // Generate helper methods for enum serialization GenerateSerializerList(sb, classInfo.SerializedFields, indent); @@ -400,6 +401,24 @@ private static void GenerateDeserializeMethod(StringBuilder sb, List fields, string indent) + { + sb.AppendLine(); + sb.AppendLine($"{indent} public {className} With({className} other)"); + sb.AppendLine($"{indent} {{"); + sb.AppendLine($"{indent} {className} result = new();"); + sb.AppendLine(); + + foreach (var field in fields) + { + sb.AppendLine($"{indent} result.{field.PropertyName} = !Equals(other.{field.PropertyName}, this.{field.PropertyName}) ? other.{field.PropertyName} : this.{field.PropertyName};"); + } + + sb.AppendLine(); + sb.AppendLine($"{indent} return result;"); + sb.AppendLine($"{indent} }}"); + } + private static void GenerateSerializerList(StringBuilder sb, List fields, string indent) { sb.AppendLine(); From b4f660779bf152fa0b2ebfaabc58fb8ee35dec6b Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 17 Jun 2026 10:28:28 -0400 Subject: [PATCH 03/56] Use the vanilla version of all presets as the default config --- RandomizerCore/RandomizerConfiguration.cs | 409 ++++++++++------------ 1 file changed, 184 insertions(+), 225 deletions(-) diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index 28f68b702..278328401 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -10,6 +10,8 @@ using NLog; using Z2Randomizer.RandomizerCore.Flags; using Z2Randomizer.RandomizerCore.Sidescroll.Palace; +// ReSharper disable InconsistentNaming +// ReSharper disable RedundantDefaultMemberInitializer namespace Z2Randomizer.RandomizerCore; @@ -30,13 +32,13 @@ public class ReactiveAttribute : Attribute [FlagSerialize] -public sealed partial class RandomizerConfiguration : INotifyPropertyChanged +public sealed partial class RandomizerConfiguration() : INotifyPropertyChanged { [IgnoreInFlags] private static readonly Logger logger = LogManager.GetCurrentClassLogger(); [IgnoreInFlags] - private readonly static Collectable[] POSSIBLE_STARTING_ITEMS = [ + private static readonly Collectable[] POSSIBLE_STARTING_ITEMS = [ Collectable.CANDLE, Collectable.GLOVE, Collectable.RAFT, @@ -48,7 +50,7 @@ public sealed partial class RandomizerConfiguration : INotifyPropertyChanged ]; [IgnoreInFlags] - private readonly static Collectable[] POSSIBLE_SHARED_STARTING_ITEMS = [ + private static readonly Collectable[] POSSIBLE_SHARED_STARTING_ITEMS = [ Collectable.GLOVE, Collectable.RAFT, Collectable.BOOTS, @@ -58,13 +60,13 @@ public sealed partial class RandomizerConfiguration : INotifyPropertyChanged ]; [IgnoreInFlags] - private readonly static Collectable[] POSSIBLE_DIFFICULTY_STARTING_ITEMS = [ + private static readonly Collectable[] POSSIBLE_DIFFICULTY_STARTING_ITEMS = [ Collectable.CANDLE, Collectable.CROSS ]; [IgnoreInFlags] - private readonly static Collectable[] POSSIBLE_STARTING_SPELLS = [ + private static readonly Collectable[] POSSIBLE_STARTING_SPELLS = [ Collectable.SHIELD_SPELL, Collectable.JUMP_SPELL, Collectable.LIFE_SPELL, @@ -78,211 +80,211 @@ public sealed partial class RandomizerConfiguration : INotifyPropertyChanged //Start Configuration [Reactive] - private bool shuffleStartingItems; + private bool shuffleStartingItems = false; [Reactive] [DifficultyOnly] - private bool startWithCandle; + private bool startWithCandle = false; [Reactive] - private bool startWithGlove; + private bool startWithGlove = false; [Reactive] - private bool startWithRaft; + private bool startWithRaft = false; [Reactive] - private bool startWithBoots; + private bool startWithBoots = false; [Reactive] - private bool startWithFlute; + private bool startWithFlute = false; [Reactive] [DifficultyOnly] - private bool startWithCross; + private bool startWithCross = false; [Reactive] - private bool startWithHammer; + private bool startWithHammer = false; [Reactive] - private bool startWithMagicKey; + private bool startWithMagicKey = false; [Reactive] - private bool shuffleStartingSpells; + private bool shuffleStartingSpells = false; [Reactive] - private StartingResourceLimit startItemsLimit; + private StartingResourceLimit startItemsLimit = StartingResourceLimit.NO_LIMIT; [Reactive] - private bool startWithShield; + private bool startWithShield = false; [Reactive] - private bool startWithJump; + private bool startWithJump = false; [Reactive] - private bool startWithLife; + private bool startWithLife = false; [Reactive] - private bool startWithFairy; + private bool startWithFairy = false; [Reactive] - private bool startWithFire; + private bool startWithFire = false; [Reactive] - private bool startWithReflect; + private bool startWithReflect = false; [Reactive] - private bool startWithSpellSpell; + private bool startWithSpellSpell = false; [Reactive] - private bool startWithThunder; + private bool startWithThunder = false; [Reactive] - private StartingResourceLimit startSpellsLimit; + private StartingResourceLimit startSpellsLimit = StartingResourceLimit.NO_LIMIT; [Reactive] [Minimum(1)] [Maximum(8)] - private int? startingHeartContainersMin; + private int? startingHeartContainersMin = 4; [Reactive] [Minimum(1)] [Maximum(8)] - private int? startingHeartContainersMax; + private int? startingHeartContainersMax = 4; [Reactive] [Minimum(1)] [Maximum(8)] - private int? startingMagicContainersMin; + private int? startingMagicContainersMin = 4; [Reactive] [Minimum(1)] [Maximum(8)] - private int? startingMagicContainersMax; + private int? startingMagicContainersMax = 4; [Reactive] - private MaxHeartsOption maxHeartContainers; + private MaxHeartsOption maxHeartContainers = MaxHeartsOption.EIGHT; [Reactive] [DifficultyOnly] - private StartingTechs startingTechniques; + private StartingTechs startingTechniques = StartingTechs.NONE; [Reactive] [DifficultyOnly] - private StartingLives startingLives; + private StartingLives startingLives = StartingLives.Lives3; [Reactive] [DifficultyOnly] [Minimum(1)] [Maximum(8)] - private int startingAttackLevel; + private int startingAttackLevel = 1; [Reactive] [DifficultyOnly] [Minimum(1)] [Maximum(8)] - private int startingMagicLevel; + private int startingMagicLevel = 1; [Reactive] [DifficultyOnly] [Minimum(1)] [Maximum(8)] - private int startingLifeLevel; + private int startingLifeLevel = 1; [Reactive] - private IndeterminateOptionRate indeterminateOptionRate; + private IndeterminateOptionRate indeterminateOptionRate = IndeterminateOptionRate.HALF; //Overworld [Reactive] - private bool? palacesCanSwapContinents; + private bool? palacesCanSwapContinents = false; [Reactive] [ConditionallyIncludeInFlags] - private bool? shuffleGP; + private bool? shuffleGP = false; public bool shuffleGPIncluded() => palacesCanSwapContinents != false; [Reactive] - private EncounterRate encounterRate; + private EncounterRate encounterRate = EncounterRate.NORMAL; [Reactive] [ConditionallyIncludeInFlags] - private bool? shuffleEncounters; + private bool? shuffleEncounters = false; public bool shuffleEncountersIncluded() => encounterRate != EncounterRate.NONE; [Reactive] [ConditionallyIncludeInFlags] - private bool allowUnsafePathEncounters; + private bool allowUnsafePathEncounters = false; public bool allowUnsafePathEncountersIncluded() => shuffleEncountersIncluded() && shuffleEncounters != false; [Reactive] [ConditionallyIncludeInFlags] - private bool includeLavaInEncounterShuffle; + private bool includeLavaInEncounterShuffle = false; public bool includeLavaInEncounterShuffleIncluded() => shuffleEncountersIncluded() && shuffleEncounters != false; [Reactive] - private bool? hidePalace; + private bool? hidePalace = true; [Reactive] - private bool? hideKasuto; + private bool? hideKasuto = true; [Reactive] [ConditionallyIncludeInFlags] - private bool? shuffleWhichLocationIsHidden; + private bool? shuffleWhichLocationIsHidden = false; public bool shuffleWhichLocationIsHiddenIncluded() => hidePalace != false || hideKasuto != false; [Reactive] - private LessImportantLocationsOption lessImportantLocationsOption; + private LessImportantLocationsOption lessImportantLocationsOption = LessImportantLocationsOption.HIDE; [Reactive] - private bool? restrictConnectionCaveShuffle; + private bool? restrictConnectionCaveShuffle = true; [Reactive] - private bool allowConnectionCavesToBeBlocked; + private bool allowConnectionCavesToBeBlocked = false; [Reactive] - private bool? goodBoots; + private bool? goodBoots = false; [Reactive] - private bool? generateBaguWoods; + private bool? generateBaguWoods = true; [Reactive] - private ContinentConnectionType continentConnectionType; + private ContinentConnectionType continentConnectionType = ContinentConnectionType.NORMAL; [Reactive] - private OverworldSizeOption westSize; + private OverworldSizeOption westSize = OverworldSizeOption.LARGE; [Reactive] - private OverworldSizeOption eastSize; + private OverworldSizeOption eastSize = OverworldSizeOption.LARGE; [Reactive] - private DmSizeOption dmSize; + private DmSizeOption dmSize = DmSizeOption.LARGE; [Reactive] - private MazeSizeOption mazeSize; + private MazeSizeOption mazeSize = MazeSizeOption.LARGE; [Reactive] - private Biome westBiome; + private Biome westBiome = Biome.VANILLA; [Reactive] - private Biome eastBiome; + private Biome eastBiome = Biome.VANILLA; [Reactive] - private Biome dmBiome; + private Biome dmBiome = Biome.VANILLA; [Reactive] - private Biome mazeBiome; + private Biome mazeBiome = Biome.VANILLA; [Reactive] - private ClimateEnum westClimate; + private ClimateEnum westClimate = ClimateEnum.CLASSIC; [Reactive] - private ClimateEnum eastClimate; + private ClimateEnum eastClimate = ClimateEnum.CLASSIC; [Reactive] - private ClimateEnum dmClimate; + private ClimateEnum dmClimate = ClimateEnum.CLASSIC; [Reactive] [ConditionallyIncludeInFlags] - private bool legacyVanillaShuffledLocations; + private bool legacyVanillaShuffledLocations = false; public bool legacyVanillaShuffledLocationsIncluded() { foreach (var biome in (List)[westBiome, eastBiome, dmBiome, mazeBiome]) { @@ -299,10 +301,10 @@ public bool legacyVanillaShuffledLocationsIncluded() { //Palaces [Reactive] - private PalaceStyle normalPalaceStyle; + private PalaceStyle normalPalaceStyle = PalaceStyle.VANILLA; [Reactive] - private PalaceStyle gpStyle; + private PalaceStyle gpStyle = PalaceStyle.VANILLA; private bool palaceStylesAreNotAllVanilla() { @@ -349,393 +351,393 @@ private bool palaceStylesAnyMetastyleSelected() [Reactive] [ConditionallyIncludeInFlags] - private bool randomStylesAllowVanilla; + private bool randomStylesAllowVanilla = false; public bool randomStylesAllowVanillaIncluded() => palaceStylesAnyMetastyleSelected(); [Reactive] - private bool? includeVanillaRooms; + private bool? includeVanillaRooms = true; [Reactive] [ConditionallyIncludeInFlags] - private bool? includev4_0Rooms; + private bool? includev4_0Rooms = false; public bool includev4_0RoomsIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); [Reactive] [ConditionallyIncludeInFlags] - private bool? includev5_0Rooms; + private bool? includev5_0Rooms = false; public bool includev5_0RoomsIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); [Reactive] [ConditionallyIncludeInFlags] - private bool blockingRoomsInAnyPalace; + private bool blockingRoomsInAnyPalace = false; public bool blockingRoomsInAnyPalaceIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); [Reactive] - private PalaceDropStyle palaceDropStyle; + private PalaceDropStyle palaceDropStyle = PalaceDropStyle.ENTRANCE; public bool palaceDropStyleIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); [Reactive] [ConditionallyIncludeInFlags] - private bool removeLongDeadEnds; + private bool removeLongDeadEnds = false; public bool removeLongDeadEndsIncluded() => includev5_0RoomsIncluded() && includev5_0Rooms is not false; [Reactive] [ConditionallyIncludeInFlags] - private bool includeExpertRooms; + private bool includeExpertRooms = false; public bool includeExpertRoomsIncluded() => palaceStylesAreNotAllVanilla(); [Reactive] [ConditionallyIncludeInFlags] - private BossRoomsExitType bossRoomsExitType; + private BossRoomsExitType bossRoomsExitType = BossRoomsExitType.OVERWORLD; public bool bossRoomsExitTypeIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); [Reactive] [ConditionallyIncludeInFlags] - private bool? tBirdRequired; + private bool? tBirdRequired = true; public bool tBirdRequiredIncluded() => palaceStylesAreNotAllVanilla(); [Reactive] [ConditionallyIncludeInFlags] - private bool removeTBird; + private bool removeTBird = false; public bool removeTBirdIncluded() => tBirdRequiredIncluded() && tBirdRequired != true; [Reactive] - private bool restartAtPalacesOnGameOver; + private bool restartAtPalacesOnGameOver = false; [Reactive] private bool? global5050JarDrop = false; [Reactive] - private bool reduceDripperVariance; + private bool reduceDripperVariance = false; [Reactive] - private bool changePalacePallettes; + private bool changePalacePallettes = false; [Reactive] - private bool randomizeBossItemDrop; + private bool randomizeBossItemDrop = false; [Reactive] [ConditionallyIncludeInFlags] - private BossRoomMinDistance darkLinkMinDistance; + private BossRoomMinDistance darkLinkMinDistance = BossRoomMinDistance.NONE; public bool darkLinkMinDistanceIncluded() => palaceStylesAreNotAllVanilla(); [Reactive] - private PalaceItemRoomCount palaceItemRoomCount; + private PalaceItemRoomCount palaceItemRoomCount = PalaceItemRoomCount.ONE; [Reactive] [Minimum(0)] [Maximum(6)] - private int palacesToCompleteMin; + private int palacesToCompleteMin = 6; [Reactive] [Minimum(0)] [Maximum(6)] [ConditionallyIncludeInFlags] [DefaultValue(6)] - private int palacesToCompleteMax; + private int palacesToCompleteMax = 6; public bool palacesToCompleteMaxIncluded() => palacesToCompleteMin != 6; [Reactive] [ConditionallyIncludeInFlags] - private bool noDuplicateRoomsByLayout; + private bool noDuplicateRoomsByLayout = false; public bool noDuplicateRoomsByLayoutIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); [Reactive] [ConditionallyIncludeInFlags] - private bool noDuplicateRoomsByEnemies; + private bool noDuplicateRoomsByEnemies = false; public bool noDuplicateRoomsByEnemiesIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); [Reactive] - private bool hardBosses; + private bool hardBosses = false; [Reactive] - private bool aggressiveTbird; + private bool aggressiveTbird = false; //Levels [Reactive] - private bool shuffleAttackExperience; + private bool shuffleAttackExperience = false; [Reactive] - private bool shuffleMagicExperience; + private bool shuffleMagicExperience = false; [Reactive] - private bool shuffleLifeExperience; + private bool shuffleLifeExperience = false; [Reactive] [DifficultyOnly] [Minimum(1)] [Maximum(8)] - private int attackLevelCap; + private int attackLevelCap = 8; [Reactive] [DifficultyOnly] [Minimum(1)] [Maximum(8)] - private int magicLevelCap; + private int magicLevelCap = 8; [Reactive] [DifficultyOnly] [Minimum(1)] [Maximum(8)] - private int lifeLevelCap; + private int lifeLevelCap = 8; [Reactive] [DifficultyOnly] [ConditionallyIncludeInFlags] - private bool scaleLevelRequirementsToCap; + private bool scaleLevelRequirementsToCap = false; public bool scaleLevelRequirementsToCapIncluded() => attackLevelCap < 8 || magicLevelCap < 8 || lifeLevelCap < 8; [Reactive] [DifficultyOnly] - private AttackEffectiveness attackEffectiveness; + private AttackEffectiveness attackEffectiveness = AttackEffectiveness.VANILLA; [Reactive] - private MagicEffectiveness magicEffectiveness; + private MagicEffectiveness magicEffectiveness = MagicEffectiveness.VANILLA; [Reactive] [DifficultyOnly] - private LifeEffectiveness lifeEffectiveness; + private LifeEffectiveness lifeEffectiveness = LifeEffectiveness.VANILLA; //Spells [Reactive] - private bool shuffleLifeRefillAmount; + private bool shuffleLifeRefillAmount = false; [Reactive] - private bool? shuffleSpellLocations; + private bool? shuffleSpellLocations = false; [Reactive] - private bool? disableMagicContainerRequirements; + private bool? disableMagicContainerRequirements = false; [Reactive] - private bool? randomizeSpellSpellEnemy; + private bool? randomizeSpellSpellEnemy = false; [Reactive] - private bool? swapUpAndDownStab; + private bool? swapUpAndDownStab = false; [Reactive] - private FireOption fireOption; + private FireOption fireOption = FireOption.NORMAL; //Enemies [Reactive] - private bool? shuffleOverworldEnemies; + private bool? shuffleOverworldEnemies = false; [Reactive] - private bool? shufflePalaceEnemies; + private bool? shufflePalaceEnemies = false; private bool anyEnemiesAreShuffled() => shuffleOverworldEnemies != false || shufflePalaceEnemies != false; [Reactive] [ConditionallyIncludeInFlags] - private DripperEnemyOption dripperEnemyOption; + private DripperEnemyOption dripperEnemyOption = DripperEnemyOption.ONLY_BOTS; public bool dripperEnemyOptionIncluded() => anyEnemiesAreShuffled(); [Reactive] [ConditionallyIncludeInFlags] - private bool? mixLargeAndSmallEnemies; + private bool? mixLargeAndSmallEnemies = false; public bool mixLargeAndSmallEnemiesIncluded() => anyEnemiesAreShuffled(); [Reactive] [ConditionallyIncludeInFlags] - private bool generatorsAlwaysMatch; + private bool generatorsAlwaysMatch = true; public bool generatorsAlwaysMatchIncluded() => anyEnemiesAreShuffled(); [Reactive] [DifficultyOnly] - private EnemyLifeOption shuffleEnemyHP; + private EnemyLifeOption shuffleEnemyHP = EnemyLifeOption.VANILLA; [Reactive] [DifficultyOnly] - private EnemyLifeOption shuffleBossHP; + private EnemyLifeOption shuffleBossHP = EnemyLifeOption.VANILLA; [Reactive] - private bool shuffleXPStealers; + private bool shuffleXPStealers = false; [Reactive] - private bool shuffleXPStolenAmount; + private bool shuffleXPStolenAmount = false; [Reactive] - private bool shuffleSwordImmunity; + private bool shuffleSwordImmunity = false; [Reactive] [DifficultyOnly] - private XPEffectiveness enemyXPDrops; + private XPEffectiveness enemyXPDrops = XPEffectiveness.VANILLA; //Items [Reactive] - private bool? shufflePalaceItems; + private bool? shufflePalaceItems = false; [Reactive] - private bool? shuffleOverworldItems; + private bool? shuffleOverworldItems = false; [Reactive] [ConditionallyIncludeInFlags] - private bool? mixOverworldAndPalaceItems; + private bool? mixOverworldAndPalaceItems = false; public bool mixOverworldAndPalaceItemsIncluded() => shufflePalaceItems != false && shuffleOverworldItems != false; [Reactive] [ConditionallyIncludeInFlags] - private bool? includePBagCavesInItemShuffle; + private bool? includePBagCavesInItemShuffle = false; public bool includePBagCavesInItemShuffleIncluded() => shuffleOverworldItems != false; [Reactive] - private bool shuffleSmallItems; + private bool shuffleSmallItems = false; [Reactive] - private bool? palacesContainExtraKeys; + private bool? palacesContainExtraKeys = false; [Reactive] - private bool randomizeNewKasutoJarRequirements; + private bool randomizeNewKasutoJarRequirements = false; [Reactive] - private bool allowImportantItemDuplicates; + private bool allowImportantItemDuplicates = false; [Reactive] - private bool? removeSpellItems; + private bool? removeSpellItems = false; [Reactive] - private bool? shufflePBagAmounts; + private bool? shufflePBagAmounts = false; [Reactive] - private bool? includeSpellsInShuffle; + private bool? includeSpellsInShuffle = false; [Reactive] - private bool? includeSwordTechsInShuffle; + private bool? includeSwordTechsInShuffle = false; [Reactive] - private bool? includeQuestItemsInShuffle; + private bool? includeQuestItemsInShuffle = false; [Reactive] private bool preventSpellItemChains; //Drops [Reactive] - private bool shuffleItemDropFrequency; + private bool shuffleItemDropFrequency = false; [Reactive] - private bool randomizeDrops; + private bool randomizeDrops = false; [Reactive] - private bool standardizeDrops; + private bool standardizeDrops = false; [Reactive] - private bool smallEnemiesCanDropBlueJar; + private bool smallEnemiesCanDropBlueJar = false; [Reactive] - private bool smallEnemiesCanDropRedJar; + private bool smallEnemiesCanDropRedJar = false; [Reactive] - private bool smallEnemiesCanDropSmallBag; + private bool smallEnemiesCanDropSmallBag = false; [Reactive] - private bool smallEnemiesCanDropMediumBag; + private bool smallEnemiesCanDropMediumBag = false; [Reactive] - private bool smallEnemiesCanDropLargeBag; + private bool smallEnemiesCanDropLargeBag = false; [Reactive] - private bool smallEnemiesCanDropXLBag; + private bool smallEnemiesCanDropXLBag = false; [Reactive] - private bool smallEnemiesCanDrop1up; + private bool smallEnemiesCanDrop1up = false; [Reactive] - private bool smallEnemiesCanDropKey; + private bool smallEnemiesCanDropKey = false; [Reactive] - private bool largeEnemiesCanDropBlueJar; + private bool largeEnemiesCanDropBlueJar = false; [Reactive] - private bool largeEnemiesCanDropRedJar; + private bool largeEnemiesCanDropRedJar = false; [Reactive] - private bool largeEnemiesCanDropSmallBag; + private bool largeEnemiesCanDropSmallBag = false; [Reactive] - private bool largeEnemiesCanDropMediumBag; + private bool largeEnemiesCanDropMediumBag = false; [Reactive] - private bool largeEnemiesCanDropLargeBag; + private bool largeEnemiesCanDropLargeBag = false; [Reactive] - private bool largeEnemiesCanDropXLBag; + private bool largeEnemiesCanDropXLBag = false; [Reactive] - private bool largeEnemiesCanDrop1up; + private bool largeEnemiesCanDrop1up = false; [Reactive] - private bool largeEnemiesCanDropKey; + private bool largeEnemiesCanDropKey = false; //Misc [Reactive] private HelpfulHintOption helpfulHints; [Reactive] - private bool? enableSpellItemHints; + private bool? enableSpellItemHints = false; [Reactive] - private bool? enableTownNameHints; + private bool? enableTownNameHints = false; [Reactive] - private bool jumpAlwaysOn; + private bool jumpAlwaysOn = false; [Reactive] - private bool dashAlwaysOn; + private bool dashAlwaysOn = false; [Reactive] - private bool permanentBeamSword; + private bool permanentBeamSword = false; //Custom [Reactive] [IgnoreInFlags] - private bool useCommunityText; + private bool useCommunityText = false; [Reactive] [IgnoreInFlags] - private BeepFrequency beepFrequency; + private BeepFrequency beepFrequency = BeepFrequency.Normal; [Reactive] [IgnoreInFlags] - private BeepThreshold beepThreshold; + private BeepThreshold beepThreshold = BeepThreshold.Normal; [Reactive] [IgnoreInFlags] - private bool disableMusic; + private bool disableMusic = false; [Reactive] [IgnoreInFlags] - private bool randomizeMusic; + private bool randomizeMusic = false; [Reactive] [IgnoreInFlags] - private bool mixCustomAndOriginalMusic; + private bool mixCustomAndOriginalMusic = false; [Reactive] [IgnoreInFlags] - private bool includeDiverseMusic; + private bool includeDiverseMusic = false; [Reactive] [IgnoreInFlags] - private bool disableUnsafeMusic; + private bool disableUnsafeMusic = false; [Reactive] [IgnoreInFlags] - private bool fastSpellCasting; + private bool fastSpellCasting = false; [Reactive] [IgnoreInFlags] - private bool upAOnController1; + private bool upAOnController1 = false; [Reactive] [IgnoreInFlags] - private bool removeFlashing; + private bool removeFlashing = false; [Reactive] [IgnoreInFlags] - private CharacterSprite sprite; + private CharacterSprite sprite = CharacterSprite.LINK; [Reactive] [IgnoreInFlags] - private string spriteName; + private string spriteName = CharacterSprite.LINK.DisplayName; [Reactive] [IgnoreInFlags] @@ -744,64 +746,64 @@ private bool palaceStylesAnyMetastyleSelected() [Reactive] [IgnoreInFlags] - private bool changeItemSprites; + private bool changeItemSprites = false; [Reactive] [IgnoreInFlags] - private NesColor tunic; + private NesColor tunic = NesColor.Default; [Reactive] [IgnoreInFlags] - private NesColor skinTone; + private NesColor skinTone = NesColor.Default; [Reactive] [IgnoreInFlags] - private NesColor tunicOutline; + private NesColor tunicOutline = NesColor.Default; [Reactive] [IgnoreInFlags] - private NesColor shieldTunic; + private NesColor shieldTunic = NesColor.Default; [Reactive] [IgnoreInFlags] - private bool shuffleSpritePalettes; + private bool shuffleSpritePalettes = false; [Reactive] [IgnoreInFlags] - private BeamSprites beamSprite; + private BeamSprites beamSprite = BeamSprites.DEFAULT; [Reactive] [IgnoreInFlags] - private bool useCustomRooms; + private bool useCustomRooms = false; [Reactive] [IgnoreInFlags] - private bool disableHUDLag; + private bool disableHUDLag = false; [Reactive] - private bool randomizeKnockback; + private bool randomizeKnockback = false; [Reactive] - private PalaceLengthOption gpLength; + private PalaceLengthOption gpLength = PalaceLengthOption.FULL; [Reactive] - private PalaceLengthOption normalPalaceLength; + private PalaceLengthOption normalPalaceLength = PalaceLengthOption.FULL; [Reactive] - private RiverDevilBlockerOption riverDevilBlockerOption; + private RiverDevilBlockerOption riverDevilBlockerOption = RiverDevilBlockerOption.PATH; [Reactive] - private bool? eastRocks; + private bool? eastRocks = false; [Reactive] - private bool generateSpoiler; + private bool generateSpoiler = false; [Reactive] - private bool revealWalkthroughWalls; + private bool revealWalkthroughWalls = false; [Reactive] - private bool shareSeedAcrossDifficulty; + private bool shareSeedAcrossDifficulty = false; //Meta [Reactive] @@ -824,49 +826,6 @@ public string SerializeSharedSeedFlags() return SerializeSharedSeed(); } - public RandomizerConfiguration() - { - startingAttackLevel = 1; - startingMagicLevel = 1; - startingLifeLevel = 1; - - maxHeartContainers = MaxHeartsOption.EIGHT; - startingHeartContainersMin = 4; - startingHeartContainersMax = 4; - startingMagicContainersMin = 4; - startingMagicContainersMax = 4; - - attackLevelCap = 8; - magicLevelCap = 8; - lifeLevelCap = 8; - - disableMusic = false; - randomizeMusic = false; - mixCustomAndOriginalMusic = true; - disableUnsafeMusic = true; - fastSpellCasting = false; - shuffleSpritePalettes = false; - permanentBeamSword = false; - upAOnController1 = false; - removeFlashing = true; - sprite = CharacterSprite.LINK; - spriteName = CharacterSprite.LINK.DisplayName!; - westClimate = ClimateEnum.CLASSIC; - eastClimate = ClimateEnum.CLASSIC; - dmClimate = ClimateEnum.CLASSIC; - if (sprite == null) - { - throw new ImpossibleException(); - } - tunic = NesColor.Default; - skinTone = NesColor.Default; - tunicOutline = NesColor.Default; - shieldTunic = NesColor.Default; - beamSprite = BeamSprites.DEFAULT; - useCustomRooms = false; - disableHUDLag = false; - } - public RandomizerConfiguration(string flagstring) : this() { Deserialize(flagstring); From 3fad1bbbc3a84e365a3ee0b8dd62e92ae0aa310f Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 17 Jun 2026 10:53:11 -0400 Subject: [PATCH 04/56] Remove any fields from the presets that are vanilla and add vanilla preset --- CrossPlatformUI/Presets/BeginnerPreset.cs | 43 --------------- CrossPlatformUI/Presets/FullShufflePreset.cs | 29 ----------- CrossPlatformUI/Presets/MaxRando2025Preset.cs | 26 ---------- CrossPlatformUI/Presets/MaxRandoPreset.cs | 16 ------ CrossPlatformUI/Presets/NormalPreset.cs | 39 -------------- .../Presets/RandomPercentPreset.cs | 14 ----- CrossPlatformUI/Presets/Sgl2025Preset.cs | 52 ------------------- CrossPlatformUI/Presets/StandardPreset.cs | 34 ------------ .../Presets/StandardSwissPreset.cs | 34 ------------ .../Presets/UpstartsTournamentPreset.cs | 37 ------------- CrossPlatformUI/Presets/VanillaPreset.cs | 11 ++++ CrossPlatformUI/Views/RandomizerView.axaml | 2 + 12 files changed, 13 insertions(+), 324 deletions(-) create mode 100644 CrossPlatformUI/Presets/VanillaPreset.cs diff --git a/CrossPlatformUI/Presets/BeginnerPreset.cs b/CrossPlatformUI/Presets/BeginnerPreset.cs index 7a70ff6e0..f18495687 100644 --- a/CrossPlatformUI/Presets/BeginnerPreset.cs +++ b/CrossPlatformUI/Presets/BeginnerPreset.cs @@ -12,28 +12,14 @@ public static class BeginnerPreset //Start StartWithCandle = true, StartWithLife = true, - ShuffleStartingItems = false, - ShuffleStartingSpells = false, - MaxHeartContainers = MaxHeartsOption.EIGHT, - StartingHeartContainersMin = 4, - StartingHeartContainersMax = 4, StartingTechniques = StartingTechs.DOWNSTAB, StartingLives = StartingLives.Lives5, - IndeterminateOptionRate = IndeterminateOptionRate.HALF, //Overworld - PalacesCanSwapContinents = false, - ShuffleGP = false, - ShuffleEncounters = false, - AllowUnsafePathEncounters = false, - IncludeLavaInEncounterShuffle = false, EncounterRate = EncounterRate.HALF, - RiverDevilBlockerOption = RiverDevilBlockerOption.PATH, EastRocks = true, GenerateBaguWoods = false, LessImportantLocationsOption = LessImportantLocationsOption.REMOVE, - RestrictConnectionCaveShuffle = true, - AllowConnectionCavesToBeBlocked = false, GoodBoots = true, HidePalace = false, HideKasuto = false, @@ -47,39 +33,22 @@ public static class BeginnerPreset MazeBiome = Biome.VANILLALIKE, WestClimate = ClimateEnum.VANILLA_WEIGHTED_WEST, EastClimate = ClimateEnum.VANILLA_WEIGHTED_EAST, - DmClimate = ClimateEnum.CLASSIC, - ContinentConnectionType = ContinentConnectionType.NORMAL, //Palaces NormalPalaceStyle = PalaceStyle.VANILLA_WEIGHTED, GpStyle = PalaceStyle.TOWER, NormalPalaceLength = PalaceLengthOption.MEDIUM, GpLength = PalaceLengthOption.SHORT, - IncludeVanillaRooms = true, Includev4_0Rooms = true, Includev5_0Rooms = true, - IncludeExpertRooms = false, - TBirdRequired = true, - RemoveTBird = false, - PalacesToCompleteMin = 6, - PalacesToCompleteMax = 6, RestartAtPalacesOnGameOver = true, Global5050JarDrop = true, ReduceDripperVariance = true, ChangePalacePallettes = true, - RandomizeBossItemDrop = false, - PalaceDropStyle = PalaceDropStyle.ENTRANCE, - BossRoomsExitType = BossRoomsExitType.OVERWORLD, NoDuplicateRoomsByLayout = true, - BlockingRoomsInAnyPalace = false, - HardBosses = false, - PalaceItemRoomCount = PalaceItemRoomCount.ONE, DarkLinkMinDistance = BossRoomMinDistance.SHORT, //Levels - AttackLevelCap = 8, - MagicLevelCap = 8, - LifeLevelCap = 8, AttackEffectiveness = AttackEffectiveness.HIGH, MagicEffectiveness = MagicEffectiveness.LOW_COST, LifeEffectiveness = LifeEffectiveness.HIGH, @@ -87,16 +56,10 @@ public static class BeginnerPreset //Spells ShuffleSpellLocations = true, DisableMagicContainerRequirements = true, - RandomizeSpellSpellEnemy = false, - SwapUpAndDownStab = false, - FireOption = FireOption.NORMAL, //Enemies ShuffleOverworldEnemies = true, ShufflePalaceEnemies = true, - DripperEnemyOption = DripperEnemyOption.ONLY_BOTS, - MixLargeAndSmallEnemies = false, - GeneratorsAlwaysMatch = true, EnemyXPDrops = XPEffectiveness.RANDOM_HIGH, @@ -105,16 +68,10 @@ public static class BeginnerPreset ShuffleOverworldItems = true, MixOverworldAndPalaceItems = true, IncludePBagCavesInItemShuffle = true, - IncludeSwordTechsInShuffle = false, - IncludeQuestItemsInShuffle = false, - IncludeSpellsInShuffle = false, ShuffleSmallItems = true, - RemoveSpellItems = false, - ShufflePBagAmounts = false, PalacesContainExtraKeys = true, RandomizeNewKasutoJarRequirements = true, - AllowImportantItemDuplicates = false, PreventSpellItemChains = false, //Drops diff --git a/CrossPlatformUI/Presets/FullShufflePreset.cs b/CrossPlatformUI/Presets/FullShufflePreset.cs index 9c20c5ff7..3306ccb64 100644 --- a/CrossPlatformUI/Presets/FullShufflePreset.cs +++ b/CrossPlatformUI/Presets/FullShufflePreset.cs @@ -9,14 +9,6 @@ public static class FullShufflePreset { //Start StartWithCandle = true, - ShuffleStartingItems = false, - ShuffleStartingSpells = false, - MaxHeartContainers = MaxHeartsOption.EIGHT, - StartingHeartContainersMin = 4, - StartingHeartContainersMax = 4, - StartingTechniques = StartingTechs.NONE, - StartingLives = StartingLives.Lives3, - IndeterminateOptionRate = IndeterminateOptionRate.HALF, //Overworld PalacesCanSwapContinents = true, @@ -28,8 +20,6 @@ public static class FullShufflePreset RiverDevilBlockerOption = RiverDevilBlockerOption.RANDOM, EastRocks = true, GenerateBaguWoods = false, - LessImportantLocationsOption = LessImportantLocationsOption.HIDE, - RestrictConnectionCaveShuffle = true, AllowConnectionCavesToBeBlocked = true, GoodBoots = true, HidePalace = null, @@ -41,33 +31,24 @@ public static class FullShufflePreset MazeBiome = Biome.VANILLALIKE, WestClimate = ClimateEnum.VANILLA_WEIGHTED_WEST, EastClimate = ClimateEnum.VANILLA_WEIGHTED_EAST, - DmClimate = ClimateEnum.CLASSIC, ContinentConnectionType = ContinentConnectionType.ANYTHING_GOES, //Palaces NormalPalaceStyle = PalaceStyle.RANDOM_PER_PALACE, GpStyle = PalaceStyle.RANDOM_WALK, - NormalPalaceLength = PalaceLengthOption.FULL, GpLength = PalaceLengthOption.SHORT, - IncludeVanillaRooms = true, Includev4_0Rooms = true, Includev5_0Rooms = true, IncludeExpertRooms = true, - TBirdRequired = true, - RemoveTBird = false, - PalacesToCompleteMin = 6, - PalacesToCompleteMax = 6, RestartAtPalacesOnGameOver = true, Global5050JarDrop = true, ReduceDripperVariance = true, ChangePalacePallettes = true, RandomizeBossItemDrop = true, PalaceDropStyle = PalaceDropStyle.RANDOM, - BossRoomsExitType = BossRoomsExitType.OVERWORLD, NoDuplicateRoomsByLayout = true, BlockingRoomsInAnyPalace = true, HardBosses = true, - PalaceItemRoomCount = PalaceItemRoomCount.ONE, DarkLinkMinDistance = BossRoomMinDistance.SHORT, //Levels @@ -75,9 +56,6 @@ public static class FullShufflePreset ShuffleMagicExperience = true, ShuffleLifeExperience = true, - AttackLevelCap = 8, - MagicLevelCap = 8, - LifeLevelCap = 8, AttackEffectiveness = AttackEffectiveness.AVERAGE, MagicEffectiveness = MagicEffectiveness.AVERAGE, LifeEffectiveness = LifeEffectiveness.AVERAGE, @@ -87,7 +65,6 @@ public static class FullShufflePreset ShuffleSpellLocations = true, DisableMagicContainerRequirements = true, RandomizeSpellSpellEnemy = true, - SwapUpAndDownStab = false, FireOption = FireOption.PAIR_WITH_RANDOM, //Enemies @@ -95,7 +72,6 @@ public static class FullShufflePreset ShufflePalaceEnemies = true, DripperEnemyOption = DripperEnemyOption.ANY_GROUND_ENEMY, MixLargeAndSmallEnemies = true, - GeneratorsAlwaysMatch = true, ShuffleEnemyHP = EnemyLifeOption.MEDIUM, ShuffleBossHP = EnemyLifeOption.MEDIUM, @@ -114,11 +90,7 @@ public static class FullShufflePreset IncludeSpellsInShuffle = true, ShuffleSmallItems = true, - RemoveSpellItems = false, - ShufflePBagAmounts = false, - PalacesContainExtraKeys = false, RandomizeNewKasutoJarRequirements = true, - AllowImportantItemDuplicates = false, PreventSpellItemChains = false, //Drops @@ -131,6 +103,5 @@ public static class FullShufflePreset EnableSpellItemHints = true, EnableTownNameHints = true, - RevealWalkthroughWalls = false, }; } diff --git a/CrossPlatformUI/Presets/MaxRando2025Preset.cs b/CrossPlatformUI/Presets/MaxRando2025Preset.cs index 4f6a7f105..856a56026 100644 --- a/CrossPlatformUI/Presets/MaxRando2025Preset.cs +++ b/CrossPlatformUI/Presets/MaxRando2025Preset.cs @@ -14,7 +14,6 @@ public static class MaxRando2025Preset StartingHeartContainersMax = 8, StartingTechniques = StartingTechs.RANDOM, StartingLives = StartingLives.LivesRandom, - IndeterminateOptionRate = IndeterminateOptionRate.HALF, //Overworld PalacesCanSwapContinents = true, @@ -23,11 +22,6 @@ public static class MaxRando2025Preset AllowUnsafePathEncounters = true, IncludeLavaInEncounterShuffle = true, EncounterRate = EncounterRate.HALF, - RiverDevilBlockerOption = RiverDevilBlockerOption.PATH, - EastRocks = false, - GenerateBaguWoods = true, - LessImportantLocationsOption = LessImportantLocationsOption.HIDE, - RestrictConnectionCaveShuffle = true, AllowConnectionCavesToBeBlocked = true, GoodBoots = true, HidePalace = null, @@ -45,39 +39,25 @@ public static class MaxRando2025Preset //Palaces NormalPalaceStyle = PalaceStyle.RECONSTRUCTED, GpStyle = PalaceStyle.RECONSTRUCTED, - RandomStylesAllowVanilla = false, - NormalPalaceLength = PalaceLengthOption.FULL, GpLength = PalaceLengthOption.RANDOM, - IncludeVanillaRooms = true, Includev4_0Rooms = true, - Includev5_0Rooms = false, IncludeExpertRooms = true, TBirdRequired = false, - RemoveTBird = false, PalacesToCompleteMin = 0, - PalacesToCompleteMax = 6, RestartAtPalacesOnGameOver = true, - Global5050JarDrop = false, - ReduceDripperVariance = false, ChangePalacePallettes = true, RandomizeBossItemDrop = true, PalaceDropStyle = PalaceDropStyle.ANY_EXIT, BossRoomsExitType = BossRoomsExitType.PALACE, - NoDuplicateRoomsByLayout = false, NoDuplicateRoomsByEnemies = true, BlockingRoomsInAnyPalace = true, HardBosses = true, - PalaceItemRoomCount = PalaceItemRoomCount.ONE, - DarkLinkMinDistance = BossRoomMinDistance.NONE, //Levels ShuffleAttackExperience = true, ShuffleMagicExperience = true, ShuffleLifeExperience = true, - AttackLevelCap = 8, - MagicLevelCap = 8, - LifeLevelCap = 8, AttackEffectiveness = AttackEffectiveness.AVERAGE, MagicEffectiveness = MagicEffectiveness.AVERAGE, LifeEffectiveness = LifeEffectiveness.AVERAGE, @@ -87,7 +67,6 @@ public static class MaxRando2025Preset ShuffleSpellLocations = true, DisableMagicContainerRequirements = true, RandomizeSpellSpellEnemy = true, - SwapUpAndDownStab = false, FireOption = FireOption.RANDOM, //Enemies @@ -95,7 +74,6 @@ public static class MaxRando2025Preset ShufflePalaceEnemies = true, DripperEnemyOption = DripperEnemyOption.ANY_GROUND_ENEMY, MixLargeAndSmallEnemies = true, - GeneratorsAlwaysMatch = true, ShuffleEnemyHP = EnemyLifeOption.MEDIUM, ShuffleBossHP = EnemyLifeOption.MEDIUM, @@ -114,11 +92,8 @@ public static class MaxRando2025Preset IncludeSpellsInShuffle = true, ShuffleSmallItems = true, - RemoveSpellItems = false, ShufflePBagAmounts = true, - PalacesContainExtraKeys = false, RandomizeNewKasutoJarRequirements = true, - AllowImportantItemDuplicates = false, PreventSpellItemChains = false, //Drops @@ -131,6 +106,5 @@ public static class MaxRando2025Preset EnableSpellItemHints = true, EnableTownNameHints = true, - RevealWalkthroughWalls = false, }; } diff --git a/CrossPlatformUI/Presets/MaxRandoPreset.cs b/CrossPlatformUI/Presets/MaxRandoPreset.cs index b807f7f1b..0e57f46e8 100644 --- a/CrossPlatformUI/Presets/MaxRandoPreset.cs +++ b/CrossPlatformUI/Presets/MaxRandoPreset.cs @@ -16,7 +16,6 @@ public static class MaxRandoPreset StartingMagicContainersMax = 8, StartingTechniques = StartingTechs.RANDOM, StartingLives = StartingLives.LivesRandom, - IndeterminateOptionRate = IndeterminateOptionRate.HALF, //Overworld PalacesCanSwapContinents = true, @@ -29,7 +28,6 @@ public static class MaxRandoPreset EastRocks = null, GenerateBaguWoods = null, LessImportantLocationsOption = LessImportantLocationsOption.RANDOM, - RestrictConnectionCaveShuffle = true, AllowConnectionCavesToBeBlocked = true, GoodBoots = true, HidePalace = null, @@ -47,17 +45,13 @@ public static class MaxRandoPreset //Palaces NormalPalaceStyle = PalaceStyle.RANDOM_PER_PALACE, GpStyle = PalaceStyle.RANDOM, - RandomStylesAllowVanilla = false, NormalPalaceLength = PalaceLengthOption.RANDOM, GpLength = PalaceLengthOption.RANDOM, - IncludeVanillaRooms = true, Includev4_0Rooms = true, Includev5_0Rooms = true, IncludeExpertRooms = true, TBirdRequired = false, - RemoveTBird = false, PalacesToCompleteMin = 0, - PalacesToCompleteMax = 6, RestartAtPalacesOnGameOver = true, Global5050JarDrop = true, ReduceDripperVariance = true, @@ -69,16 +63,12 @@ public static class MaxRandoPreset BlockingRoomsInAnyPalace = true, HardBosses = true, PalaceItemRoomCount = PalaceItemRoomCount.RANDOM_INCLUDE_ZERO, - DarkLinkMinDistance = BossRoomMinDistance.NONE, //Levels ShuffleAttackExperience = true, ShuffleMagicExperience = true, ShuffleLifeExperience = true, - AttackLevelCap = 8, - MagicLevelCap = 8, - LifeLevelCap = 8, AttackEffectiveness = AttackEffectiveness.AVERAGE, MagicEffectiveness = MagicEffectiveness.AVERAGE, LifeEffectiveness = LifeEffectiveness.AVERAGE, @@ -88,7 +78,6 @@ public static class MaxRandoPreset ShuffleSpellLocations = true, DisableMagicContainerRequirements = true, RandomizeSpellSpellEnemy = true, - SwapUpAndDownStab = false, FireOption = FireOption.RANDOM, //Enemies @@ -96,7 +85,6 @@ public static class MaxRandoPreset ShufflePalaceEnemies = true, DripperEnemyOption = DripperEnemyOption.ANY_GROUND_ENEMY, MixLargeAndSmallEnemies = true, - GeneratorsAlwaysMatch = true, ShuffleEnemyHP = EnemyLifeOption.MEDIUM, ShuffleBossHP = EnemyLifeOption.MEDIUM, @@ -115,11 +103,8 @@ public static class MaxRandoPreset IncludeSpellsInShuffle = true, ShuffleSmallItems = true, - RemoveSpellItems = false, ShufflePBagAmounts = true, - PalacesContainExtraKeys = false, RandomizeNewKasutoJarRequirements = true, - AllowImportantItemDuplicates = false, PreventSpellItemChains = false, //Drops @@ -132,6 +117,5 @@ public static class MaxRandoPreset EnableSpellItemHints = true, EnableTownNameHints = true, - RevealWalkthroughWalls = false, }; } diff --git a/CrossPlatformUI/Presets/NormalPreset.cs b/CrossPlatformUI/Presets/NormalPreset.cs index da9c12433..a6d7d5df1 100644 --- a/CrossPlatformUI/Presets/NormalPreset.cs +++ b/CrossPlatformUI/Presets/NormalPreset.cs @@ -9,32 +9,19 @@ public static class NormalPreset { //Start StartWithCandle = true, - ShuffleStartingItems = false, - ShuffleStartingSpells = false, - MaxHeartContainers = MaxHeartsOption.EIGHT, - StartingHeartContainersMin = 4, - StartingHeartContainersMax = 4, - StartingTechniques = StartingTechs.NONE, - StartingLives = StartingLives.Lives3, - IndeterminateOptionRate = IndeterminateOptionRate.HALF, //Overworld PalacesCanSwapContinents = true, ShuffleGP = true, - ShuffleEncounters = false, - AllowUnsafePathEncounters = false, - IncludeLavaInEncounterShuffle = false, EncounterRate = EncounterRate.HALF, RiverDevilBlockerOption = RiverDevilBlockerOption.RANDOM, EastRocks = true, GenerateBaguWoods = false, LessImportantLocationsOption = LessImportantLocationsOption.REMOVE, - RestrictConnectionCaveShuffle = true, AllowConnectionCavesToBeBlocked = true, GoodBoots = true, HidePalace = null, HideKasuto = null, - ShuffleWhichLocationIsHidden = false, DmSize = DmSizeOption.MEDIUM, WestBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, EastBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, @@ -42,7 +29,6 @@ public static class NormalPreset DmBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, WestClimate = ClimateEnum.VANILLA_WEIGHTED_WEST, EastClimate = ClimateEnum.VANILLA_WEIGHTED_EAST, - DmClimate = ClimateEnum.CLASSIC, ContinentConnectionType = ContinentConnectionType.TRANSPORTATION_SHUFFLE, //Palaces @@ -50,25 +36,16 @@ public static class NormalPreset GpStyle = PalaceStyle.RANDOM_WALK, NormalPalaceLength = PalaceLengthOption.MEDIUM, GpLength = PalaceLengthOption.SHORT, - IncludeVanillaRooms = true, Includev4_0Rooms = true, Includev5_0Rooms = true, - IncludeExpertRooms = false, - TBirdRequired = true, - RemoveTBird = false, - PalacesToCompleteMin = 6, - PalacesToCompleteMax = 6, RestartAtPalacesOnGameOver = true, Global5050JarDrop = true, ReduceDripperVariance = true, ChangePalacePallettes = true, - RandomizeBossItemDrop = false, PalaceDropStyle = PalaceDropStyle.RANDOM, - BossRoomsExitType = BossRoomsExitType.OVERWORLD, NoDuplicateRoomsByLayout = true, BlockingRoomsInAnyPalace = true, HardBosses = true, - PalaceItemRoomCount = PalaceItemRoomCount.ONE, DarkLinkMinDistance = BossRoomMinDistance.SHORT, //Levels @@ -76,27 +53,18 @@ public static class NormalPreset ShuffleMagicExperience = true, ShuffleLifeExperience = true, - AttackLevelCap = 8, - MagicLevelCap = 8, - LifeLevelCap = 8, AttackEffectiveness = AttackEffectiveness.AVERAGE, MagicEffectiveness = MagicEffectiveness.AVERAGE, LifeEffectiveness = LifeEffectiveness.AVERAGE, //Spells - ShuffleLifeRefillAmount = false, ShuffleSpellLocations = true, DisableMagicContainerRequirements = true, - RandomizeSpellSpellEnemy = false, - SwapUpAndDownStab = false, - FireOption = FireOption.NORMAL, //Enemies ShuffleOverworldEnemies = true, ShufflePalaceEnemies = true, DripperEnemyOption = DripperEnemyOption.EASIER_GROUND_ENEMIES, - MixLargeAndSmallEnemies = false, - GeneratorsAlwaysMatch = true, ShuffleEnemyHP = EnemyLifeOption.MEDIUM, ShuffleBossHP = EnemyLifeOption.MEDIUM, @@ -110,16 +78,10 @@ public static class NormalPreset ShuffleOverworldItems = true, MixOverworldAndPalaceItems = true, IncludePBagCavesInItemShuffle = true, - IncludeSwordTechsInShuffle = false, IncludeQuestItemsInShuffle = true, - IncludeSpellsInShuffle = false, ShuffleSmallItems = true, - RemoveSpellItems = false, - ShufflePBagAmounts = false, - PalacesContainExtraKeys = false, RandomizeNewKasutoJarRequirements = true, - AllowImportantItemDuplicates = false, PreventSpellItemChains = false, //Drops @@ -132,6 +94,5 @@ public static class NormalPreset EnableSpellItemHints = true, EnableTownNameHints = true, - RevealWalkthroughWalls = false, }; } diff --git a/CrossPlatformUI/Presets/RandomPercentPreset.cs b/CrossPlatformUI/Presets/RandomPercentPreset.cs index c42abdacc..b88cf28cd 100644 --- a/CrossPlatformUI/Presets/RandomPercentPreset.cs +++ b/CrossPlatformUI/Presets/RandomPercentPreset.cs @@ -15,14 +15,11 @@ public static class RandomPercentPreset StartingHeartContainersMax = 8, StartingTechniques = StartingTechs.RANDOM, StartingLives = StartingLives.LivesRandom, - IndeterminateOptionRate = IndeterminateOptionRate.HALF, //Overworld PalacesCanSwapContinents = null, ShuffleGP = null, ShuffleEncounters = null, - AllowUnsafePathEncounters = false, - IncludeLavaInEncounterShuffle = false, EncounterRate = EncounterRate.HALF, RiverDevilBlockerOption = RiverDevilBlockerOption.RANDOM, EastRocks = null, @@ -53,30 +50,21 @@ public static class RandomPercentPreset Includev4_0Rooms = null, Includev5_0Rooms = null, TBirdRequired = null, - RemoveTBird = false, PalacesToCompleteMin = 0, - PalacesToCompleteMax = 6, RestartAtPalacesOnGameOver = true, Global5050JarDrop = null, - ReduceDripperVariance = false, ChangePalacePallettes = true, RandomizeBossItemDrop = true, PalaceDropStyle = PalaceDropStyle.ANYTHING_GOES, BossRoomsExitType = BossRoomsExitType.RANDOM_PER_PALACE, NoDuplicateRoomsByLayout = true, BlockingRoomsInAnyPalace = true, - HardBosses = false, - PalaceItemRoomCount = PalaceItemRoomCount.ONE, - DarkLinkMinDistance = BossRoomMinDistance.NONE, //Levels ShuffleAttackExperience = true, ShuffleMagicExperience = true, ShuffleLifeExperience = true, - AttackLevelCap = 8, - MagicLevelCap = 8, - LifeLevelCap = 8, AttackEffectiveness = AttackEffectiveness.AVERAGE, MagicEffectiveness = MagicEffectiveness.AVERAGE, LifeEffectiveness = LifeEffectiveness.AVERAGE, @@ -117,7 +105,6 @@ public static class RandomPercentPreset ShufflePBagAmounts = null, PalacesContainExtraKeys = null, RandomizeNewKasutoJarRequirements = true, - AllowImportantItemDuplicates = false, PreventSpellItemChains = false, //Drops @@ -130,6 +117,5 @@ public static class RandomPercentPreset EnableSpellItemHints = null, EnableTownNameHints = null, - RevealWalkthroughWalls = false, }; } diff --git a/CrossPlatformUI/Presets/Sgl2025Preset.cs b/CrossPlatformUI/Presets/Sgl2025Preset.cs index 2bb6ea103..09ed259aa 100644 --- a/CrossPlatformUI/Presets/Sgl2025Preset.cs +++ b/CrossPlatformUI/Presets/Sgl2025Preset.cs @@ -9,40 +9,23 @@ public static class Sgl2025Preset //Start StartWithCandle = true, StartWithCross = true, - ShuffleStartingItems = false, StartWithFire = true, - ShuffleStartingSpells = false, MaxHeartContainers = MaxHeartsOption.SIX, StartingHeartContainersMin = 3, StartingHeartContainersMax = 3, - StartingTechniques = StartingTechs.NONE, - StartingLives = StartingLives.Lives3, - IndeterminateOptionRate = IndeterminateOptionRate.HALF, //Overworld PalacesCanSwapContinents = true, ShuffleGP = true, - ShuffleEncounters = false, - AllowUnsafePathEncounters = false, - IncludeLavaInEncounterShuffle = false, EncounterRate = EncounterRate.HALF, - RiverDevilBlockerOption = RiverDevilBlockerOption.PATH, - EastRocks = false, - GenerateBaguWoods = true, - LessImportantLocationsOption = LessImportantLocationsOption.HIDE, - RestrictConnectionCaveShuffle = true, AllowConnectionCavesToBeBlocked = true, GoodBoots = true, HidePalace = null, HideKasuto = null, - ShuffleWhichLocationIsHidden = false, WestBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, EastBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, MazeBiome = Biome.VANILLALIKE, DmBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, - WestClimate = ClimateEnum.CLASSIC, - EastClimate = ClimateEnum.CLASSIC, - DmClimate = ClimateEnum.CLASSIC, ContinentConnectionType = ContinentConnectionType.TRANSPORTATION_SHUFFLE, //Palaces @@ -50,27 +33,15 @@ public static class Sgl2025Preset GpStyle = PalaceStyle.RECONSTRUCTED, NormalPalaceLength = PalaceLengthOption.SHORT, GpLength = PalaceLengthOption.MEDIUM, - IncludeVanillaRooms = true, Includev4_0Rooms = true, - Includev5_0Rooms = false, IncludeExpertRooms = true, - TBirdRequired = true, - PalacesToCompleteMin = 6, - PalacesToCompleteMax = 6, RestartAtPalacesOnGameOver = true, - Global5050JarDrop = false, - ReduceDripperVariance = false, ChangePalacePallettes = true, - RandomizeBossItemDrop = false, PalaceDropStyle = PalaceDropStyle.ANY_EXIT, - BossRoomsExitType = BossRoomsExitType.OVERWORLD, - NoDuplicateRoomsByLayout = false, NoDuplicateRoomsByEnemies = true, BlockingRoomsInAnyPalace = true, HardBosses = true, AggressiveTbird = true, - PalaceItemRoomCount = PalaceItemRoomCount.ONE, - DarkLinkMinDistance = BossRoomMinDistance.NONE, //Levels ShuffleAttackExperience = true, @@ -78,55 +49,33 @@ public static class Sgl2025Preset ShuffleLifeExperience = true, AttackLevelCap = 6, - MagicLevelCap = 8, - LifeLevelCap = 8, AttackEffectiveness = AttackEffectiveness.AVERAGE_LOW, // was AttackValues = [2, 3, 4, 6, 8, 12, 14, 16]; MagicEffectiveness = MagicEffectiveness.AVERAGE, - LifeEffectiveness = LifeEffectiveness.VANILLA, //Spells - ShuffleLifeRefillAmount = false, ShuffleSpellLocations = true, DisableMagicContainerRequirements = true, - RandomizeSpellSpellEnemy = false, SwapUpAndDownStab = true, - FireOption = FireOption.NORMAL, // +"Expensive Thunder" //Enemies ShuffleOverworldEnemies = true, ShufflePalaceEnemies = true, - DripperEnemyOption = DripperEnemyOption.ONLY_BOTS, MixLargeAndSmallEnemies = true, - GeneratorsAlwaysMatch = true, ShuffleEnemyHP = EnemyLifeOption.MEDIUM, // was 100-150% - ShuffleBossHP = EnemyLifeOption.VANILLA, ShuffleXPStealers = true, ShuffleXPStolenAmount = true, - ShuffleSwordImmunity = false, EnemyXPDrops = XPEffectiveness.SLIGHTLY_HIGH, // was +0 to +2 //Items ShufflePalaceItems = true, ShuffleOverworldItems = true, MixOverworldAndPalaceItems = true, - IncludePBagCavesInItemShuffle = false, - IncludeSwordTechsInShuffle = false, - IncludeQuestItemsInShuffle = false, - IncludeSpellsInShuffle = false, - ShuffleSmallItems = false, - RemoveSpellItems = false, - ShufflePBagAmounts = false, - PalacesContainExtraKeys = false, - RandomizeNewKasutoJarRequirements = false, - AllowImportantItemDuplicates = false, PreventSpellItemChains = false, //Drops - ShuffleItemDropFrequency = false, - RandomizeDrops = false, StandardizeDrops = true, SmallEnemiesCanDropBlueJar = true, LargeEnemiesCanDropRedJar = true, @@ -137,6 +86,5 @@ public static class Sgl2025Preset EnableSpellItemHints = true, EnableTownNameHints = true, - RevealWalkthroughWalls = false, }; } diff --git a/CrossPlatformUI/Presets/StandardPreset.cs b/CrossPlatformUI/Presets/StandardPreset.cs index 4a28d6f8a..676310b29 100644 --- a/CrossPlatformUI/Presets/StandardPreset.cs +++ b/CrossPlatformUI/Presets/StandardPreset.cs @@ -8,15 +8,6 @@ public static class StandardPreset public static readonly RandomizerConfiguration Preset = new() { //Start - StartWithCandle = false, - ShuffleStartingItems = false, - ShuffleStartingSpells = false, - MaxHeartContainers = MaxHeartsOption.EIGHT, - StartingHeartContainersMin = 4, - StartingHeartContainersMax = 4, - StartingTechniques = StartingTechs.NONE, - StartingLives = StartingLives.Lives3, - IndeterminateOptionRate = IndeterminateOptionRate.HALF, //Overworld PalacesCanSwapContinents = true, @@ -28,8 +19,6 @@ public static class StandardPreset RiverDevilBlockerOption = RiverDevilBlockerOption.RANDOM, EastRocks = true, GenerateBaguWoods = false, - LessImportantLocationsOption = LessImportantLocationsOption.HIDE, - RestrictConnectionCaveShuffle = true, AllowConnectionCavesToBeBlocked = true, GoodBoots = true, HidePalace = null, @@ -39,31 +28,21 @@ public static class StandardPreset EastBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, MazeBiome = Biome.VANILLALIKE, DmBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, - WestClimate = ClimateEnum.CLASSIC, - EastClimate = ClimateEnum.CLASSIC, - DmClimate = ClimateEnum.CLASSIC, ContinentConnectionType = ContinentConnectionType.ANYTHING_GOES, //Palaces NormalPalaceStyle = PalaceStyle.RANDOM_WALK, GpStyle = PalaceStyle.RANDOM_WALK, - NormalPalaceLength = PalaceLengthOption.FULL, GpLength = PalaceLengthOption.SHORT, - IncludeVanillaRooms = true, Includev4_0Rooms = true, Includev5_0Rooms = true, IncludeExpertRooms = true, - TBirdRequired = true, - RemoveTBird = false, - PalacesToCompleteMin = 6, - PalacesToCompleteMax = 6, RestartAtPalacesOnGameOver = true, Global5050JarDrop = true, ReduceDripperVariance = true, ChangePalacePallettes = true, RandomizeBossItemDrop = true, PalaceDropStyle = PalaceDropStyle.ANY_EXIT, - BossRoomsExitType = BossRoomsExitType.OVERWORLD, NoDuplicateRoomsByLayout = true, BlockingRoomsInAnyPalace = true, HardBosses = true, @@ -75,9 +54,6 @@ public static class StandardPreset ShuffleMagicExperience = true, ShuffleLifeExperience = true, - AttackLevelCap = 8, - MagicLevelCap = 8, - LifeLevelCap = 8, AttackEffectiveness = AttackEffectiveness.AVERAGE, MagicEffectiveness = MagicEffectiveness.AVERAGE, LifeEffectiveness = LifeEffectiveness.AVERAGE, @@ -87,15 +63,12 @@ public static class StandardPreset ShuffleSpellLocations = true, DisableMagicContainerRequirements = true, RandomizeSpellSpellEnemy = true, - SwapUpAndDownStab = false, FireOption = FireOption.PAIR_WITH_RANDOM, //Enemies ShuffleOverworldEnemies = true, ShufflePalaceEnemies = true, - DripperEnemyOption = DripperEnemyOption.ONLY_BOTS, MixLargeAndSmallEnemies = true, - GeneratorsAlwaysMatch = true, ShuffleEnemyHP = EnemyLifeOption.MEDIUM, ShuffleBossHP = EnemyLifeOption.MEDIUM, @@ -109,16 +82,11 @@ public static class StandardPreset ShuffleOverworldItems = true, MixOverworldAndPalaceItems = true, IncludePBagCavesInItemShuffle = true, - IncludeSwordTechsInShuffle = false, IncludeQuestItemsInShuffle = true, IncludeSpellsInShuffle = true, ShuffleSmallItems = true, - RemoveSpellItems = false, - ShufflePBagAmounts = false, - PalacesContainExtraKeys = false, RandomizeNewKasutoJarRequirements = true, - AllowImportantItemDuplicates = false, PreventSpellItemChains = false, //Drops @@ -131,7 +99,5 @@ public static class StandardPreset EnableSpellItemHints = true, EnableTownNameHints = true, - GenerateSpoiler = false, - RevealWalkthroughWalls = false, }; } diff --git a/CrossPlatformUI/Presets/StandardSwissPreset.cs b/CrossPlatformUI/Presets/StandardSwissPreset.cs index 9fbe0138f..d0b932810 100644 --- a/CrossPlatformUI/Presets/StandardSwissPreset.cs +++ b/CrossPlatformUI/Presets/StandardSwissPreset.cs @@ -9,27 +9,16 @@ public static class StandardSwissPreset { //Start StartWithCandle = true, - ShuffleStartingItems = false, - ShuffleStartingSpells = false, - MaxHeartContainers = MaxHeartsOption.EIGHT, - StartingHeartContainersMin = 4, - StartingHeartContainersMax = 4, - StartingTechniques = StartingTechs.NONE, - StartingLives = StartingLives.Lives3, - IndeterminateOptionRate = IndeterminateOptionRate.HALF, //Overworld PalacesCanSwapContinents = true, ShuffleGP = true, ShuffleEncounters = true, AllowUnsafePathEncounters = true, - IncludeLavaInEncounterShuffle = false, EncounterRate = EncounterRate.HALF, RiverDevilBlockerOption = RiverDevilBlockerOption.RANDOM, EastRocks = true, GenerateBaguWoods = false, - LessImportantLocationsOption = LessImportantLocationsOption.HIDE, - RestrictConnectionCaveShuffle = true, AllowConnectionCavesToBeBlocked = true, GoodBoots = true, HidePalace = null, @@ -39,31 +28,20 @@ public static class StandardSwissPreset EastBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, MazeBiome = Biome.VANILLALIKE, DmBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, - WestClimate = ClimateEnum.CLASSIC, - EastClimate = ClimateEnum.CLASSIC, - DmClimate = ClimateEnum.CLASSIC, ContinentConnectionType = ContinentConnectionType.ANYTHING_GOES, //Palaces NormalPalaceStyle = PalaceStyle.RANDOM_WALK, GpStyle = PalaceStyle.RANDOM_WALK, - NormalPalaceLength = PalaceLengthOption.FULL, GpLength = PalaceLengthOption.SHORT, - IncludeVanillaRooms = true, Includev4_0Rooms = true, Includev5_0Rooms = true, IncludeExpertRooms = true, - TBirdRequired = true, - RemoveTBird = false, - PalacesToCompleteMin = 6, - PalacesToCompleteMax = 6, RestartAtPalacesOnGameOver = true, Global5050JarDrop = true, ReduceDripperVariance = true, ChangePalacePallettes = true, - RandomizeBossItemDrop = false, PalaceDropStyle = PalaceDropStyle.ANY_EXIT, - BossRoomsExitType = BossRoomsExitType.OVERWORLD, NoDuplicateRoomsByLayout = true, BlockingRoomsInAnyPalace = true, HardBosses = true, @@ -75,19 +53,14 @@ public static class StandardSwissPreset ShuffleMagicExperience = true, ShuffleLifeExperience = true, - AttackLevelCap = 8, - MagicLevelCap = 8, - LifeLevelCap = 8, AttackEffectiveness = AttackEffectiveness.AVERAGE, MagicEffectiveness = MagicEffectiveness.AVERAGE, LifeEffectiveness = LifeEffectiveness.AVERAGE, //Spells - ShuffleLifeRefillAmount = false, ShuffleSpellLocations = true, DisableMagicContainerRequirements = true, RandomizeSpellSpellEnemy = true, - SwapUpAndDownStab = false, FireOption = FireOption.PAIR_WITH_RANDOM, //Enemies @@ -95,7 +68,6 @@ public static class StandardSwissPreset ShufflePalaceEnemies = true, DripperEnemyOption = DripperEnemyOption.ANY_GROUND_ENEMY, MixLargeAndSmallEnemies = true, - GeneratorsAlwaysMatch = true, ShuffleEnemyHP = EnemyLifeOption.MEDIUM, ShuffleBossHP = EnemyLifeOption.MEDIUM, @@ -109,16 +81,11 @@ public static class StandardSwissPreset ShuffleOverworldItems = true, MixOverworldAndPalaceItems = true, IncludePBagCavesInItemShuffle = true, - IncludeSwordTechsInShuffle = false, IncludeQuestItemsInShuffle = true, IncludeSpellsInShuffle = true, ShuffleSmallItems = true, - RemoveSpellItems = false, - ShufflePBagAmounts = false, - PalacesContainExtraKeys = false, RandomizeNewKasutoJarRequirements = true, - AllowImportantItemDuplicates = false, PreventSpellItemChains = false, //Drops @@ -131,7 +98,6 @@ public static class StandardSwissPreset EnableSpellItemHints = true, EnableTownNameHints = true, - GenerateSpoiler = false, RevealWalkthroughWalls = true, }; } diff --git a/CrossPlatformUI/Presets/UpstartsTournamentPreset.cs b/CrossPlatformUI/Presets/UpstartsTournamentPreset.cs index 9ea3f0899..6dc904486 100644 --- a/CrossPlatformUI/Presets/UpstartsTournamentPreset.cs +++ b/CrossPlatformUI/Presets/UpstartsTournamentPreset.cs @@ -9,14 +9,6 @@ public static class UpstartsTournamentPreset { //Start StartWithCandle = true, - ShuffleStartingItems = false, - ShuffleStartingSpells = false, - MaxHeartContainers = MaxHeartsOption.EIGHT, - StartingHeartContainersMin = 4, - StartingHeartContainersMax = 4, - StartingTechniques = StartingTechs.NONE, - StartingLives = StartingLives.Lives3, - IndeterminateOptionRate = IndeterminateOptionRate.HALF, //Overworld PalacesCanSwapContinents = true, @@ -25,10 +17,6 @@ public static class UpstartsTournamentPreset AllowUnsafePathEncounters = true, IncludeLavaInEncounterShuffle = true, EncounterRate = EncounterRate.HALF, - EastRocks = false, - GenerateBaguWoods = true, - LessImportantLocationsOption = LessImportantLocationsOption.HIDE, - RestrictConnectionCaveShuffle = true, AllowConnectionCavesToBeBlocked = true, GoodBoots = true, HidePalace = null, @@ -39,39 +27,24 @@ public static class UpstartsTournamentPreset EastBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, MazeBiome = Biome.VANILLALIKE, DmBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, - WestClimate = ClimateEnum.CLASSIC, - EastClimate = ClimateEnum.CLASSIC, - DmClimate = ClimateEnum.CLASSIC, //Palaces NormalPalaceStyle = PalaceStyle.RECONSTRUCTED, GpStyle = PalaceStyle.RECONSTRUCTED, - NormalPalaceLength = PalaceLengthOption.FULL, GpLength = PalaceLengthOption.SHORT, - IncludeVanillaRooms = true, Includev4_0Rooms = true, - Includev5_0Rooms = false, IncludeExpertRooms = true, - TBirdRequired = true, - PalacesToCompleteMin = 6, - PalacesToCompleteMax = 6, RestartAtPalacesOnGameOver = true, ChangePalacePallettes = true, PalaceDropStyle = PalaceDropStyle.ANY_EXIT, - BossRoomsExitType = BossRoomsExitType.OVERWORLD, - NoDuplicateRoomsByLayout = false, NoDuplicateRoomsByEnemies = true, BlockingRoomsInAnyPalace = true, HardBosses = true, - PalaceItemRoomCount = PalaceItemRoomCount.ONE, //Levels ShuffleAttackExperience = true, ShuffleMagicExperience = true, ShuffleLifeExperience = true, - AttackLevelCap = 8, - MagicLevelCap = 8, - LifeLevelCap = 8, AttackEffectiveness = AttackEffectiveness.AVERAGE, MagicEffectiveness = MagicEffectiveness.AVERAGE, LifeEffectiveness = LifeEffectiveness.AVERAGE, @@ -80,8 +53,6 @@ public static class UpstartsTournamentPreset ShuffleLifeRefillAmount = true, ShuffleSpellLocations = true, DisableMagicContainerRequirements = true, - RandomizeSpellSpellEnemy = false, - SwapUpAndDownStab = false, FireOption = FireOption.PAIR_WITH_RANDOM, //Enemies @@ -89,7 +60,6 @@ public static class UpstartsTournamentPreset ShufflePalaceEnemies = true, DripperEnemyOption = DripperEnemyOption.ANY_GROUND_ENEMY, MixLargeAndSmallEnemies = true, - GeneratorsAlwaysMatch = true, ShuffleEnemyHP = EnemyLifeOption.MEDIUM, ShuffleBossHP = EnemyLifeOption.MEDIUM, @@ -103,16 +73,10 @@ public static class UpstartsTournamentPreset ShuffleOverworldItems = true, MixOverworldAndPalaceItems = true, IncludePBagCavesInItemShuffle = true, - IncludeSwordTechsInShuffle = false, - IncludeQuestItemsInShuffle = false, - IncludeSpellsInShuffle = false, ShuffleSmallItems = true, - RemoveSpellItems = false, ShufflePBagAmounts = true, - PalacesContainExtraKeys = false, RandomizeNewKasutoJarRequirements = true, - AllowImportantItemDuplicates = false, PreventSpellItemChains = false, //Drops @@ -125,6 +89,5 @@ public static class UpstartsTournamentPreset EnableSpellItemHints = true, EnableTownNameHints = true, - RevealWalkthroughWalls = false, }; } diff --git a/CrossPlatformUI/Presets/VanillaPreset.cs b/CrossPlatformUI/Presets/VanillaPreset.cs new file mode 100644 index 000000000..9755612b0 --- /dev/null +++ b/CrossPlatformUI/Presets/VanillaPreset.cs @@ -0,0 +1,11 @@ +using Z2Randomizer.RandomizerCore; + +namespace CrossPlatformUI.Presets; + +/// +/// Uses all the default values, which should always be vanilla. +/// +public static class VanillaPreset +{ + public static readonly RandomizerConfiguration Preset = new(); +} diff --git a/CrossPlatformUI/Views/RandomizerView.axaml b/CrossPlatformUI/Views/RandomizerView.axaml index 43dc2d822..e92492c3b 100644 --- a/CrossPlatformUI/Views/RandomizerView.axaml +++ b/CrossPlatformUI/Views/RandomizerView.axaml @@ -41,6 +41,8 @@ CommandParameter="{Binding Source={x:Static presets:MaxRandoPreset.Preset}}"/> + Date: Sun, 12 Jul 2026 07:49:36 +0200 Subject: [PATCH 05/56] Slight tweaks to RandomizerConfiguration defaults --- .../FlagsSerializeGenerator.cs | 46 +++++++++++-------- RandomizerCore/RandomizerConfiguration.cs | 37 ++------------- 2 files changed, 31 insertions(+), 52 deletions(-) diff --git a/CoreSourceGenerator/FlagsSerializeGenerator.cs b/CoreSourceGenerator/FlagsSerializeGenerator.cs index 55882e30d..b8335becf 100644 --- a/CoreSourceGenerator/FlagsSerializeGenerator.cs +++ b/CoreSourceGenerator/FlagsSerializeGenerator.cs @@ -58,7 +58,7 @@ private static bool IsPartialClass(SyntaxNode node) .Select(f => { // Look up the source code for the declaration to find if it has a default value - var equalsSyntax = f.DeclaringSyntaxReferences[0].GetSyntax() switch + var equalsSyntax = f.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() switch { PropertyDeclarationSyntax property => property.Initializer, VariableDeclaratorSyntax variable => variable.Initializer, @@ -81,18 +81,32 @@ private static bool IsPartialClass(SyntaxNode node) var serializeFields = classSymbol.GetMembers() .OfType() .Where(f => !HasIgnoreInFlagsAttribute(f)) - .Select(f => new SerializedFieldInfo() + .Select(f => { - FieldName = f.Name, - FieldType = f.Type.ToDisplayString(), - IsDifficultyOnly = HasDifficultyOnlyAttribute(f), - IsConditionallyIncluded = HasConditionallyIncludedInFlagsAttribute(f), - DefaultValue = GetDefaultValue(f), - IsEnum = f.Type.TypeKind == TypeKind.Enum, - EnumSymbol = f.Type.TypeKind == TypeKind.Enum ? f.Type as INamedTypeSymbol : null, - Minimum = GetCustomMinimum(f), - Maximum = GetCustomMaximum(f), - CustomSerializerName = GetCustomFlagSerializer(f), + var dictionaryInterface = f.Type.AllInterfaces + .FirstOrDefault(i => i.OriginalDefinition.ToDisplayString().StartsWith("System.Collections.Generic.IDictionary")); + var innerType = dictionaryInterface != null ? dictionaryInterface.TypeArguments[0] : f.Type; + + var equalsSyntax = f.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() switch + { + PropertyDeclarationSyntax property => property.Initializer, + VariableDeclaratorSyntax variable => variable.Initializer, + _ => null + }; + + return new SerializedFieldInfo() + { + FieldName = f.Name, + FieldType = f.Type.ToDisplayString(), + IsDifficultyOnly = HasDifficultyOnlyAttribute(f), + IsConditionallyIncluded = HasConditionallyIncludedInFlagsAttribute(f), + DefaultValue = equalsSyntax?.Value.ToString(), + IsEnum = f.Type.TypeKind == TypeKind.Enum, + EnumSymbol = f.Type.TypeKind == TypeKind.Enum ? f.Type as INamedTypeSymbol : null, + Minimum = GetCustomMinimum(f), + Maximum = GetCustomMaximum(f), + CustomSerializerName = GetCustomFlagSerializer(f), + }; }).ToList(); classInfo.SerializedFields.AddRange(serializeFields); @@ -140,14 +154,6 @@ private static bool HasDifficultyOnlyAttribute(IFieldSymbol field) .Any(attr => attr.AttributeClass?.Name.StartsWith("DifficultyOnly") ?? false); } - private static string GetDefaultValue(IFieldSymbol f) - { - var defaultAttr = f.GetAttributes() - .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "System.ComponentModel.DefaultValueAttribute"); - var arg = defaultAttr?.ConstructorArguments[0]; - return arg != null ? FormatArgument(arg.Value) : "default"; - } - private static bool HasIgnoreInFlagsAttribute(IFieldSymbol field) { return field.GetAttributes() diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index 278328401..df1740b34 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -4,8 +4,6 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Linq; -using System.Linq.Expressions; -using System.Reflection; using System.Runtime.CompilerServices; using NLog; using Z2Randomizer.RandomizerCore.Flags; @@ -274,10 +272,10 @@ public sealed partial class RandomizerConfiguration() : INotifyPropertyChanged private Biome mazeBiome = Biome.VANILLA; [Reactive] - private ClimateEnum westClimate = ClimateEnum.CLASSIC; + private ClimateEnum westClimate = ClimateEnum.VANILLA_WEIGHTED_WEST; [Reactive] - private ClimateEnum eastClimate = ClimateEnum.CLASSIC; + private ClimateEnum eastClimate = ClimateEnum.VANILLA_WEIGHTED_EAST; [Reactive] private ClimateEnum dmClimate = ClimateEnum.CLASSIC; @@ -373,7 +371,7 @@ private bool palaceStylesAnyMetastyleSelected() public bool blockingRoomsInAnyPalaceIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); [Reactive] - private PalaceDropStyle palaceDropStyle = PalaceDropStyle.ENTRANCE; + private PalaceDropStyle palaceDropStyle = PalaceDropStyle.ANY_EXIT; public bool palaceDropStyleIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); [Reactive] @@ -433,7 +431,6 @@ private bool palaceStylesAnyMetastyleSelected() [Minimum(0)] [Maximum(6)] [ConditionallyIncludeInFlags] - [DefaultValue(6)] private int palacesToCompleteMax = 6; public bool palacesToCompleteMaxIncluded() => palacesToCompleteMin != 6; @@ -729,7 +726,7 @@ private bool palaceStylesAnyMetastyleSelected() [Reactive] [IgnoreInFlags] - private bool removeFlashing = false; + private bool removeFlashing = true; [Reactive] [IgnoreInFlags] @@ -794,7 +791,7 @@ private bool palaceStylesAnyMetastyleSelected() private RiverDevilBlockerOption riverDevilBlockerOption = RiverDevilBlockerOption.PATH; [Reactive] - private bool? eastRocks = false; + private bool? eastRocks = true; [Reactive] private bool generateSpoiler = false; @@ -1964,28 +1961,4 @@ private void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - - /// - /// Should be called with the field to reset as the expression body: - /// SetFieldToDefault(() => SettingToReset); - /// - public void SetFieldToDefault(Expression> fieldExpr) - { - if (fieldExpr.Body is not MemberExpression member || member.Member is not FieldInfo field) - { - throw new ArgumentException("Expression must reference a field"); - } - - var target = ((ConstantExpression)member.Expression!).Value; - - var attr = field.GetCustomAttribute(); - if (attr != null) - { - field.SetValue(target, attr.Value); - } - else - { - field.SetValue(target, default); - } - } } From 635036a26cfdbe2a3873216192b85b9f8f75fef3 Mon Sep 17 00:00:00 2001 From: initsu Date: Sun, 31 May 2026 22:54:05 +0200 Subject: [PATCH 06/56] Change Climate variants handling to make enums smaller --- CrossPlatformUI/Presets/BeginnerPreset.cs | 4 ++-- CrossPlatformUI/Presets/FullShufflePreset.cs | 4 ++-- CrossPlatformUI/Presets/NormalPreset.cs | 4 ++-- RandomizerCore/EnumTypes.cs | 18 ++++-------------- RandomizerCore/Overworld/Climates.cs | 18 ++++++++++-------- RandomizerCore/Overworld/DeathMountain.cs | 2 +- RandomizerCore/Overworld/EastHyrule.cs | 2 +- RandomizerCore/Overworld/WestHyrule.cs | 2 +- RandomizerCore/Overworld/World.cs | 2 +- RandomizerCore/RandomizerConfiguration.cs | 4 ++-- 10 files changed, 26 insertions(+), 34 deletions(-) diff --git a/CrossPlatformUI/Presets/BeginnerPreset.cs b/CrossPlatformUI/Presets/BeginnerPreset.cs index f18495687..e12092791 100644 --- a/CrossPlatformUI/Presets/BeginnerPreset.cs +++ b/CrossPlatformUI/Presets/BeginnerPreset.cs @@ -31,8 +31,8 @@ public static class BeginnerPreset EastBiome = Biome.VANILLALIKE, DmBiome = Biome.VANILLALIKE, MazeBiome = Biome.VANILLALIKE, - WestClimate = ClimateEnum.VANILLA_WEIGHTED_WEST, - EastClimate = ClimateEnum.VANILLA_WEIGHTED_EAST, + WestClimate = ClimateEnum.VANILLA_WEIGHTED, + EastClimate = ClimateEnum.VANILLA_WEIGHTED, //Palaces NormalPalaceStyle = PalaceStyle.VANILLA_WEIGHTED, diff --git a/CrossPlatformUI/Presets/FullShufflePreset.cs b/CrossPlatformUI/Presets/FullShufflePreset.cs index 3306ccb64..28e01dd18 100644 --- a/CrossPlatformUI/Presets/FullShufflePreset.cs +++ b/CrossPlatformUI/Presets/FullShufflePreset.cs @@ -29,8 +29,8 @@ public static class FullShufflePreset EastBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, DmBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, MazeBiome = Biome.VANILLALIKE, - WestClimate = ClimateEnum.VANILLA_WEIGHTED_WEST, - EastClimate = ClimateEnum.VANILLA_WEIGHTED_EAST, + WestClimate = ClimateEnum.VANILLA_WEIGHTED, + EastClimate = ClimateEnum.VANILLA_WEIGHTED, ContinentConnectionType = ContinentConnectionType.ANYTHING_GOES, //Palaces diff --git a/CrossPlatformUI/Presets/NormalPreset.cs b/CrossPlatformUI/Presets/NormalPreset.cs index a6d7d5df1..e994d94f1 100644 --- a/CrossPlatformUI/Presets/NormalPreset.cs +++ b/CrossPlatformUI/Presets/NormalPreset.cs @@ -27,8 +27,8 @@ public static class NormalPreset EastBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, MazeBiome = Biome.VANILLALIKE, DmBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE, - WestClimate = ClimateEnum.VANILLA_WEIGHTED_WEST, - EastClimate = ClimateEnum.VANILLA_WEIGHTED_EAST, + WestClimate = ClimateEnum.VANILLA_WEIGHTED, + EastClimate = ClimateEnum.VANILLA_WEIGHTED, ContinentConnectionType = ContinentConnectionType.TRANSPORTATION_SHUFFLE, //Palaces diff --git a/RandomizerCore/EnumTypes.cs b/RandomizerCore/EnumTypes.cs index 78bd161bd..44715fb34 100644 --- a/RandomizerCore/EnumTypes.cs +++ b/RandomizerCore/EnumTypes.cs @@ -414,9 +414,7 @@ public enum ClimateEnum [Description("Classic"), DefaultWeight(1)] CLASSIC, [Description("Vanilla-Weighted"), DefaultWeight(1)] - VANILLA_WEIGHTED_WEST, - [Description("Vanilla-Weighted"), DefaultWeight(1)] - VANILLA_WEIGHTED_EAST, + VANILLA_WEIGHTED, [Description("Chaos"), DefaultWeight(1)] CHAOS, [Description("Wetlands"), DefaultWeight(0)] @@ -425,8 +423,6 @@ public enum ClimateEnum GREAT_LAKES, [Description("Scrubland"), DefaultWeight(1)] SCRUBLAND, - [Description("Scrubland"), DefaultWeight(1)] - DM_SCRUBLAND, [Description("Random"), Metastyle] RANDOM } @@ -437,9 +433,7 @@ public static bool IsWestClimate(this ClimateEnum climate) { return climate switch { - ClimateEnum.VANILLA_WEIGHTED_WEST => true, - ClimateEnum.VANILLA_WEIGHTED_EAST => false, - ClimateEnum.DM_SCRUBLAND => false, + ClimateEnum.VANILLA_WEIGHTED => true, _ => true, }; } @@ -448,9 +442,7 @@ public static bool IsEastClimate(this ClimateEnum climate) { return climate switch { - ClimateEnum.VANILLA_WEIGHTED_WEST => false, - ClimateEnum.VANILLA_WEIGHTED_EAST => true, - ClimateEnum.DM_SCRUBLAND => false, + ClimateEnum.VANILLA_WEIGHTED => true, _ => true, }; } @@ -459,9 +451,7 @@ public static bool IsDmClimate(this ClimateEnum climate) { return climate switch { - ClimateEnum.VANILLA_WEIGHTED_WEST => false, - ClimateEnum.VANILLA_WEIGHTED_EAST => false, - ClimateEnum.SCRUBLAND => false, + ClimateEnum.VANILLA_WEIGHTED => false, _ => true, }; } diff --git a/RandomizerCore/Overworld/Climates.cs b/RandomizerCore/Overworld/Climates.cs index 0292f11e3..1e8a280de 100644 --- a/RandomizerCore/Overworld/Climates.cs +++ b/RandomizerCore/Overworld/Climates.cs @@ -67,7 +67,7 @@ public static class Climates ( Terrain.ROAD, 328 ), ]), 30, - ClimateEnum.VANILLA_WEIGHTED_WEST + ClimateEnum.VANILLA_WEIGHTED ); private static readonly Climate VANILLA_WEIGHTED_EAST = new( @@ -100,7 +100,7 @@ public static class Climates ( Terrain.ROAD, 128 ), ]), 30, - ClimateEnum.VANILLA_WEIGHTED_EAST + ClimateEnum.VANILLA_WEIGHTED ); private static readonly Climate CHAOS = new( @@ -243,7 +243,7 @@ public static class Climates ( Terrain.ROAD, 2 ), ]), 30, - ClimateEnum.DM_SCRUBLAND + ClimateEnum.SCRUBLAND ); private static readonly Climate GREAT_LAKES = new( @@ -281,18 +281,20 @@ public static class Climates ClimateEnum.GREAT_LAKES ); - public static Climate Create(ClimateEnum climate) + public static Climate Create(Continent continentId, ClimateEnum climate) { return climate switch { ClimateEnum.CLASSIC => CLASSIC.CloneWithInvertedDistances(), - ClimateEnum.VANILLA_WEIGHTED_WEST => VANILLA_WEIGHTED_WEST.Clone(), - ClimateEnum.VANILLA_WEIGHTED_EAST => VANILLA_WEIGHTED_EAST.Clone(), + ClimateEnum.VANILLA_WEIGHTED => continentId is Continent.WEST ? + VANILLA_WEIGHTED_WEST.Clone() : + VANILLA_WEIGHTED_EAST.Clone(), ClimateEnum.CHAOS => CHAOS.CloneWithInvertedDistances(), ClimateEnum.WETLANDS => WETLANDS.CloneWithInvertedDistances(), ClimateEnum.GREAT_LAKES => GREAT_LAKES.CloneWithInvertedDistances(), - ClimateEnum.SCRUBLAND => SCRUBLAND.CloneWithInvertedDistances(), - ClimateEnum.DM_SCRUBLAND => DM_SCRUBLAND.CloneWithInvertedDistances(), + ClimateEnum.SCRUBLAND => continentId is Continent.DM ? + DM_SCRUBLAND.CloneWithInvertedDistances() : + SCRUBLAND.CloneWithInvertedDistances(), _ => throw new NotImplementedException() }; } diff --git a/RandomizerCore/Overworld/DeathMountain.cs b/RandomizerCore/Overworld/DeathMountain.cs index b97c081d2..ce04e8505 100644 --- a/RandomizerCore/Overworld/DeathMountain.cs +++ b/RandomizerCore/Overworld/DeathMountain.cs @@ -191,7 +191,7 @@ .. rom.LoadLocations(LocationID.DM_SPEC_ROCK, 1, terrains), walkableTerrains = new List() { Terrain.DESERT, Terrain.FOREST, Terrain.GRAVE }; randomTerrainFilter = new List() { Terrain.DESERT, Terrain.FOREST, Terrain.GRAVE, Terrain.MOUNTAIN, Terrain.WALKABLEWATER, Terrain.WATER }; - climate = Climates.Create(props.DmClimate); + climate = Climates.Create(continentId, props.DmClimate); climate.SeedTerrainCount = Math.Min(climate.SeedTerrainCount, biome.SeedTerrainLimit()); //SetVanillaCollectables(props.ReplaceFireWithDash); } diff --git a/RandomizerCore/Overworld/EastHyrule.cs b/RandomizerCore/Overworld/EastHyrule.cs index 82dee6322..0db2ddc7c 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -298,7 +298,7 @@ .. rom.LoadLocations(LocationID.EAST_TOWN_OLD_KASUTO, 4, terrains), hiddenKasutoLocation = newKasuto; //Climate filtering - climate = Climates.Create(props.EastClimate); + climate = Climates.Create(continentId, props.EastClimate); climate.SeedTerrainCount = Math.Min(climate.SeedTerrainCount, biome.SeedTerrainLimit()); climate.DisallowTerrain(props.CanWalkOnWaterWithBoots ? Terrain.WATER : Terrain.WALKABLEWATER); } diff --git a/RandomizerCore/Overworld/WestHyrule.cs b/RandomizerCore/Overworld/WestHyrule.cs index 3f1d66fdc..2aa4ec81f 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -266,7 +266,7 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), } //Climate filtering - climate = Climates.Create(props.WestClimate); + climate = Climates.Create(continentId, props.WestClimate); climate.SeedTerrainCount = Math.Min(climate.SeedTerrainCount, biome.SeedTerrainLimit()); climate.DisallowTerrain(props.CanWalkOnWaterWithBoots ? Terrain.WATER : Terrain.WALKABLEWATER); //climate.DisallowTerrain(Terrain.LAVA); diff --git a/RandomizerCore/Overworld/World.cs b/RandomizerCore/Overworld/World.cs index f5546ec9e..7ed719566 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -764,7 +764,7 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr //Debug.WriteLine(GetMapDebug()); int maxBridgeLength = MAXIMUM_BRIDGE_LENGTH[biome]; //Great lakes and bad boots are more likely to be incompleteable, so extend max bridge length to give them a fighting chance - if(canWalkOnWater || climate.Name == Climates.Create(ClimateEnum.GREAT_LAKES).Name) + if (!canWalkOnWater || climate.Name == Climates.Create(continentId, ClimateEnum.GREAT_LAKES).Name) { maxBridgeLength = (int)(maxBridgeLength * 1.5); } diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index df1740b34..325a35485 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -272,10 +272,10 @@ public sealed partial class RandomizerConfiguration() : INotifyPropertyChanged private Biome mazeBiome = Biome.VANILLA; [Reactive] - private ClimateEnum westClimate = ClimateEnum.VANILLA_WEIGHTED_WEST; + private ClimateEnum westClimate = ClimateEnum.VANILLA_WEIGHTED; [Reactive] - private ClimateEnum eastClimate = ClimateEnum.VANILLA_WEIGHTED_EAST; + private ClimateEnum eastClimate = ClimateEnum.VANILLA_WEIGHTED; [Reactive] private ClimateEnum dmClimate = ClimateEnum.CLASSIC; From d2ed946120679bebd77d9badee9c4ae32dae5456 Mon Sep 17 00:00:00 2001 From: initsu Date: Wed, 10 Jun 2026 14:00:37 +0200 Subject: [PATCH 07/56] Update Avalonia to 12.1.0 and other dependencies - Update FtRandoLib reference - Make wasm output setting be local to browser project - Adjust margins changed in Avalonia 12 --- .../CrossPlatformUI.Browser.csproj | 6 +++++ CrossPlatformUI/Views/NesColorDropdown.axaml | 6 ++--- Directory.Build.props | 5 ----- Directory.Packages.props | 22 +++++++++---------- FtRandoLib | 2 +- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj b/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj index 3bbe79176..a63b7f6d5 100644 --- a/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj +++ b/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj @@ -22,8 +22,14 @@ false false + true + true + + + + true diff --git a/CrossPlatformUI/Views/NesColorDropdown.axaml b/CrossPlatformUI/Views/NesColorDropdown.axaml index ac0761779..de79d87a1 100644 --- a/CrossPlatformUI/Views/NesColorDropdown.axaml +++ b/CrossPlatformUI/Views/NesColorDropdown.axaml @@ -3,10 +3,10 @@ xmlns:assists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles" x:Class="CrossPlatformUI.Views.NesColorDropdown" Width="340" Height="40"> - - + + - diff --git a/Directory.Build.props b/Directory.Build.props index 576f389f9..14b26f7cc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,12 +9,7 @@ false partial - true - - - - diff --git a/Directory.Packages.props b/Directory.Packages.props index 46473f16b..b87a26203 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,10 +2,10 @@ true true - 5.3.0 + 5.6.0 7.5.1 - 1.0.7 - 12.0.4 + 1.0.9 + 12.1.0 3.119.4 @@ -30,7 +30,7 @@ - + @@ -38,15 +38,15 @@ - + - - + + - + @@ -55,9 +55,9 @@ - - - + + + diff --git a/FtRandoLib b/FtRandoLib index 068c5f14b..fca211534 160000 --- a/FtRandoLib +++ b/FtRandoLib @@ -1 +1 @@ -Subproject commit 068c5f14bfdbd8768ba09961e8266d3d62be3ff4 +Subproject commit fca2115348e63b939118e154f80e93d6410be3c6 From 2a08c4eb337b9c7ab518be5819a757feea00166f Mon Sep 17 00:00:00 2001 From: initsu Date: Wed, 10 Jun 2026 16:15:27 +0200 Subject: [PATCH 08/56] Fix build warnings --- CoreSourceGenerator/CoreSourceGenerator.csproj | 5 +---- CrossPlatformUI/CrossPlatformUI.csproj | 11 ++++++----- Directory.Build.props | 7 +++---- Directory.Packages.props | 10 +++++++--- RandomizerCore/RandomizerCore.csproj | 2 +- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/CoreSourceGenerator/CoreSourceGenerator.csproj b/CoreSourceGenerator/CoreSourceGenerator.csproj index bed7384f5..b20c92c0b 100644 --- a/CoreSourceGenerator/CoreSourceGenerator.csproj +++ b/CoreSourceGenerator/CoreSourceGenerator.csproj @@ -11,10 +11,7 @@ Debug;Release;Unsafe Debug AnyCPU - - true - true - + true false diff --git a/CrossPlatformUI/CrossPlatformUI.csproj b/CrossPlatformUI/CrossPlatformUI.csproj index 36406d1c1..fa91327a7 100644 --- a/CrossPlatformUI/CrossPlatformUI.csproj +++ b/CrossPlatformUI/CrossPlatformUI.csproj @@ -13,11 +13,6 @@ true - - - $(NoWarn);IL2026 - - true false @@ -35,6 +30,7 @@ + @@ -46,6 +42,11 @@ + + + + + diff --git a/Directory.Build.props b/Directory.Build.props index 14b26f7cc..0b154ef44 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,13 +3,12 @@ net10.0 enable true - false - true - true + true + false false - partial + true diff --git a/Directory.Packages.props b/Directory.Packages.props index b87a26203..e1bae488a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,7 +10,7 @@ - + @@ -34,6 +34,7 @@ + @@ -48,9 +49,12 @@ + + + - - + + diff --git a/RandomizerCore/RandomizerCore.csproj b/RandomizerCore/RandomizerCore.csproj index afe3c142d..78116bdc6 100644 --- a/RandomizerCore/RandomizerCore.csproj +++ b/RandomizerCore/RandomizerCore.csproj @@ -57,7 +57,7 @@ - + From f82e12c02434b35175b226d9dcc7d59c045d0d2c Mon Sep 17 00:00:00 2001 From: initsu Date: Tue, 10 Feb 2026 11:09:43 +0100 Subject: [PATCH 09/56] Clean up leftover App.config of old --- RandomizerCore/App.config | 69 --------------------------------------- Statistics/App.config | 6 ---- 2 files changed, 75 deletions(-) delete mode 100644 RandomizerCore/App.config delete mode 100644 Statistics/App.config diff --git a/RandomizerCore/App.config b/RandomizerCore/App.config deleted file mode 100644 index e0f14fb3a..000000000 --- a/RandomizerCore/App.config +++ /dev/null @@ -1,69 +0,0 @@ - - - - -
- - - - - - - - - - - - False - - - 0 - - - False - - - False - - - False - - - 0 - - - 7 - - - - - - - - - - - - - - - True - - - 0 - - - False - - - - - - True - - - False - - - - diff --git a/Statistics/App.config b/Statistics/App.config deleted file mode 100644 index 56efbc7b5..000000000 --- a/Statistics/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From d80ecc90d62f91f3d2605dd30ad44e25d8e06e18 Mon Sep 17 00:00:00 2001 From: initsu Date: Sat, 30 May 2026 11:25:01 +0200 Subject: [PATCH 10/56] GitHub workflows: Update triggers --- .github/workflows/build-debug.yaml | 4 +++- .github/workflows/build-latest.yaml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-debug.yaml b/.github/workflows/build-debug.yaml index b4ded7d4e..35eb3f934 100644 --- a/.github/workflows/build-debug.yaml +++ b/.github/workflows/build-debug.yaml @@ -5,7 +5,9 @@ on: branches-ignore: - main - testing - - '5.1' + - extras + - sgl + - '5.2' pull_request: env: diff --git a/.github/workflows/build-latest.yaml b/.github/workflows/build-latest.yaml index 06f3f7c8d..54c37e8ae 100644 --- a/.github/workflows/build-latest.yaml +++ b/.github/workflows/build-latest.yaml @@ -5,7 +5,9 @@ on: branches: - main - testing - - '5.1' + - extras + - sgl + - '5.2' env: # I'm not a fan of the telemetry as-is, but this also suppresses some lines in the build log. From 8a39ddb156fad03842b476bc8e971e991a0ec424 Mon Sep 17 00:00:00 2001 From: initsu Date: Mon, 13 Jul 2026 04:44:57 +0200 Subject: [PATCH 11/56] Add IntVector2 methods and tests --- RandomizerCore/Direction.cs | 12 ++ RandomizerCore/IntVector2.cs | 109 +++++++++++++-- Tests/IntVector2Tests.cs | 259 +++++++++++++++++++++++++++++++++++ 3 files changed, 372 insertions(+), 8 deletions(-) create mode 100644 Tests/IntVector2Tests.cs diff --git a/RandomizerCore/Direction.cs b/RandomizerCore/Direction.cs index 623666310..cefd5a202 100644 --- a/RandomizerCore/Direction.cs +++ b/RandomizerCore/Direction.cs @@ -20,6 +20,18 @@ public static Direction Reverse(this Direction direction) }; } + public static IntVector2 ToIntVector2(this Direction direction) + { + return direction switch + { + Direction.NORTH => IntVector2.NORTH, + Direction.SOUTH => IntVector2.SOUTH, + Direction.EAST => IntVector2.EAST, + Direction.WEST => IntVector2.WEST, + _ => throw new ArgumentException("Invalid direction: " + direction) + }; + } + public static int DeltaX(this Direction direction) { return direction switch diff --git a/RandomizerCore/IntVector2.cs b/RandomizerCore/IntVector2.cs index 06a704477..701718899 100644 --- a/RandomizerCore/IntVector2.cs +++ b/RandomizerCore/IntVector2.cs @@ -1,9 +1,25 @@ using System; +using System.Diagnostics; namespace Z2Randomizer.RandomizerCore; public readonly struct IntVector2 : IEquatable { + public static readonly IntVector2 ZERO = new(0, 0); + public static readonly IntVector2 ORIGIN = new(0, 0); + public static readonly IntVector2 NORTH = new(0, -1); + public static readonly IntVector2 EAST = new(1, 0); + public static readonly IntVector2 SOUTH = new(0, 1); + public static readonly IntVector2 WEST = new(-1, 0); + public static readonly IntVector2 NORTH_EAST = new(1, -1); + public static readonly IntVector2 SOUTH_EAST = new(1, 1); + public static readonly IntVector2 SOUTH_WEST = new(-1, 1); + public static readonly IntVector2 NORTH_WEST = new(-1, -1); + + public static readonly IntVector2[] CARDINALS = [NORTH, EAST, SOUTH, WEST]; + public static readonly IntVector2[] ORDINALS = [NORTH_EAST, SOUTH_EAST, SOUTH_WEST, NORTH_WEST]; + public static readonly IntVector2[] DIRECTIONS = [NORTH, NORTH_EAST, EAST, SOUTH_EAST, SOUTH, SOUTH_WEST, WEST, NORTH_WEST]; + public readonly int X; public readonly int Y; @@ -31,12 +47,70 @@ public IntVector2(int x, int y) public static IntVector2 operator *(int k, IntVector2 v) => new(k * v.X, k * v.Y); - public static readonly IntVector2 ZERO = new(0, 0); - public static readonly IntVector2 WEST = new(-1, 0); - public static readonly IntVector2 EAST = new(1, 0); - public static readonly IntVector2 NORTH = new(0, -1); - public static readonly IntVector2 SOUTH = new(0, 1); - public static readonly IntVector2[] CARDINALS = [NORTH, SOUTH, EAST, WEST]; + public static IntVector2 operator /(IntVector2 v, int d) + => new(v.X / d, v.Y / d); + + public static int Dot(IntVector2 a, IntVector2 b) + => a.X * b.X + a.Y * b.Y; + + public static int Cross(IntVector2 a, IntVector2 b) + => a.X * b.Y - a.Y * b.X; + + public IntVector2 ComponentMultiply(IntVector2 other) + => new(X * other.X, Y * other.Y); + + public IntVector2 Normalize() + { + Debug.Assert(X == 0 || Y == 0, "Normalized IntVector2 can't be diagonal"); + if (X != 0) + { + return X > 0 ? EAST : WEST; + } + else if (Y != 0) + { + return Y > 0 ? SOUTH : NORTH; + } + else + { + return ZERO; + } + } + + public bool IsZero + => X == 0 && Y == 0; + + public IntVector2 Abs() + => new(Math.Abs(X), Math.Abs(Y)); + + public int MinComponent() + => Math.Min(X, Y); + + public int MaxComponent() + => Math.Max(X, Y); + + public static IntVector2 Min(IntVector2 a, IntVector2 b) + => new(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y)); + + public static IntVector2 Max(IntVector2 a, IntVector2 b) + => new(Math.Max(a.X, b.X), Math.Max(a.Y, b.Y)); + + public int LengthSquared + => X * X + Y * Y; + + public double Length + => Math.Sqrt(LengthSquared); + + public int ManhattanLength + => Math.Abs(X) + Math.Abs(Y); + + public int DistanceSquared(IntVector2 other) + => (this - other).LengthSquared; + + public double Distance(IntVector2 other) + => Math.Sqrt(DistanceSquared(other)); + + public int ManhattanDistance(IntVector2 other) + => Math.Abs(X - other.X) + Math.Abs(Y - other.Y); public static IntVector2 Random(Random r, int maxX, int maxY) { @@ -66,17 +140,36 @@ public override int GetHashCode() { return HashCode.Combine(X, Y); } + + public override string ToString() + { + return $"{X},{Y}"; + } } public static class IntVector2Ext { public static IntVector2 Perpendicular(this IntVector2 v) { - return new(v.Y, -v.X); + return new(-v.Y, v.X); } public static IntVector2 PerpendicularCounterClockwise(this IntVector2 v) { - return new(-v.Y, v.X); + return new(v.Y, -v.X); + } + + /// + /// Determines whether lies behind + /// along the specified direction. + /// + /// The point being tested. + /// The reference point to compare against. + /// The forward direction from the reference point. + /// true if is behind ; otherwise, false. + public static bool IsBehind(this IntVector2 self, IntVector2 other, IntVector2 direction) + { + // A negative dot product means the direction from 'other' to 'self' faces away from the 'direction' vector. + return IntVector2.Dot(self - other, direction) < 0; } } diff --git a/Tests/IntVector2Tests.cs b/Tests/IntVector2Tests.cs new file mode 100644 index 000000000..084383f7f --- /dev/null +++ b/Tests/IntVector2Tests.cs @@ -0,0 +1,259 @@ +using FluentAssertions; +using Z2Randomizer.RandomizerCore; +using Random = Z2Randomizer.RandomizerCore.Random; + +namespace Tests; + +[TestClass] +public class IntVector2Tests +{ + [TestMethod] + public void Constructor_SetsCoordinates() + { + var v = new IntVector2(3, -4); + v.X.Should().Be(3); + v.Y.Should().Be(-4); + } + + [TestMethod] + public void EqualityOperator_True_WhenSame() + { + (new IntVector2(1, 2) == new IntVector2(1, 2)).Should().BeTrue(); + (new IntVector2(1, 2) == new IntVector2(3, 4)).Should().BeFalse(); + } + + [TestMethod] + public void InequalityOperator_True_WhenDifferent() + { + (new IntVector2(1, 2) != new IntVector2(3, 4)).Should().BeTrue(); + (new IntVector2(1, 2) != new IntVector2(1, 2)).Should().BeFalse(); + } + + [TestMethod] + public void AddOperator_SumsComponents() + { + var a = new IntVector2(1, 2); + var b = new IntVector2(3, 4); + (a + b).Should().Be(new IntVector2(4, 6)); + } + + [TestMethod] + public void SubtractOperator_DiffsComponents() + { + var a = new IntVector2(5, 7); + var b = new IntVector2(2, 3); + (a - b).Should().Be(new IntVector2(3, 4)); + } + + [TestMethod] + public void NegationOperator_NegatesComponents() + { + var v = new IntVector2(3, -4); + (-v).Should().Be(new IntVector2(-3, 4)); + } + + [TestMethod] + public void MultiplyByScalar_ScalesComponents() + { + var v = new IntVector2(2, -3); + (3 * v).Should().Be(new IntVector2(6, -9)); + } + + [TestMethod] + public void DivideByScalar_DividesComponents() + { + var v = new IntVector2(6, -9); + (v / 3).Should().Be(new IntVector2(2, -3)); + } + + [TestMethod] + public void Dot_Product() + { + IntVector2.Dot(new IntVector2(1, 2), new IntVector2(3, 4)).Should().Be(11); + } + + [TestMethod] + public void Cross_Product() + { + IntVector2.Cross(new IntVector2(1, 0), new IntVector2(0, 1)).Should().Be(1); + IntVector2.Cross(new IntVector2(0, 1), new IntVector2(1, 0)).Should().Be(-1); + IntVector2.Cross(new IntVector2(1, 2), new IntVector2(3, 4)).Should().Be(-2); + } + + [TestMethod] + public void ComponentMultiply_MultipliesElementWise() + { + new IntVector2(2, 3).ComponentMultiply(new IntVector2(4, 5)).Should().Be(new IntVector2(8, 15)); + } + + [TestMethod] + public void IsZero_ReturnsCorrectValue() + { + IntVector2.ZERO.IsZero.Should().BeTrue(); + new IntVector2(1, 0).IsZero.Should().BeFalse(); + new IntVector2(0, 1).IsZero.Should().BeFalse(); + new IntVector2(1, 2).IsZero.Should().BeFalse(); + } + + [TestMethod] + public void Abs_ReturnsAbsoluteComponents() + { + new IntVector2(-3, 4).Abs().Should().Be(new IntVector2(3, 4)); + new IntVector2(3, -4).Abs().Should().Be(new IntVector2(3, 4)); + new IntVector2(-3, -4).Abs().Should().Be(new IntVector2(3, 4)); + new IntVector2(3, 4).Abs().Should().Be(new IntVector2(3, 4)); + } + + [TestMethod] + public void Min_TakesComponentWiseMinimum() + { + IntVector2.Min(new IntVector2(1, 5), new IntVector2(3, 2)).Should().Be(new IntVector2(1, 2)); + } + + [TestMethod] + public void Max_TakesComponentWiseMaximum() + { + IntVector2.Max(new IntVector2(1, 5), new IntVector2(3, 2)).Should().Be(new IntVector2(3, 5)); + } + + [TestMethod] + public void LengthSquared_Correct() + { + new IntVector2(3, 4).LengthSquared.Should().Be(25); + IntVector2.ZERO.LengthSquared.Should().Be(0); + new IntVector2(-3, 4).LengthSquared.Should().Be(25); + } + + [TestMethod] + public void Length_Correct() + { + new IntVector2(3, 4).Length.Should().BeApproximately(5.0, 1e-10); + IntVector2.ZERO.Length.Should().Be(0.0); + } + + [TestMethod] + public void ManhattanLength_Correct() + { + new IntVector2(3, 4).ManhattanLength.Should().Be(7); + new IntVector2(-3, 4).ManhattanLength.Should().Be(7); + IntVector2.ZERO.ManhattanLength.Should().Be(0); + } + + [TestMethod] + public void DistanceSquared_Correct() + { + new IntVector2(0, 0).DistanceSquared(new IntVector2(3, 4)).Should().Be(25); + new IntVector2(1, 1).DistanceSquared(new IntVector2(4, 5)).Should().Be(25); + } + + [TestMethod] + public void Distance_Correct() + { + new IntVector2(0, 0).Distance(new IntVector2(3, 4)).Should().BeApproximately(5.0, 1e-10); + new IntVector2(0, 0).Distance(new IntVector2(0, 0)).Should().BeApproximately(0.0, 1e-10); + } + + [TestMethod] + public void ManhattanDistance_Correct() + { + new IntVector2(0, 0).ManhattanDistance(new IntVector2(3, 4)).Should().Be(7); + new IntVector2(1, 1).ManhattanDistance(new IntVector2(4, 5)).Should().Be(7); + new IntVector2(0, 0).ManhattanDistance(new IntVector2(-3, 4)).Should().Be(7); + } + + [TestMethod] + public void Random_MaxOnly_InRange() + { + var r = new Random(42L); + for (int i = 0; i < 100; i++) + { + var v = IntVector2.Random(r, 10, 20); + v.X.Should().BeInRange(0, 9); + v.Y.Should().BeInRange(0, 19); + } + } + + [TestMethod] + public void Random_Range_InRange() + { + var r = new Random(42L); + for (int i = 0; i < 100; i++) + { + var v = IntVector2.Random(r, 5, 10, -3, 7); + v.X.Should().BeInRange(5, 9); + v.Y.Should().BeInRange(-3, 6); + } + } + + [TestMethod] + public void Random_IsDeterministic() + { + var r1 = new Random(999L); + var r2 = new Random(999L); + for (int i = 0; i < 10; i++) + { + IntVector2.Random(r1, 100, 100).Should().Be(IntVector2.Random(r2, 100, 100)); + } + } + + [TestMethod] + public void Equals_Struct_True_WhenSame() + { + new IntVector2(1, 2).Equals(new IntVector2(1, 2)).Should().BeTrue(); + new IntVector2(1, 2).Equals(new IntVector2(3, 4)).Should().BeFalse(); + } + + [TestMethod] + public void Equals_Object_True_WhenSame() + { + ((object)new IntVector2(1, 2)).Equals((object)new IntVector2(1, 2)).Should().BeTrue(); + ((object)new IntVector2(1, 2)).Equals("other").Should().BeFalse(); + ((object)new IntVector2(1, 2)).Equals(null).Should().BeFalse(); + } + + [TestMethod] + public void GetHashCode_Consistent() + { + var a = new IntVector2(3, 4); + var b = new IntVector2(3, 4); + a.GetHashCode().Should().Be(b.GetHashCode()); + } + + [TestMethod] + public void ToString_Formatted() + { + new IntVector2(3, 4).ToString().Should().Be("3,4"); + new IntVector2(-1, 0).ToString().Should().Be("-1,0"); + } + + // Extension methods + [TestMethod] + public void Perpendicular_Clockwise() + { + IntVector2.NORTH.Perpendicular().Should().Be(IntVector2.EAST); + IntVector2.EAST.Perpendicular().Should().Be(IntVector2.SOUTH); + IntVector2.SOUTH.Perpendicular().Should().Be(IntVector2.WEST); + IntVector2.WEST.Perpendicular().Should().Be(IntVector2.NORTH); + } + + [TestMethod] + public void PerpendicularCounterClockwise_Correct() + { + IntVector2.NORTH.PerpendicularCounterClockwise().Should().Be(IntVector2.WEST); + IntVector2.WEST.PerpendicularCounterClockwise().Should().Be(IntVector2.SOUTH); + IntVector2.SOUTH.PerpendicularCounterClockwise().Should().Be(IntVector2.EAST); + IntVector2.EAST.PerpendicularCounterClockwise().Should().Be(IntVector2.NORTH); + } + + [TestMethod] + public void IsBehind_East_SelfBehind() + { + new IntVector2(3, 2).IsBehind(new IntVector2(5, 2), IntVector2.EAST).Should().BeTrue(); + } + + [TestMethod] + public void IsBehind_South_SelfBehind() + { + new IntVector2(2, 3).IsBehind(new IntVector2(2, 7), IntVector2.SOUTH).Should().BeTrue(); + } +} From 42a9c10d981cc9a67d6edf76f5f28f5dc128e173 Mon Sep 17 00:00:00 2001 From: initsu Date: Sat, 13 Jun 2026 22:29:01 +0200 Subject: [PATCH 12/56] Add tests for our Random implementation --- Tests/RandomTests.cs | 540 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 540 insertions(+) create mode 100644 Tests/RandomTests.cs diff --git a/Tests/RandomTests.cs b/Tests/RandomTests.cs new file mode 100644 index 000000000..a86c903ab --- /dev/null +++ b/Tests/RandomTests.cs @@ -0,0 +1,540 @@ +using FluentAssertions; +using Random = Z2Randomizer.RandomizerCore.Random; + +namespace Tests; + +[TestClass] +public class RandomTests +{ + #region Constructor + + [TestMethod] + public void Constructor_Deterministic_ForSameSeed() + { + var r1 = new Random(42L); + var r2 = new Random(42L); + + for (int i = 0; i < 100; i++) + { + r1.Next().Should().Be(r2.Next()); + } + } + + [TestMethod] + public void Constructor_DifferentSequences_ForDifferentSeeds() + { + var r1 = new Random(42L); + var r2 = new Random(43L); + + bool differs = false; + for (int i = 0; i < 100; i++) + { + if (r1.Next() != r2.Next()) + { + differs = true; + break; + } + } + differs.Should().BeTrue(); + } + + #endregion + + #region GetState / SetState + + [TestMethod] + public void GetState_ReturnsBase64String() + { + var r = new Random(42L); + string state = r.GetState(); + + state.Should().NotBeNullOrEmpty(); + state.Length.Should().Be(44); // Base64 of 32 bytes = 44 chars (with padding) + + byte[] decoded = Convert.FromBase64String(state); + decoded.Length.Should().Be(32); + } + + [TestMethod] + public void SetState_RestoresExactSequence() + { + var r1 = new Random(42L); + int[] before = Enumerable.Range(0, 10).Select(_ => r1.Next()).ToArray(); + + string state = r1.GetState(); + int[] afterState = Enumerable.Range(0, 10).Select(_ => r1.Next()).ToArray(); + + var r2 = new Random(999L); + r2.SetState(state); + int[] restored = Enumerable.Range(0, 10).Select(_ => r2.Next()).ToArray(); + + restored.Should().Equal(afterState); + restored.Should().NotEqual(before); + } + + [TestMethod] + public void SetState_InvalidLength_Throws() + { + var r = new Random(42L); + string shortState = Convert.ToBase64String(new byte[16]); + + Action act = () => r.SetState(shortState); + act.Should().Throw(); + } + + [TestMethod] + public void SetState_AllZero_Throws() + { + var r = new Random(42L); + string zeroState = Convert.ToBase64String(new byte[32]); + + Action act = () => r.SetState(zeroState); + act.Should().Throw(); + } + + [TestMethod] + public void GetState_SetState_PreservesAllMethods() + { + var r1 = new Random(123L); + _ = r1.Next(); + _ = r1.NextInt64(); + _ = r1.NextDouble(); + + string state = r1.GetState(); + + var r2 = new Random(999L); + r2.SetState(state); + + for (int i = 0; i < 50; i++) + { + r1.Next().Should().Be(r2.Next()); + r1.NextInt64().Should().Be(r2.NextInt64()); + r1.NextDouble().Should().Be(r2.NextDouble()); + } + } + + #endregion + + #region Next() + + [TestMethod] + public void Next_ReturnsNonNegative() + { + var r = new Random(42L); + for (int i = 0; i < 1000; i++) + { + int val = r.Next(); + val.Should().BeGreaterThanOrEqualTo(0); + val.Should().BeLessThan(int.MaxValue); + } + } + + [TestMethod] + public void Next_Distribution_IsNotConstant() + { + var r = new Random(42L); + var values = Enumerable.Range(0, 10000).Select(_ => r.Next()).ToList(); + + values.Distinct().Count().Should().BeGreaterThan(100); + } + + #endregion + + #region Next(maxValue) + + [TestMethod] + public void NextMaxValue_One_ReturnsZero() + { + var r = new Random(42L); + for (int i = 0; i < 100; i++) + { + r.Next(1).Should().Be(0); + } + } + + [TestMethod] + public void NextMaxValue_Two_ReturnsZeroOrOne() + { + var r = new Random(42L); + for (int i = 0; i < 1000; i++) + { + int val = r.Next(2); + val.Should().BeInRange(0, 1); + } + } + + [TestMethod] + public void NextMaxValue_InRange() + { + var r = new Random(42L); + int max = 100; + for (int i = 0; i < 10000; i++) + { + int val = r.Next(max); + val.Should().BeGreaterThanOrEqualTo(0); + val.Should().BeLessThan(max); + } + } + + [TestMethod] + public void NextMaxValue_Distribution_ContainsAllValues() + { + var r = new Random(42L); + int max = 10; + var values = Enumerable.Range(0, 10000).Select(_ => r.Next(max)).ToList(); + + for (int i = 0; i < max; i++) + { + values.Should().Contain(i); + } + } + + #endregion + + #region Next(minValue, maxValue) + + [TestMethod] + public void NextRange_InRange() + { + var r = new Random(42L); + int min = -50, max = 50; + for (int i = 0; i < 10000; i++) + { + int val = r.Next(min, max); + val.Should().BeGreaterThanOrEqualTo(min); + val.Should().BeLessThan(max); + } + } + + [TestMethod] + public void NextRange_NegativeRange() + { + var r = new Random(42L); + for (int i = 0; i < 1000; i++) + { + int val = r.Next(-100, -10); + val.Should().BeGreaterThanOrEqualTo(-100); + val.Should().BeLessThan(-10); + } + } + + [TestMethod] + public void NextRange_SameMinMax_ReturnsMin() + { + var r = new Random(42L); + for (int i = 0; i < 100; i++) + { + r.Next(5, 6).Should().Be(5); + } + } + + [TestMethod] + public void NextRange_Distribution_ContainsAllValues() + { + var r = new Random(42L); + var values = Enumerable.Range(0, 10000).Select(_ => r.Next(0, 10)).ToList(); + + for (int i = 0; i < 10; i++) + { + values.Should().Contain(i); + } + } + + #endregion + + #region NextInt64() + + [TestMethod] + public void NextInt64_ReturnsNonNegative() + { + var r = new Random(42L); + for (int i = 0; i < 1000; i++) + { + long val = r.NextInt64(); + val.Should().BeGreaterThanOrEqualTo(0); + val.Should().BeLessThan(long.MaxValue); + } + } + + [TestMethod] + public void NextInt64_Distribution_IsNotConstant() + { + var r = new Random(42L); + var values = Enumerable.Range(0, 10000).Select(_ => r.NextInt64()).ToList(); + + values.Distinct().Count().Should().BeGreaterThan(100); + } + + #endregion + + #region NextInt64(maxValue) + + [TestMethod] + public void NextInt64MaxValue_One_ReturnsZero() + { + var r = new Random(42L); + for (int i = 0; i < 100; i++) + { + r.NextInt64(1L).Should().Be(0L); + } + } + + [TestMethod] + public void NextInt64MaxValue_InRange() + { + var r = new Random(42L); + long max = 1000; + for (int i = 0; i < 10000; i++) + { + long val = r.NextInt64(max); + val.Should().BeGreaterThanOrEqualTo(0); + val.Should().BeLessThan(max); + } + } + + #endregion + + #region NextInt64(minValue, maxValue) + + [TestMethod] + public void NextInt64Range_InRange() + { + var r = new Random(42L); + long min = -500, max = 500; + for (int i = 0; i < 10000; i++) + { + long val = r.NextInt64(min, max); + val.Should().BeGreaterThanOrEqualTo(min); + val.Should().BeLessThan(max); + } + } + + [TestMethod] + public void NextInt64Range_Distribution_ContainsAllValues() + { + var r = new Random(42L); + var values = Enumerable.Range(0, 10000).Select(_ => r.NextInt64(0L, 10L)).ToList(); + + for (long i = 0; i < 10; i++) + { + values.Should().Contain(i); + } + } + + #endregion + + #region NextBytes + + [TestMethod] + public void NextBytes_FillsBuffer() + { + var r = new Random(42L); + byte[] buffer = new byte[64]; + r.NextBytes(buffer); + + buffer.Any(b => b != 0).Should().BeTrue(); + } + + [TestMethod] + public void NextBytes_CorrectLength() + { + var r = new Random(42L); + byte[] buffer = new byte[37]; + r.NextBytes(buffer); + + buffer.Length.Should().Be(37); + } + + [TestMethod] + public void NextBytes_EmptyBuffer_DoesNotThrow() + { + var r = new Random(42L); + byte[] buffer = Array.Empty(); + Action act = () => r.NextBytes(buffer); + act.Should().NotThrow(); + } + + [TestMethod] + public void NextBytes_Deterministic() + { + var r1 = new Random(42L); + var r2 = new Random(42L); + + byte[] b1 = new byte[64]; + byte[] b2 = new byte[64]; + r1.NextBytes(b1); + r2.NextBytes(b2); + + b1.Should().Equal(b2); + } + + [TestMethod] + public void NextBytes_SingleByte() + { + var r = new Random(42L); + byte[] buffer = new byte[1]; + r.NextBytes(buffer); + buffer.Length.Should().Be(1); + } + + #endregion + + #region NextDouble + + [TestMethod] + public void NextDouble_InRange() + { + var r = new Random(42L); + for (int i = 0; i < 10000; i++) + { + double val = r.NextDouble(); + val.Should().BeGreaterThanOrEqualTo(0.0); + val.Should().BeLessThan(1.0); + } + } + + [TestMethod] + public void NextDouble_Distribution_IsNotConstant() + { + var r = new Random(42L); + var values = Enumerable.Range(0, 10000).Select(_ => r.NextDouble()).ToList(); + + values.Distinct().Count().Should().BeGreaterThan(100); + } + + [TestMethod] + public void NextDouble_Deterministic() + { + var r1 = new Random(42L); + var r2 = new Random(42L); + + for (int i = 0; i < 100; i++) + { + r1.NextDouble().Should().Be(r2.NextDouble()); + } + } + + #endregion + + #region GetItems + + [TestMethod] + public void GetItemsArray_ChoicesFromSource() + { + var r = new Random(42L); + int[] choices = { 10, 20, 30 }; + + int[] result = r.GetItems(choices, 100); + result.Length.Should().Be(100); + foreach (int val in result) + { + val.Should().BeOneOf(10, 20, 30); + } + } + + [TestMethod] + public void GetItemsArray_LengthZero_ReturnsEmpty() + { + var r = new Random(42L); + int[] choices = { 1, 2, 3 }; + + int[] result = r.GetItems(choices, 0); + result.Should().BeEmpty(); + } + + [TestMethod] + public void GetItemsSpan_FillsDestination() + { + var r = new Random(42L); + string[] choices = { "a", "b", "c" }; + string[] dest = new string[5]; + + r.GetItems(choices, dest); + dest.Length.Should().Be(5); + foreach (string val in dest) + { + val.Should().BeOneOf("a", "b", "c"); + } + } + + [TestMethod] + public void GetItems_Distribution_ContainsAllChoices() + { + var r = new Random(42L); + int[] choices = { 1, 2, 3, 4, 5 }; + + int[] result = r.GetItems(choices, 1000); + foreach (int choice in choices) + { + result.Should().Contain(choice); + } + } + + #endregion + + #region Shuffle + + [TestMethod] + public void Shuffle_PreservesElements() + { + var r = new Random(42L); + int[] original = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + int[] shuffled = (int[])original.Clone(); + r.Shuffle(shuffled); + + shuffled.Should().BeEquivalentTo(original); // ignores order + } + + [TestMethod] + public void Shuffle_Deterministic() + { + var r1 = new Random(42L); + var r2 = new Random(42L); + + int[] a = { 1, 2, 3, 4, 5 }; + int[] b = (int[])a.Clone(); + r1.Shuffle(a); + r2.Shuffle(b); + + a.Should().Equal(b); + } + + [TestMethod] + public void Shuffle_Empty() + { + var r = new Random(42L); + int[] arr = Array.Empty(); + Action act = () => r.Shuffle(arr); + act.Should().NotThrow(); + } + + [TestMethod] + public void Shuffle_SingleElement() + { + var r = new Random(42L); + int[] arr = { 42 }; + r.Shuffle(arr); + arr.Should().Equal([42]); + } + + [TestMethod] + public void Shuffle_ChangesOrder() + { + var r = new Random(42L); + int[] arr = Enumerable.Range(0, 100).ToArray(); + r.Shuffle(arr); + + bool changed = false; + for (int i = 0; i < arr.Length; i++) + { + if (arr[i] != i) + { + changed = true; + break; + } + } + changed.Should().BeTrue(); + } + + #endregion +} From d0fa8aa15e23519902b0bd90f7bfd732280cb4f2 Mon Sep 17 00:00:00 2001 From: initsu Date: Thu, 28 May 2026 15:34:13 +0200 Subject: [PATCH 13/56] Wrap World.map in a new class OverworldMap --- RandomizerCore/Overworld/DeathMountain.cs | 4 +-- RandomizerCore/Overworld/EastHyrule.cs | 4 +-- RandomizerCore/Overworld/MazeIsland.cs | 4 +-- RandomizerCore/Overworld/OverworldMap.cs | 42 +++++++++++++++++++++++ RandomizerCore/Overworld/WestHyrule.cs | 4 +-- RandomizerCore/Overworld/World.cs | 18 +++++----- 6 files changed, 59 insertions(+), 17 deletions(-) create mode 100644 RandomizerCore/Overworld/OverworldMap.cs diff --git a/RandomizerCore/Overworld/DeathMountain.cs b/RandomizerCore/Overworld/DeathMountain.cs index ce04e8505..858704fd9 100644 --- a/RandomizerCore/Overworld/DeathMountain.cs +++ b/RandomizerCore/Overworld/DeathMountain.cs @@ -226,7 +226,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) { Debug.Assert(MapRows == 75); Debug.Assert(MapColumns == 64); - map = rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns); + map = new OverworldMap(rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns)); if (biome == Biome.VANILLA_SHUFFLE) { ShuffleLocations(AllLocations); @@ -245,7 +245,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) int bytesWritten = 2000; while (bytesWritten > MAP_SIZE_BYTES) { - map = new Terrain[MapRows, MapColumns]; + map = new OverworldMap(MapRows, MapColumns); Terrain riverT = Terrain.MOUNTAIN; if (biome != Biome.CANYON && biome != Biome.DRY_CANYON && biome != Biome.CALDERA && biome != Biome.ISLANDS) { diff --git a/RandomizerCore/Overworld/EastHyrule.cs b/RandomizerCore/Overworld/EastHyrule.cs index 0db2ddc7c..1fadbdd5b 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -339,7 +339,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) { Debug.Assert(MapRows == 75); Debug.Assert(MapColumns == 64); - map = rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns); + map = new OverworldMap(rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns)); if (biome == Biome.VANILLA_SHUFFLE) { @@ -444,7 +444,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) }; } - map = new Terrain[MapRows, MapColumns]; + map = new OverworldMap(MapRows, MapColumns); for (int i = 0; i < MapRows; i++) { diff --git a/RandomizerCore/Overworld/MazeIsland.cs b/RandomizerCore/Overworld/MazeIsland.cs index 8533f7c00..b849f9ad8 100644 --- a/RandomizerCore/Overworld/MazeIsland.cs +++ b/RandomizerCore/Overworld/MazeIsland.cs @@ -118,7 +118,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) { Debug.Assert(MapRows == 75); Debug.Assert(MapColumns == 64); - map = rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns); + map = new OverworldMap(rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns)); if (biome == Biome.VANILLA_SHUFFLE) { ShuffleLocations(AllLocations); @@ -144,7 +144,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) while (bytesWritten > MAP_SIZE_BYTES) { - map = new Terrain[MapRows, 64]; + map = new OverworldMap(MapRows, 64); bool[,] visited = new bool[MapRows, MapColumns]; for (int i = 0; i < MapColumns; i++) diff --git a/RandomizerCore/Overworld/OverworldMap.cs b/RandomizerCore/Overworld/OverworldMap.cs new file mode 100644 index 000000000..8760e4b91 --- /dev/null +++ b/RandomizerCore/Overworld/OverworldMap.cs @@ -0,0 +1,42 @@ +namespace Z2Randomizer.RandomizerCore.Overworld; + +public class OverworldMap +{ + private Terrain[,] inner; + + public OverworldMap(int mapRows, int mapColumns) + { + inner = new Terrain[mapRows, mapColumns]; + } + + public OverworldMap(Terrain[,] map) + { + inner = map; + } + + public Terrain this[int y, int x] + { + get => inner[y, x]; + + set + { + // if (y == 27 && x == 20) Debugger.Break(); + inner[y, x] = value; + } + } + + public Terrain this[IntVector2 pos] + { + get => inner[pos.Y, pos.X]; + + set + { + inner[pos.Y, pos.X] = value; + } + } + + public int GetLength(int dimension) + { + return inner.GetLength(dimension); + } +} diff --git a/RandomizerCore/Overworld/WestHyrule.cs b/RandomizerCore/Overworld/WestHyrule.cs index 2aa4ec81f..12dc2ff58 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -340,7 +340,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) { Debug.Assert(MapRows == 75); Debug.Assert(MapColumns == 64); - map = rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns); + map = new OverworldMap(rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns)); if (biome == Biome.VANILLA_SHUFFLE) { areasByLocation = new SortedDictionary> @@ -398,7 +398,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) sariaSouth.CanShuffle = false; sariaNorth.CanShuffle = false; - map = new Terrain[MapRows, MapColumns]; + map = new OverworldMap(MapRows, MapColumns); //blank the whole map to start for (int i = 0; i < MapRows; i++) diff --git a/RandomizerCore/Overworld/World.cs b/RandomizerCore/Overworld/World.cs index 7ed719566..f2c646515 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -27,7 +27,7 @@ public abstract class World public IReadOnlyList overworldEncounterMapDuplicate { get; protected set; } = []; public IReadOnlyList nonEncounterMaps { get; protected set; } protected SortedDictionary<(int, int), Location> locsByCoords; - public Terrain[,] map; + public OverworldMap map; public const int MAP_ROWS_FULL = 75; public const int MAP_COLS_FULL = 64; private const int MAX_LOCATION_PLACEMENT_ATTEMPTS = 5000; @@ -1507,7 +1507,7 @@ private PlacedTerrainPreCalc[] GrowTerrainGetPlacedTerrains(Climate climate) protected bool GrowTerrain(Climate climate) { TerrainGrowthAttempts++; - Terrain[,] mapCopy = new Terrain[MapRows, MapColumns]; + var mapCopy = new OverworldMap(MapRows, MapColumns); PlacedTerrainPreCalc[] placedTerrains = GrowTerrainGetPlacedTerrains(climate); const double EPSILON = 1e-9; double distance, minDistance; @@ -1940,7 +1940,7 @@ protected bool DrawBridge(Direction direction) return DrawBridge(RNG, map, bridge, walkableTerrains, direction); } - public static bool DrawBridge(Random r, Terrain[,] map, Location? bridge, List walkableTerrains, Direction direction) + public static bool DrawBridge(Random r, OverworldMap map, Location? bridge, List walkableTerrains, Direction direction) { if (bridge == null) { throw new Exception("Unable to draw unloaded bridge"); } int x = 0; @@ -1981,7 +1981,7 @@ protected bool DrawRaft(Direction direction) return DrawRaft(RNG, map, raft, walkableTerrains, direction); } - public static bool DrawRaft(Random r, Terrain[,] map, Location? raft, List walkableTerrains, Direction direction) + public static bool DrawRaft(Random r, OverworldMap map, Location? raft, List walkableTerrains, Direction direction) { if (raft == null) { throw new Exception("Unable to draw unloaded raft"); } int x = 0; @@ -2017,7 +2017,7 @@ public static bool DrawRaft(Random r, Terrain[,] map, Location? raft, List walkableTerrains, int x, int y) + protected static bool IsValidEndTile(OverworldMap map, List walkableTerrains, int x, int y) { int mapRows = map.GetLength(0); int mapCols = map.GetLength(1); @@ -2097,7 +2097,7 @@ protected static bool IsValidEndTile(Terrain[,] map, List walkableTerra return walkableTerrains.Contains(map[y, x]); } - public static void PlaceBridge(Terrain[,] map, Location bridge, int x, int y, Direction direction) + public static void PlaceBridge(OverworldMap map, Location bridge, int x, int y, Direction direction) { int mapRows = map.GetLength(0); int mapCols = map.GetLength(1); @@ -2138,7 +2138,7 @@ public static void PlaceBridge(Terrain[,] map, Location bridge, int x, int y, Di } } - public static void PlaceRaft(Terrain[,] map, Location raft, int x, int y) + public static void PlaceRaft(OverworldMap map, Location raft, int x, int y) { map[y, x] = Terrain.BRIDGE; raft.Xpos = x; From 798d6e6af836d5d40d932dadecf3dd9d2db1682f Mon Sep 17 00:00:00 2001 From: initsu Date: Sat, 23 May 2026 11:48:05 +0200 Subject: [PATCH 14/56] Determine linked fire spell when RandomizerProperties is created --- RandomizerCore/CustomTexts.cs | 2 +- RandomizerCore/Hyrule.cs | 8 +++--- RandomizerCore/ROM.cs | 5 ++-- RandomizerCore/RandomizerConfiguration.cs | 31 +++++++++++++++++------ RandomizerCore/RandomizerProperties.cs | 2 +- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index 28a4a4a23..8cc4617d7 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -622,7 +622,7 @@ public static List GenerateTexts( if (props.TownNameHints) { - GenerateTownNameHints(texts, locations, props.CombineFire); + GenerateTownNameHints(texts, locations, props.LinkedFireSpell != null); } } while (TextLength(texts) > MAX_TEXT_LENGTH); diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 51787cd08..ea03cf602 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -335,9 +335,9 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC OverworldEnemyShuffler.Shuffle(worlds, assembler, ROMData, props.MixLargeAndSmallEnemies, props.GeneratorsAlwaysMatch, r); } - if (props.CombineFire) + if (props.LinkedFireSpell != null) { - ROMData.CombineFireSpell(assembler, spellListOrder, r); + ROMData.CombineFireSpell(assembler, props.LinkedFireSpell.Value, spellListOrder); } if (props.StartsWithCollectable(westHyrule.ruto.Town?.GetWizard()?.Collectable)) @@ -2302,7 +2302,7 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra 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. - if(props.CombineFire) + if(props.LinkedFireSpell != null) { rom.Put(ROM.ChrRomOffset + 0x1a0E0, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.linkedFire.chr")); } @@ -2661,7 +2661,7 @@ private void UpdateRom() //ROMData.UpdateWizardText(WizardCollectables); // Add marker for linked fire. Do this before old spell name shuffle so this gets shuffled in there too - if (props.CombineFire) + if (props.LinkedFireSpell != null) { ROMData.Put(0x1c72, [..ROM.StringToZ2Bytes("FIRE"), 0xFC]); } diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 0dda71918..26b77a754 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -1997,7 +1997,7 @@ public void DashSpell(Assembler asm) a.Byt(dash); } - public void CombineFireSpell(Assembler asm, List? customSpellOrder, Random RNG) + public void CombineFireSpell(Assembler asm, Collectable linkedSpell, List? customSpellOrder) { // These are the bit flags for each spell. The old implementation // changed this table directly. However as this table is also used in @@ -2007,8 +2007,7 @@ public void CombineFireSpell(Assembler asm, List? customSpellOrder, byte[] spellBytes = GetBytes(0xdcb, 8); int fireSpellIndex = 4; - int r = RNG.Next(7); - int linkedSpellIndex = r > 3 ? r + 1 : r; + int linkedSpellIndex = linkedSpell.VanillaSpellOrder(); byte combinedSpellBits = (byte)(spellBytes[linkedSpellIndex] | spellBytes[fireSpellIndex]); spellBytes[fireSpellIndex] = combinedSpellBits; diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index 325a35485..d3816bf8a 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -75,6 +75,16 @@ public sealed partial class RandomizerConfiguration() : INotifyPropertyChanged Collectable.THUNDER_SPELL ]; + [IgnoreInFlags] + private readonly static Collectable[] POSSIBLE_LINKED_FIRE_SPELLS = [ + Collectable.SHIELD_SPELL, + Collectable.JUMP_SPELL, + Collectable.LIFE_SPELL, + Collectable.FAIRY_SPELL, + Collectable.REFLECT_SPELL, + Collectable.SPELL_SPELL, + Collectable.THUNDER_SPELL + ]; //Start Configuration [Reactive] @@ -988,38 +998,43 @@ public RandomizerProperties Export(Random r, bool includeDifficulty = true) } while (!properties.HasEnoughSpaceToAllocateItems()); //Handle Fire + Collectable RollLinkedFireSpell() + { + return POSSIBLE_LINKED_FIRE_SPELLS[r.Next(POSSIBLE_LINKED_FIRE_SPELLS.Length)]; + } switch (fireOption) { case FireOption.NORMAL: - properties.CombineFire = false; + properties.LinkedFireSpell = null; properties.ReplaceFireWithDash = false; break; case FireOption.PAIR_WITH_RANDOM: - properties.CombineFire = true; + properties.LinkedFireSpell = RollLinkedFireSpell(); properties.ReplaceFireWithDash = false; break; case FireOption.REPLACE_WITH_DASH: - properties.CombineFire = false; + properties.LinkedFireSpell = null; properties.ReplaceFireWithDash = true; break; case FireOption.RANDOM: switch (r.Next(3)) { case 0: - properties.CombineFire = false; + properties.LinkedFireSpell = null; properties.ReplaceFireWithDash = false; break; case 1: - properties.CombineFire = true; + properties.LinkedFireSpell = RollLinkedFireSpell(); properties.ReplaceFireWithDash = false; break; case 2: - properties.CombineFire = false; + properties.LinkedFireSpell = null; properties.ReplaceFireWithDash = true; break; - } break; + default: + throw new Exception("Illegal Fire option"); } ResolveStartingTechniques(properties, r, includeDifficulty); @@ -1504,7 +1519,7 @@ public RandomizerProperties Export(Random r, bool includeDifficulty = true) if (properties.ReplaceFireWithDash) { - properties.CombineFire = false; + Debug.Assert(properties.LinkedFireSpell == null); } //If spells are in the shuffle pool, shuffle spells means nothing, so diable it diff --git a/RandomizerCore/RandomizerProperties.cs b/RandomizerCore/RandomizerProperties.cs index f17abd98b..dbd93bdf8 100644 --- a/RandomizerCore/RandomizerProperties.cs +++ b/RandomizerCore/RandomizerProperties.cs @@ -48,7 +48,7 @@ public class RandomizerProperties public bool StartReflect { get; set; } public bool StartSpell { get; set; } public bool StartThunder { get; set; } - public bool CombineFire { get; set; } + public Collectable? LinkedFireSpell { get; set; } public bool ReplaceFireWithDash { get; set; } public StartingResourceLimit StartSpellsLimit { get; set; } From a15ea680769165a9df040e84118af95ba89858e1 Mon Sep 17 00:00:00 2001 From: initsu Date: Thu, 28 May 2026 11:26:47 +0200 Subject: [PATCH 15/56] Add some more free ROM memory areas --- RandomizerCore/Asm/Init.s | 8 +++++--- RandomizerCore/ROM.cs | 9 +++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/RandomizerCore/Asm/Init.s b/RandomizerCore/Asm/Init.s index 88747cacb..3484426c1 100644 --- a/RandomizerCore/Asm/Init.s +++ b/RandomizerCore/Asm/Init.s @@ -31,6 +31,7 @@ ; Mark unused areas in the ROM so the linker can place stuff here +FREE "PRG0" [$a89e, $a980) ; FREE "PRG0" [$AA40, $c000) FREE "PRG0" [$AB00, $c000) ; give room for z2edit to patch $aa40 @@ -44,9 +45,9 @@ FREE "PRG2" [$93c9, $9400) FREE "PRG2" [$9f85, $a000) FREE "PRG2" [$a933, $b480) -;FREE "PRG3" [$8bb1, $90f0] -;FREE "PRG3" [$9134, $9400] -;FREE "PRG3" [$9d0a, $a000] +FREE "PRG3" [$8cc0, $90f0) +FREE "PRG3" [$9135, $9400) +FREE "PRG3" [$9d0b, $a000) FREE "PRG3" [$B803, $c000) FREE "PRG4" [$83DC, $8470) @@ -76,6 +77,7 @@ FREE "PRG5" [$bda1, $c000) ;FREE "PRG6" [$ac09, $c000) TEMP FREE "PRG6" [$ac21, $c000) +FREE "PRG7" [$d39a, $d3ca) ; DPCM data, will affect dpcm sfx but not gameplay so its fine to use this as a last ditch ; free space for patches. Keep it disabled as much as possible ; FREE "PRG7" [$f369, $fcfb) diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 26b77a754..7e3d48a24 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -873,17 +873,18 @@ jmp LoadNewLevelCap nop LoadNewLevelCapReturn: ; $9F7F -.org $A89E +.reloc LoadNewLevelCap: lda $0777,X ; the instruction we overwrote with jmp cmp LevelCaps,X jmp LoadNewLevelCapReturn """); - a.Org(0xa8a7); + a.Reloc(); a.Label("LevelCaps"); a.Byt((byte)atkMax); a.Byt((byte)magicMax); a.Byt((byte)lifeMax); + a.Export("LevelCaps"); } /// @@ -902,6 +903,8 @@ public void ChangeLevelUpCancelling(Assembler asm) a.Code(""" ; This is called when the game checks if it should skip the cancel option ; in the level up menu or not. It runs once for each stat (X). +.import LevelCaps + .segment "PRG0" .org $A0D4 jmp CheckIfStatMaxed @@ -925,8 +928,6 @@ asl a asl a jmp CheckStatNormally """); - a.Org(0xa8a7); - a.Label("LevelCaps"); } public void UseExtendedBanksForPalaceRooms(Assembler a) From a22b2c8437f77d3b1c23bf484d97fb0dcbe8c623 Mon Sep 17 00:00:00 2001 From: initsu Date: Sat, 18 Apr 2026 10:25:12 +0200 Subject: [PATCH 16/56] Update RAM map --- RandomizerCore/Asm/BuffCarock.s | 1 - RandomizerCore/Asm/ram.inc | 131 +++++++++++++++++++++++--------- 2 files changed, 94 insertions(+), 38 deletions(-) diff --git a/RandomizerCore/Asm/BuffCarock.s b/RandomizerCore/Asm/BuffCarock.s index 94b765269..be7d828f2 100644 --- a/RandomizerCore/Asm/BuffCarock.s +++ b/RandomizerCore/Asm/BuffCarock.s @@ -2,7 +2,6 @@ .segment "PRG4" -ProjectileEnemyData = $07c0 ; SinWaveVelocityIncrement = $ba1c ; Hook into carrock's update code to add the new position calculation diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index 0b0d108c0..6da45c43d 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -1,7 +1,7 @@ ; Define low zero page addresses even if they're not well-defined ; so they will have debug info precedence over other constants -zp_00 = $00 -zp_01 = $01 +zp_00 = $00 ; LSB of general-purpose pointer +zp_01 = $01 ; MSB of general-purpose pointer zp_02 = $02 zp_03 = $03 zp_04 = $04 @@ -16,13 +16,13 @@ zp_0c = $0c zp_0d = $0d zp_0e = $0e zp_0f = $0f -zp_10 = $10 -zp_11 = $11 +EnemyIndex = $10 ; 0-5=Enemy index being processed +EntityIndex = $11 ; 0=Link, 1-6 - Enemy index(-1) being processed FrameCounter = $12 FairyState = $13 ; 0 = Not in Fairy state -zp_14 = $14 -zp_15 = $15 -zp_16 = $16 +LinkXFrameDiff = $14 +LinkScreenX = $15 ; Link's X position on the screen (not in the map) +LinksShadowScreenX = $16 LinkStanding = $17 ; 0 = Link in ducked shield position, 1 = Link standing DarkLinkStanding = $18 ; 0 = Dark Link ducking, 1 = Dark Link standing LinkYPositionHi = $19 ; [SS] 0 = Invisible, 1 = Normal, 2-FF = Fall in hole - [FILE] Cursor position @@ -39,8 +39,8 @@ Enemy3Condition = $23 Enemy4Condition = $24 Enemy5Condition = $25 OverworldStepCounter = $26 -zp_27 = $27 -zp_28 = $28 +VramAddrHi = $27 +VramAddrLo = $28 LinkYPos = $29 Enemy0YPositionLo = $2a Enemy1YPositionLo = $2b @@ -56,12 +56,12 @@ Projectile4YPosition = $34 Projectile5YPosition = $35 LinkProjectile0YPosition = $36 LinkProjectile1YPosition = $37 -zp_38 = $38 -LinkProjectileYPositionRelated = $39 -zp_3a = $3a -LinkXPositionHi = $3b -Enemy0XPositionHi = $3c -Enemy1XPositionHi = $3d +EndingDisplayTileY0 = $38 +LinkSwordYBreakableBlockAdjusted = $39 ; [SS] Link's sword hitbox Y as used for breakable blocks - [Ending] Y of tile 1 in ending sequence +EndingDisplayTileY2 = $3a +LinkXPositionHi = $3b ; [SS] Which page of the room Link is in - [Ending] X of tile 0 in ending sequence +Enemy0XPositionHi = $3c ; [SS] Which page of the room Enemy0 is in - [Ending] X of tile 1 in ending sequence +Enemy1XPositionHi = $3d ; [SS] Which page of the room Enemy1 is in - [Ending] X of tile 2 in ending sequence Enemy2XPositionHi = $3e Enemy3XPositionHi = $3f Enemy4XPositionHi = $40 @@ -74,9 +74,9 @@ Projectile4XPositionHi = $46 Projectile5XPositionHi = $47 LinkProjectile0XPositionHi = $48 LinkProjectile1XPositionHi = $49 -zp_4a = $4a -LinkProjectileXPositionHiRelated = $4b -zp_4c = $4c +zp_4a = $4a ; Unused? (Likely leftover of Link having 3 projectiles) +LinkSwordBreakableBlockAdjusted = $4b +zp_4c = $4c ; Unused? LinkXPositionLo = $4d Enemy0XPositionLo = $4e Enemy1XPositionLo = $4f @@ -92,9 +92,9 @@ Projectile4XPositionLo = $58 Projectile5XPositionLo = $59 LinkProjectile0XPositionLo = $5a LinkProjectile1XPositionLo = $5b -zp_5c = $5c -LinkProjectileXPositionLoRelated = $5d -zp_5e = $5e +zp_5c = $5c ; Unused? +LinkSwordXBreakableBlockAdjusted = $5d +zp_5e = $5e ; Unused? LinkFacingDirection = $5f Enemy0XFacing = $60 Enemy1XFacing = $61 @@ -108,10 +108,10 @@ Projectile2XFacing = $68 Projectile3XFacing = $69 Projectile4XFacing = $6a Projectile5XFacing = $6b -zp_6c = $6c -zp_6d = $6d -zp_6e = $6e -zp_6f = $6f +zp_6c = $6c ; Unused? +LinkProjectile0Facing = $6d +LinkProjectile1Facing = $6e +zp_6f = $6f ; Unused? LinkXVelocity = $70 Enemy0XVelocity = $71 Enemy1XVelocity = $72 @@ -127,7 +127,7 @@ Projectile4XVelocity = $7b Projectile5XVelocity = $7c LinkProjectile0XVelocity = $7d LinkProjectile1XVelocity = $7e -zp_7f = $7f +OverworldYScroll = $7f LinkAnimationFrame = $80 Enemy0AnimationFrame = $81 OverworldEnemy0Type = $82 ; [SS] Enemy1AnimationFrame - [OW] OverworldEnemy0Type, 1 = Weak, 2 = Strong, 3 = Fairy @@ -143,7 +143,7 @@ Projectile4Type = $8b Projectile5Type = $8c LinkProjectile0State = $8d LinkProjectile1State = $8e -zp_8f = $8f +zp_8f = $8f ; Unused? SpriteShuffleOffsetLink = $90 SpriteShuffleOffsetEnemy0 = $91 SpriteShuffleOffsetEnemy1 = $92 @@ -160,7 +160,7 @@ SpriteShuffleOffsetProjectile5 = $9c SpriteShuffleOffsetLinkProjectile0 = $9d SpriteShuffleOffsetLinkProjectile1 = $9e DpadHorizontalState = $9f ; 1 = Right, 2 = Left -zp_a0 = $a0 +LinksShadowFacingDirection = $a0 ; Link's Shadow's facing direction when his shadow is displayed during a boss explosion and during the final battle Enemy0Type = $a1 ; Enemy (Elevator) slot Enemy1Type = $a2 ; Enemy (Locked door) slot Enemy2Type = $a3 @@ -174,7 +174,7 @@ Enemy2State = $aa Enemy3State = $ab Enemy4State = $ac Enemy5State = $ad -zp_ae = $ae +LinkWalkingAnimationFrame = $ae ; 0-2 cycling Enemy0State2 = $af Enemy1State2 = $b0 Enemy2State2 = $b1 @@ -201,7 +201,7 @@ Enemy3HP = $c5 Enemy4HP = $c6 Enemy5HP = $c7 SideviewExitScreen = $c8 ; 3 = Exit screen -zp_c9 = $c9 +EnemyBitFlags = $c9 zp_ca = $ca zp_cb = $cb LinkXScreenPosition = $cc @@ -226,16 +226,40 @@ zp_de = $de zp_df = $df ; Enemy type temporary storage ; $e0-$ef are music and sound variables +Z2NoiseSoundQueue = $ed +Z2Square2SoundQueue = $ee +Z2Square1SoundQueue = $ef + +NoiseSoundQueue = $f0 +Square2SoundQueue = $f1 +Square1SoundQueue = $f2 ; Potentially free zero page memory bytes at $f1-$f4 and since they are used by FDS BIOS but not the NES Controller1ButtonsPressed = $f5 Controller2ButtonsPressed = $f6 Controller1ButtonsHeld = $f7 Controller2ButtonsHeld = $f8 -; Potentially free zero page memory bytes at $f9-$fc +; Free zero page memory bytes at $f9-$fb (would have been reserved for Famicom Disk System BIOS) +OverworldXScroll = $fc ScrollPosShadow = $fd PpuCtrlShadow = $ff +; $100-$1ff stack memory + +; $200-$2ff OAM buffer +SpriteYPosition = $200 +SpriteTilenumber = $201 +SpriteAttributes = $202 +SpriteXPosition = $203 +SpriteData = SpriteYPosition + +PpuBufferLength = $0301 +PpuAddrHi = $0302 +PpuAddrLo = $0303 +PpuTextLength = $0304 +PpuTextData = $0305 +PpuBuffer2Length = $0362 + LinkXSubpixel = $03d6 Enemy0XSubpixel = $03d7 Enemy1XSubpixel = $03d8 @@ -247,15 +271,22 @@ LinkYVelocityLo = $03e6 LinkSwordAttackFrame = $0400 Enemy0HitState = $040e +BackgroundColumnUpdateArray = $0464 ; Palette & Tile index for each y position 0 to 12 LinkInAir = $0479 ; 0 = On ground, 1 = Falling from walking off platform, 2 = Jumping +LinkSwordXHitbox = $047e ; Link's sword's X position in screen space +LinksShadowSwordXHitbox = $047f +LinkSwordYHitbox = $0480 ; 0xf8 when not attacking +LinksShadowSwordYHitbox = $0481 LandingAnimationTimer = $0497 HitboxXCoord = $047e HitboxYCoord = $0480 +SpriteSlotShuffleCounter = $0485 ; 0-5 LinkIFrameTimer = $500 timer_501 = $0501 ; $501 through $50d are timers that are decreased each frame until 0 LinkEntrySlideTime = $0503 SwordAttackTimer = $050a +InjuryTimer = $050c EncounterSpawnTimer = $0516 RNG = $051b MapNumber = $0561 @@ -263,6 +294,12 @@ TownNumber = $056b PalaceRegionIndex = $056c ; Palace index in continent (0-2) LinkYVelocityHi = $057d Enemy0YVelocity = $057e +TownfolkTimer0 = $05bd +TownfolkTimer1 = $05be +TownfolkTimer2 = $05bf +TownfolkTimer3 = $05c0 +TownfolkTimer4 = $05c1 +TownfolkTimer5 = $05c2 LinkProjectile0Timer = $05ca LinkProjectile1Timer = $05cb ; Always 0 since Fire projectiles don't expire SavedDoorExit1Page = $05d1 ; accessed as $5cc,X with X = 5 @@ -272,25 +309,30 @@ SavedDoorExit2X = $05d9 SmallDropCount = $05df LargeDropCount = $05e0 +OverworldStepCounterHi = $06e0 ; [Z2R] Used for halved encounter rate to only trigger encounters every other time the steps are reached + Lives = $0700 EntryFacing = $0701 ; Sideview entry facing direction. 0=Facing right (enter from the left), 1=Facing left (enter from the right) EnterFromElevatorAbove = $0704 ; >0 Link enters the room from the top EnterCentered = $0705 ; >0 Link enters centered; like at North Palace, taking an elevator exit or entering a random encounter RegionNumber = $0706 ; 0=West, 1=Death Mountain, 2=East, 3=Maze Island WorldNumber = $0707 ; 0=Caves & enemy encounters, 1=West towns, 2=East towns, 3=Palace 1/2/5, 4=Palace 3/4/6, 5=Great Palace +SideviewObjectPage = $0717 ; 0-3 page of current sideview object being processed ScrollDirection = $071f ; Current scroll direction. 1=Scrolling right, 2=Scrolling left ColumnScrollDirection = $0720 ; Scroll direction that is updated only when scrolling crosses a column boundary ColumnScrollX = $0721 ; Scroll position within the current tile column ($00-$1f) DialogBoxDrawing = $0726 ; >0 when drawing dialog and during black screen transitions ScrollFrozen = $0728 ; 1=Screen scrolling frozen (for boss fights) -ScrollPage = $072a ; Which page the left-most pixel of the screen is on (0-3) -ScrollX = $072c +ScrollLeftPage = $072a ; Which page the left-most pixel of the screen is on (0-3) +ScrollRightPage = $72b +ScrollLeftX = $072c +ScrollRightX = $072d SideviewCommandY = $0730 SideviewCommandOp = $0731 -ScrollLeftPage = $0732 -ScrollRightPage = $0733 -ScrollLeftColumn = $0734 ; Which left background column to update during scrolling -ScrollRightColumn = $0735 ; Which right background column to update during scrolling +UpdateScrollColumnLeftPage = $0732 +UpdateScrollColumnRightPage = $0733 +UpdateScrollColumnLeftTile = $0734 ; Which left background column to update during scrolling +UpdateScrollColumnRightTile = $0735 ; Which right background column to update during scrolling GameMode = $0736 GameModeCurrent = $0737 ; GameMode is compared to this and only triggered if it has changed @@ -302,6 +344,8 @@ LocationNumber = $0748 ; Location index sideview exits to or $ MagicSelectorPosition = $0749 LinkFlashingTimer = $074b ; Link changes palette and this value is counted down until it's 0. 0b10000000 bit determines if the background changes color. CurrentDialogType = $074c ; 0=None, 1 = Level Up, 2 = Talking +HudSectionFlag = $074f ; 0x80=Draw Magic & Attack, 0x40=Draw Life & XP +HudDrawState = $0750 ExpToAddHi = $0755 ExpToAddLo = $0756 DoorDepth = $075b ; Used as an index for saving/loading your x position when you enter/leave (nested) doors @@ -340,6 +384,12 @@ HaveFlute = $0789 HaveCross = $078a HaveHammer = $078b HaveMagicKey = $078c +Crystal1Placed = $078d +Crystal2Placed = $078e +Crystal3Placed = $078f +Crystal4Placed = $0790 +Crystal5Placed = $0791 +Crystal6Placed = $0792 Keys = $0793 ; Current number of Keys Crystals = $0794 ; Number of Crystals left HaveStabs = $0796 ; 0x04 = Have Downstab | 0x10 = Have Upstab @@ -351,6 +401,13 @@ HaveChild = $079c ; 0x20 = Have Child HaveBasementAccess = $079d ; 0x08 = Have New Kasuto Basement access ContinuesUsed = $079f +; New variables that use free memory +ProjectileEnemyData = $07c0 ; [Z2R] 6 bytes for Hard Carock projectile type + +BackgroundTilesPage1 = $6000 +BackgroundTilesPage2 = $60d0 +BackgroundTilesPage3 = $61a0 +BackgroundTilesPage4 = $6270 SavedDoorExit1ScrollPage = $69aa ; accessed as $69a5,X with X = 5 SavedDoorExit1ScrollX = $69b1 ; accessed as $69ac,X with X = 5 From 81507e5f4f0d67c811c2bf4bb8affd8f5e0a434c Mon Sep 17 00:00:00 2001 From: initsu Date: Mon, 6 Jul 2026 02:48:44 +0200 Subject: [PATCH 17/56] Set both East P-bag caves to have 500 P like vanilla --- RandomizerCore/Hyrule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index ea03cf602..80793f1b9 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -684,7 +684,7 @@ private void ShuffleItems() else { westHyrule.pbagCave.SetCollectables([Collectable.LARGE_BAG]); - eastHyrule.pbagCave1.SetCollectables([Collectable.LARGE_BAG]); + eastHyrule.pbagCave1.SetCollectables([Collectable.XL_BAG]); eastHyrule.pbagCave2.SetCollectables([Collectable.XL_BAG]); } From 8198de4b113739188bae7b6b2df899818b1911c4 Mon Sep 17 00:00:00 2001 From: initsu Date: Sun, 10 May 2026 18:49:04 +0200 Subject: [PATCH 18/56] Refactor requireables to be a Set instead of List/Array --- RandomizerCore/Hyrule.cs | 6 +++--- Tests/RequirementsTests.cs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 80793f1b9..c665693d0 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -1385,7 +1385,7 @@ private bool IsEverythingReachable(Dictionary itemGet) previousReachableLocationsCount = reachableLocationsCount; previousGettableItemsCount = gettableItemsCount; gettableItemsCount = UpdateItemGets(props); - HashSet requireables = GetRequireables(props); + IReadOnlySet requireables = GetRequireables(props); westHyrule.UpdateVisit(requireables); deathMountain.UpdateVisit(requireables); eastHyrule.UpdateVisit(requireables); @@ -1556,7 +1556,7 @@ private void ShortenWizards() /// Whether any items were marked accessable private int UpdateItemGets(RandomizerProperties props) { - HashSet requireables; + IReadOnlySet requireables; accessibleMagicContainers = props.StartMagicContainers; accessibleHeartContainers = props.StartHearts; Location newKasuto = eastHyrule.newKasuto; @@ -1600,7 +1600,7 @@ private int UpdateItemGets(RandomizerProperties props) return gottenItems.Count; } - public HashSet GetRequireables(RandomizerProperties props) + public IReadOnlySet GetRequireables(RandomizerProperties props) { HashSet requireables = []; diff --git a/Tests/RequirementsTests.cs b/Tests/RequirementsTests.cs index 4f49a2a56..754ed66a3 100644 --- a/Tests/RequirementsTests.cs +++ b/Tests/RequirementsTests.cs @@ -14,6 +14,7 @@ public void TestJsonConstructor() Assert.AreEqual(json, serialized); } + [TestMethod] public void TestEmptyJsonConstructor() { string? json = @"[]"; From 2b0e98542b6dbadb1f2638c7441bb89f5f140a7b Mon Sep 17 00:00:00 2001 From: initsu Date: Thu, 28 May 2026 18:05:23 +0200 Subject: [PATCH 19/56] Refactor placeLongBridge code --- RandomizerCore/Overworld/World.cs | 178 ++++++++++++++---------------- 1 file changed, 82 insertions(+), 96 deletions(-) diff --git a/RandomizerCore/Overworld/World.cs b/RandomizerCore/Overworld/World.cs index f2c646515..63f53cebb 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -985,83 +985,69 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr Location bridge1 = GetLocation(LocationID.WEST_BRIDGE_AFTER_DM_WEST); Location bridge2 = GetLocation(LocationID.WEST_BRIDGE_AFTER_DM_EAST); - if (!walkableTerrains.Contains(map[y - deltaY, x - deltaX])) + IntVector2 pos = new(x, y); + IntVector2 startPos = new(startX, startY); + IntVector2 forward = new(deltaX, deltaY); + IntVector2 side = forward.Perpendicular(); + + IntVector2 backPos = pos - forward; + if (!walkableTerrains.Contains(map[backPos])) { - map[y - deltaY, x - deltaX] = Terrain.BRIDGE; + map[backPos] = Terrain.BRIDGE; } - while (!crossingTerrains.Contains(map[y + perpDy, x + perpDx]) && !crossingTerrains.Contains(map[y - perpDy, x - perpDx])) + while (!crossingTerrains.Contains(map[pos + side]) && + !crossingTerrains.Contains(map[pos - side])) { - if ((deltaY < 0 && y > startY) || (deltaY > 0 && y < startY) || (deltaX < 0 && x > startX) || (deltaX > 0 && x < startX)) + if (startPos.IsBehind(pos, forward)) { logger.Warn("Unable to roll back bridge location with jagged entrance"); return false; } - x -= deltaX; - y -= deltaY; + pos -= forward; } - if (crossingTerrains.Contains(map[y + deltaY, x + deltaX])) + + if (crossingTerrains.Contains(map[pos + forward])) { - x += deltaX; - y += deltaY; + pos += forward; } if (deltaX > 0 || deltaY > 0) { - bridge2.Xpos = x; - bridge2.Y = y; + bridge2.Pos = pos; } else { - bridge1.Xpos = x; - bridge1.Y = y; + bridge1.Pos = pos; } - if ((map[y + perpDy, x + perpDx] == Terrain.MOUNTAIN || map[y + perpDy, x + perpDx].IsWater()) - && map[y - perpDy, x - perpDx] != Terrain.MOUNTAIN && !map[y - perpDy, x - perpDx].IsWater()) - { - map[y - perpDy, x - perpDx] = map[y + perpDy, x + perpDx]; - } - if ((map[y - perpDy, x - perpDx] == Terrain.MOUNTAIN || map[y - perpDy, x - perpDx].IsWater()) - && map[y + perpDy, x + perpDx] != Terrain.MOUNTAIN && !map[y + perpDy, x + perpDx].IsWater()) - { - map[y + perpDy, x + perpDx] = map[y - perpDy, x - perpDx]; - } + NormalizeBridgeSideTerrain(map, pos, side); - while ((deltaX != 0 && x != startX) || (deltaY != 0 && y != startY)) + while (pos != startPos) { - map[y, x] = Terrain.BRIDGE; - x -= deltaX; - y -= deltaY; + map[pos] = Terrain.BRIDGE; + pos -= forward; } - if (crossingTerrains.Contains(map[y,x])) - { - map[y, x] = map[y - deltaY, x - deltaX]; - } - x += deltaX; - y += deltaY; - if ((map[y + perpDy, x + perpDx] == Terrain.MOUNTAIN || map[y + perpDy, x + perpDx].IsWater()) - && map[y - perpDy, x - perpDx] != Terrain.MOUNTAIN && !map[y - perpDy, x - perpDx].IsWater()) - { - map[y - perpDy, x - perpDx] = map[y + perpDy, x + perpDx]; - } - if ((map[y - perpDy, x - perpDx] == Terrain.MOUNTAIN || map[y - perpDy, x - perpDx].IsWater()) - && map[y + perpDy, x + perpDx] != Terrain.MOUNTAIN && !map[y + perpDy, x + perpDx].IsWater()) + if (crossingTerrains.Contains(map[pos])) { - map[y + perpDy, x + perpDx] = map[y - perpDy, x - perpDx]; + map[pos] = map[pos - forward]; } - map[y, x] = Terrain.BRIDGE; + + pos += forward; + + NormalizeBridgeSideTerrain(map, pos, side); + map[pos] = Terrain.BRIDGE; + if (deltaX > 0 || deltaY > 0) { - bridge1.Xpos = x; - bridge1.Y = y; + bridge1.Pos = pos; } else { - bridge2.Xpos = x; - bridge2.Y = y; + bridge2.Pos = pos; } + placeLongBridge = false; bridge1.CanShuffle = false; bridge2.CanShuffle = false; @@ -1072,89 +1058,70 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr Location bridge2 = GetLocation(LocationID.EAST_TRAP_DESERT1); if (bridge1.CanShuffle && bridge2.CanShuffle) { - if (!walkableTerrains.Contains(map[y - deltaY, x - deltaX])) + IntVector2 pos = new(x, y); + IntVector2 startPos = new(startX, startY); + IntVector2 forward = new(deltaX, deltaY); + IntVector2 side = new(perpDx, perpDy); + + if (!walkableTerrains.Contains(map[pos - forward])) { - map[y - deltaY, x - deltaX] = Terrain.DESERT; + map[pos - forward] = Terrain.DESERT; } - //Walk backwards until the first tile flanked on both sides by the river - while (!crossingTerrains.Contains(map[y + perpDy, x + perpDx]) && !crossingTerrains.Contains(map[y - perpDy, x - perpDx])) + + while (!crossingTerrains.Contains(map[pos + side]) && + !crossingTerrains.Contains(map[pos - side])) { - if ((deltaY < 0 && y > startY) || (deltaY > 0 && y < startY) || (deltaX < 0 && x > startX) || (deltaX > 0 && x < startX)) + if (startPos.IsBehind(pos, forward)) { logger.Warn("Unable to roll back bridge location with jagged entrance"); return false; } - x -= deltaX; - y -= deltaY; + pos -= forward; } - //That's where the first bridge encounter is + // first bridge encounter if (deltaX > 0 || deltaY > 0) { - bridge2.Xpos = x; - bridge2.Y = y; + bridge2.Pos = pos; } else { - bridge1.Xpos = x; - bridge1.Y = y; + bridge1.Pos = pos; } - if ((map[y + perpDy, x + perpDx] == Terrain.MOUNTAIN || map[y + perpDy, x + perpDx].IsWater()) - && map[y - perpDy, x - perpDx] != Terrain.MOUNTAIN && !map[y - perpDy, x - perpDx].IsWater()) - { - map[y - perpDy, x - perpDx] = map[y + perpDy, x + perpDx]; - } - if ((map[y - perpDy, x - perpDx] == Terrain.MOUNTAIN || map[y - perpDy, x - perpDx].IsWater()) - && map[y + perpDy, x + perpDx] != Terrain.MOUNTAIN && !map[y + perpDy, x + perpDx].IsWater()) - { - map[y + perpDy, x + perpDx] = map[y - perpDy, x - perpDx]; - } + NormalizeBridgeSideTerrain(map, pos, side); - //Keep walking back placing deserts until the path opens up - while ((deltaX != 0 && x != startX) || (deltaY != 0 && y != startY)) + while (pos != startPos) { - map[y, x] = Terrain.DESERT; - x -= deltaX; - y -= deltaY; + map[pos] = Terrain.DESERT; + pos -= forward; } - //If you're going into a C-shape, you'll have a dead stub of water. fill it in - if (map[y, x].IsWater()) + + if (map[pos].IsWater()) { - map[y, x] = map[y - deltaY, x - deltaX]; + map[pos] = map[pos - forward]; } - //now we're past the opening, so go the other way 1 - x += deltaX; - y += deltaY; + pos += forward; + + NormalizeBridgeSideTerrain(map, pos, side); + + map[pos] = Terrain.DESERT; - //If exactly one of the tiles adjacent to the bridge is a potentially impassable, demolish it so you can't get stuck in it. - if ((map[y + perpDy, x + perpDx] == Terrain.MOUNTAIN || map[y + perpDy, x + perpDx].IsWater()) - && map[y - perpDy, x - perpDx] != Terrain.MOUNTAIN && !map[y - perpDy, x - perpDx].IsWater()) - { - map[y - perpDy, x - perpDx] = map[y + perpDy, x + perpDx]; - } - if ((map[y - perpDy, x - perpDx] == Terrain.MOUNTAIN || map[y - perpDy, x - perpDx].IsWater()) - && map[y + perpDy, x + perpDx] != Terrain.MOUNTAIN && !map[y + perpDy, x + perpDx].IsWater()) - { - map[y + perpDy, x + perpDx] = map[y - perpDy, x - perpDx]; - } - map[y, x] = Terrain.DESERT; if (deltaX > 0 || deltaY > 0) { - bridge1.Xpos = x; - bridge1.Y = y; + bridge1.Pos = pos; } else { - bridge2.Xpos = x; - bridge2.Y = y; + bridge2.Pos = pos; } + bridge1.CanShuffle = false; bridge2.CanShuffle = false; } - placeDaruniaDesert = false; + placeDaruniaDesert = false; } else { @@ -1397,6 +1364,25 @@ private string PrintTerrainGlobMap(int[,] mass) return sb.ToString(); } + private static void NormalizeBridgeSideTerrain(OverworldMap map, IntVector2 pos, IntVector2 side) + { + IntVector2 left = pos - side; + IntVector2 right = pos + side; + + bool leftBlocked = map[left] == Terrain.MOUNTAIN || map[left].IsWater(); + bool rightBlocked = map[right] == Terrain.MOUNTAIN || map[right].IsWater(); + + if (rightBlocked && !leftBlocked) + { + map[left] = map[right]; + } + + if (leftBlocked && !rightBlocked) + { + map[right] = map[left]; + } + } + protected List NextToWaterDirections(int x, int y, Terrain[] crossingTerrains) { List directions = []; From 8beb430629940138f27c4635a290f15835e3369c Mon Sep 17 00:00:00 2001 From: initsu Date: Thu, 28 May 2026 18:41:00 +0200 Subject: [PATCH 20/56] Fix overshooting bridges and Darunia trap locations --- RandomizerCore/Overworld/World.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/RandomizerCore/Overworld/World.cs b/RandomizerCore/Overworld/World.cs index 63f53cebb..cb880dcff 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -990,10 +990,10 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr IntVector2 forward = new(deltaX, deltaY); IntVector2 side = forward.Perpendicular(); - IntVector2 backPos = pos - forward; - if (!walkableTerrains.Contains(map[backPos])) + pos -= forward; + if (!walkableTerrains.Contains(map[pos])) { - map[backPos] = Terrain.BRIDGE; + map[pos] = Terrain.BRIDGE; } while (!crossingTerrains.Contains(map[pos + side]) && @@ -1063,9 +1063,10 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr IntVector2 forward = new(deltaX, deltaY); IntVector2 side = new(perpDx, perpDy); - if (!walkableTerrains.Contains(map[pos - forward])) + pos -= forward; + if (!walkableTerrains.Contains(map[pos])) { - map[pos - forward] = Terrain.DESERT; + map[pos] = Terrain.DESERT; } while (!crossingTerrains.Contains(map[pos + side]) && From c02425a59b0ff4edf5a0ecc27b35a8571bbbd906 Mon Sep 17 00:00:00 2001 From: initsu Date: Wed, 29 Apr 2026 04:39:35 +0200 Subject: [PATCH 21/56] Move shared World.UpdateVisit case to base class --- RandomizerCore/Overworld/EastHyrule.cs | 22 ------------------- RandomizerCore/Overworld/WestHyrule.cs | 19 +---------------- RandomizerCore/Overworld/World.cs | 29 +++++++++++++++++++++++--- 3 files changed, 27 insertions(+), 43 deletions(-) diff --git a/RandomizerCore/Overworld/EastHyrule.cs b/RandomizerCore/Overworld/EastHyrule.cs index 1fadbdd5b..c65d139db 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -1594,28 +1594,6 @@ private bool RandomizeHiddenPalace(ROM rom, bool shuffleHidden, bool hiddenKasut } } - public override void UpdateVisit(IReadOnlySet requireables) - { - UpdateReachable(requireables); - - foreach (Location location in AllLocations) - { - if (location.Y > 0 && visitation[location.Y, location.Xpos]) - { - if(location.AccessRequirements.AreSatisfiedBy(requireables)) - { - location.Reachable = true; - if (connections.ContainsKey(location) && (location.Town == null || location.Town.CanBeTraversed(requireables))) - { - Location connectedLocation = connections[location]; - connectedLocation.Reachable = true; - visitation[connectedLocation.Y, connectedLocation.Xpos] = true; - } - } - } - } - } - private double ComputeDistance(Location l, Location l2) { return Math.Sqrt(Math.Pow(l.Xpos - l2.Xpos, 2) + Math.Pow(l.Y - l2.Y, 2)); diff --git a/RandomizerCore/Overworld/WestHyrule.cs b/RandomizerCore/Overworld/WestHyrule.cs index 12dc2ff58..c59068b73 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -1353,24 +1353,7 @@ private void DrawMountains() public override void UpdateVisit(IReadOnlySet requireables) { visitation[northPalace.Y, northPalace.Xpos] = true; - UpdateReachable(requireables); - - foreach (Location location in AllLocations) - { - if (location.Y > 0 && visitation[location.Y, location.Xpos]) - { - if (location.AccessRequirements.AreSatisfiedBy(requireables)) - { - location.Reachable = true; - if (connections.ContainsKey(location) && (location.Town == null || location.Town.CanBeTraversed(requireables))) - { - Location connectedLocation = connections[location]; - connectedLocation.Reachable = true; - visitation[connectedLocation.Y, connectedLocation.Xpos] = true; - } - } - } - } + base.UpdateVisit(requireables); } protected override List GetPathingStarts() diff --git a/RandomizerCore/Overworld/World.cs b/RandomizerCore/Overworld/World.cs index cb880dcff..d3b2a41a4 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -3158,8 +3158,6 @@ public bool ValidateCaves() return true; } - public abstract void UpdateVisit(IReadOnlySet requireables); - public abstract IEnumerable RequiredLocations(bool hiddenPalace, bool hiddenKasuto); //protected abstract void SetVanillaCollectables(bool useDash); @@ -3177,4 +3175,29 @@ public bool HasCollidingLocations() } return false; } -} \ No newline at end of file + + /// + /// Updates the visitation matrix and location reachability + /// + public virtual void UpdateVisit(IReadOnlySet requireables) + { + UpdateReachable(requireables); + + foreach (Location location in AllLocations) + { + if (location.Y > 0 && visitation[location.Y, location.Xpos]) + { + if (location.AccessRequirements.AreSatisfiedBy(requireables)) + { + location.Reachable = true; + if (connections.ContainsKey(location) && (location.Town == null || location.Town.CanBeTraversed(requireables))) + { + Location connectedLocation = connections[location]; + connectedLocation.Reachable = true; + visitation[connectedLocation.Y, connectedLocation.Xpos] = true; + } + } + } + } + } +} From f62e457d95e148bcdf09e28c7cc326944e917903 Mon Sep 17 00:00:00 2001 From: initsu Date: Mon, 26 Jan 2026 03:24:21 +0100 Subject: [PATCH 22/56] Comment out FillPalaceRooms assembler test run --- RandomizerCore/Hyrule.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index c665693d0..b843edcab 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -1338,6 +1338,7 @@ private async Task FillPalaceRooms(AsmModule sideviewModule) sideviewModule.Byt(itemBits); } + /* this shouldn't be needed anymore try { ROM testRom = new(ROMData); @@ -1360,6 +1361,7 @@ private async Task FillPalaceRooms(AsmModule sideviewModule) logger.Error(e, "Failed to build assembly patches"); throw; } + */ return true; } From a7813230dca82d698047ceeca8441600143a2a7b Mon Sep 17 00:00:00 2001 From: initsu Date: Fri, 10 Jul 2026 01:10:56 +0200 Subject: [PATCH 23/56] Change PalacesView checkboxes to use enabled via observable logic --- CrossPlatformUI/Assets/palace-blockers.png | Bin 27434 -> 0 bytes CrossPlatformUI/Lang/Resources.resx | 6 +- .../ViewModels/Tabs/PalacesViewModel.cs | 45 ++++++++ CrossPlatformUI/Views/Tabs/PalacesView.axaml | 105 ++++++++++-------- .../Views/Tabs/PalacesView.axaml.cs | 67 +---------- RandomizerCore/RandomizerConfiguration.cs | 19 +++- 6 files changed, 122 insertions(+), 120 deletions(-) delete mode 100644 CrossPlatformUI/Assets/palace-blockers.png diff --git a/CrossPlatformUI/Assets/palace-blockers.png b/CrossPlatformUI/Assets/palace-blockers.png deleted file mode 100644 index 7493ce1b0e53fb822aeaf12e50e4eb303c5681ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27434 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 diff --git a/CrossPlatformUI/Lang/Resources.resx b/CrossPlatformUI/Lang/Resources.resx index 9c3c371fd..43171f48b 100644 --- a/CrossPlatformUI/Lang/Resources.resx +++ b/CrossPlatformUI/Lang/Resources.resx @@ -775,8 +775,10 @@ considered duplicates of each other. Has no effect on vanilla palaces. Does not prevent duplicates for Random Walk generated palaces. -Allows rooms that require an item or spell to traverse to appear in any palace. Otherwise, -palaces can only contain blocks according to the following table. +Allows rooms that cannot be passed without a specific item or spell to appear in any palace. +Otherwise, the allowed requirements for rooms in palaces are based on the items and spells +needed to pass through every room in that palace in vanilla (from either direction). +See the Wiki for the full table. Removes dead-end rooms from the room pool. This helps newer players who are not yet diff --git a/CrossPlatformUI/ViewModels/Tabs/PalacesViewModel.cs b/CrossPlatformUI/ViewModels/Tabs/PalacesViewModel.cs index 37748c7da..6892599b2 100644 --- a/CrossPlatformUI/ViewModels/Tabs/PalacesViewModel.cs +++ b/CrossPlatformUI/ViewModels/Tabs/PalacesViewModel.cs @@ -12,15 +12,36 @@ public class PalacesViewModel : ReactiveObject, IActivatableViewModel public ViewModelActivator Activator { get; } public MainViewModel Main { get; } + public IObservable BossRoomsExitTypeIncludedObservable { get; } + public IObservable NoDuplicateRoomsByLayoutIncludedObservable { get; } + public IObservable NoDuplicateRoomsByEnemiesIncludedObservable { get; } public IObservable RandomStylesAllowVanillaIncludedObservable { get; } public IObservable RemoveLongDeadEndsIncludedObservable { get; } + public IObservable IncludeVanillaRoomsIncludedObservable { get; } + public IObservable Includev4_0RoomsIncludedObservable { get; } + public IObservable Includev5_0RoomsIncludedObservable { get; } public IObservable IncludeExpertRoomsIncludedObservable { get; } + public IObservable BlockingRoomsInAnyPalaceIncludedObservable { get; } + public IObservable RemoveTBirdIncludedObservable { get; } + public IObservable TBirdRequiredIncludedObservable { get; } public PalacesViewModel(MainViewModel main) { Main = main; Activator = new(); + BossRoomsExitTypeIncludedObservable = Main.FlagsChanged + .Select(_ => Main.Config.bossRoomsExitTypeIncluded()) + .DistinctUntilChanged(); + + NoDuplicateRoomsByLayoutIncludedObservable = Main.FlagsChanged + .Select(_ => Main.Config.noDuplicateRoomsByLayoutIncluded()) + .DistinctUntilChanged(); + + NoDuplicateRoomsByEnemiesIncludedObservable = Main.FlagsChanged + .Select(_ => Main.Config.noDuplicateRoomsByEnemiesIncluded()) + .DistinctUntilChanged(); + RandomStylesAllowVanillaIncludedObservable = Main.FlagsChanged .Select(_ => Main.Config.randomStylesAllowVanillaIncluded()) .DistinctUntilChanged(); @@ -29,10 +50,34 @@ public PalacesViewModel(MainViewModel main) .Select(_ => Main.Config.removeLongDeadEndsIncluded()) .DistinctUntilChanged(); + IncludeVanillaRoomsIncludedObservable = Main.FlagsChanged + .Select(_ => Main.Config.includeVanillaRoomsIncluded()) + .DistinctUntilChanged(); + + Includev4_0RoomsIncludedObservable = Main.FlagsChanged + .Select(_ => Main.Config.includev4_0RoomsIncluded()) + .DistinctUntilChanged(); + + Includev5_0RoomsIncludedObservable = Main.FlagsChanged + .Select(_ => Main.Config.includev5_0RoomsIncluded()) + .DistinctUntilChanged(); + IncludeExpertRoomsIncludedObservable = Main.FlagsChanged .Select(_ => Main.Config.includeExpertRoomsIncluded()) .DistinctUntilChanged(); + BlockingRoomsInAnyPalaceIncludedObservable = Main.FlagsChanged + .Select(_ => Main.Config.blockingRoomsInAnyPalaceIncluded()) + .DistinctUntilChanged(); + + RemoveTBirdIncludedObservable = Main.FlagsChanged + .Select(_ => Main.Config.removeTBirdIncluded()) + .DistinctUntilChanged(); + + TBirdRequiredIncludedObservable = Main.FlagsChanged + .Select(_ => Main.Config.tBirdRequiredIncluded()) + .DistinctUntilChanged(); + this.WhenActivated(OnActivate); } diff --git a/CrossPlatformUI/Views/Tabs/PalacesView.axaml b/CrossPlatformUI/Views/Tabs/PalacesView.axaml index 9f0153f5b..c693715b5 100644 --- a/CrossPlatformUI/Views/Tabs/PalacesView.axaml +++ b/CrossPlatformUI/Views/Tabs/PalacesView.axaml @@ -23,7 +23,7 @@ Foreground="#4557c4" Cursor="Hand" /> - + + + + + + + + + Number of Palaces to Complete @@ -123,20 +132,24 @@ - - + + No Duplicate Rooms(By Layout) - + + No Duplicate Rooms(By Layout and Enemy Set) @@ -147,11 +160,13 @@ Content="Restart at Palaces on Game Over"> + 50/50 Palace Statue CyclesBetween Jar and Iron Knuckle + @@ -160,22 +175,27 @@ - - - @@ -191,21 +211,17 @@ - - - - - - - - + + + @@ -223,31 +239,28 @@ Content="Harder Carock"> - - - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/CrossPlatformUI/Views/Tabs/PalacesView.axaml.cs b/CrossPlatformUI/Views/Tabs/PalacesView.axaml.cs index 9ec5991e2..fb792b4a4 100644 --- a/CrossPlatformUI/Views/Tabs/PalacesView.axaml.cs +++ b/CrossPlatformUI/Views/Tabs/PalacesView.axaml.cs @@ -22,12 +22,11 @@ public PalacesView() this.WhenActivated(disposables => { + // at most one of these two checkboxes must be checked CheckBox noDuplicateRoomsByLayoutCheckbox = this.FindControl("NoDuplicateRoomsByEnemiesCheckbox") ?? throw new System.Exception("Missing Required Validation Element"); CheckBox noDuplicateRoomsByEnemiesCheckbox = this.FindControl("NoDuplicateRoomsByLayoutCheckbox") ?? throw new System.Exception("Missing Required Validation Element"); - IObservable byLayoutObservable = noDuplicateRoomsByLayoutCheckbox.GetObservable(CheckBox.IsCheckedProperty); IObservable byEnemiesObservable = noDuplicateRoomsByEnemiesCheckbox.GetObservable(CheckBox.IsCheckedProperty); - byLayoutObservable.Subscribe(byLayoutValue => { if (byLayoutValue ?? false) @@ -36,7 +35,6 @@ public PalacesView() } }) .DisposeWith(disposables); - byEnemiesObservable.Subscribe(byEnemiesValue => { if (byEnemiesValue ?? false) @@ -46,70 +44,7 @@ public PalacesView() }) .DisposeWith(disposables); - ComboBox normalPalaceStyleSelector = this.FindControl("NormalPalaceStyleSelector") ?? throw new System.Exception("Missing Required Validation Element"); - ComboBox gpStyleSelector = this.FindControl("GPPalaceStyleSelector") ?? throw new System.Exception("Missing Required Validation Element"); - CheckBox thunderbirdRequiredCheckbox = this.FindControl("TbirdRequiredCheckbox") ?? throw new System.Exception("Missing Required Validation Element"); - CheckBox includeVanillaCheckbox = this.FindControl("IncludeVanillaRoomsCheckbox") ?? throw new System.Exception("Missing Required Validation Element"); - CheckBox include4_0Checkbox = this.FindControl("Include4_0RoomsCheckbox") ?? throw new System.Exception("Missing Required Validation Element"); - CheckBox include5_0Checkbox = this.FindControl("Include5_0RoomsCheckbox") ?? throw new System.Exception("Missing Required Validation Element"); - - CheckBox blockingRoomsAnywhereCheckbox = this.FindControl("BlockingRoomsInAnyPalaceCheckbox") ?? throw new System.Exception("Missing Required Validation Element"); - ComboBox bossRoomsExitTypeSelector = this.FindControl("BossRoomsExitTypeSelector") ?? throw new System.Exception("Missing Required Validation Element"); - - - var normalStyleObservable = normalPalaceStyleSelector.GetObservable(ComboBox.SelectedItemProperty); - var gpStyleObservable = gpStyleSelector.GetObservable(ComboBox.SelectedItemProperty); - - gpStyleObservable.Subscribe(selectedItem => - { - EnumDescription? selectedDescription = selectedItem as EnumDescription; - if(selectedDescription != null) - { - PalaceStyle palaceStyle = (PalaceStyle)(selectedDescription.Value ?? PalaceStyle.RECONSTRUCTED); - if (palaceStyle == PalaceStyle.VANILLA) - { - thunderbirdRequiredCheckbox.IsChecked = true; - thunderbirdRequiredCheckbox.IsEnabled = false; - } - else - { - thunderbirdRequiredCheckbox.IsEnabled = true; - } - } - }) - .DisposeWith(disposables); - - normalStyleObservable.CombineLatest(gpStyleObservable, (normal, gp) => - { - EnumDescription? normalStyleDescription = normal as EnumDescription; - PalaceStyle normalPalaceStyle = (PalaceStyle)(normalStyleDescription?.Value ?? PalaceStyle.RECONSTRUCTED); - EnumDescription? gpPalaceStyleDescription = gp as EnumDescription; - PalaceStyle gpPalaceStyle = (PalaceStyle)(gpPalaceStyleDescription?.Value ?? PalaceStyle.RECONSTRUCTED); - return !((normalPalaceStyle == PalaceStyle.VANILLA || normalPalaceStyle == PalaceStyle.SHUFFLED) - && (gpPalaceStyle == PalaceStyle.VANILLA || gpPalaceStyle == PalaceStyle.SHUFFLED)); - }) - .Subscribe(enableRoomSelection => - { - includeVanillaCheckbox.IsEnabled = enableRoomSelection; - include4_0Checkbox.IsEnabled = enableRoomSelection; - include5_0Checkbox.IsEnabled = enableRoomSelection; - noDuplicateRoomsByLayoutCheckbox.IsEnabled = enableRoomSelection; - noDuplicateRoomsByEnemiesCheckbox.IsEnabled = enableRoomSelection; - blockingRoomsAnywhereCheckbox.IsEnabled = enableRoomSelection; - bossRoomsExitTypeSelector.IsEnabled = enableRoomSelection; - if (!enableRoomSelection) - { - includeVanillaCheckbox.IsChecked = true; - include4_0Checkbox.IsChecked = false; - include5_0Checkbox.IsChecked = false; - noDuplicateRoomsByLayoutCheckbox.IsChecked = enableRoomSelection; - noDuplicateRoomsByEnemiesCheckbox.IsChecked = enableRoomSelection; - blockingRoomsAnywhereCheckbox.IsChecked = enableRoomSelection; - bossRoomsExitTypeSelector.SelectedIndex = 0; - } - }) - .DisposeWith(disposables); }); } } diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index d3816bf8a..159c52c59 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -345,6 +345,11 @@ private bool palaceStylesAreNotAllVanillaOrShuffled() return false; } + private bool roomSelectionEnabled() + { + return palaceStylesAreNotAllVanillaOrShuffled(); + } + private bool palaceStylesAnyMetastyleSelected() { foreach (var style in (List)[normalPalaceStyle, gpStyle]) @@ -363,26 +368,28 @@ private bool palaceStylesAnyMetastyleSelected() public bool randomStylesAllowVanillaIncluded() => palaceStylesAnyMetastyleSelected(); [Reactive] + [ConditionallyIncludeInFlags] private bool? includeVanillaRooms = true; + public bool includeVanillaRoomsIncluded() => roomSelectionEnabled(); [Reactive] [ConditionallyIncludeInFlags] private bool? includev4_0Rooms = false; - public bool includev4_0RoomsIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); + public bool includev4_0RoomsIncluded() => roomSelectionEnabled(); [Reactive] [ConditionallyIncludeInFlags] private bool? includev5_0Rooms = false; - public bool includev5_0RoomsIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); + public bool includev5_0RoomsIncluded() => roomSelectionEnabled(); [Reactive] [ConditionallyIncludeInFlags] private bool blockingRoomsInAnyPalace = false; - public bool blockingRoomsInAnyPalaceIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); + public bool blockingRoomsInAnyPalaceIncluded() => palaceStylesAreNotAllVanilla(); [Reactive] private PalaceDropStyle palaceDropStyle = PalaceDropStyle.ANY_EXIT; - public bool palaceDropStyleIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); + public bool palaceDropStyleIncluded() => palaceStylesAreNotAllVanilla(); [Reactive] [ConditionallyIncludeInFlags] @@ -392,12 +399,12 @@ private bool palaceStylesAnyMetastyleSelected() [Reactive] [ConditionallyIncludeInFlags] private bool includeExpertRooms = false; - public bool includeExpertRoomsIncluded() => palaceStylesAreNotAllVanilla(); + public bool includeExpertRoomsIncluded() => roomSelectionEnabled(); [Reactive] [ConditionallyIncludeInFlags] private BossRoomsExitType bossRoomsExitType = BossRoomsExitType.OVERWORLD; - public bool bossRoomsExitTypeIncluded() => palaceStylesAreNotAllVanillaOrShuffled(); + public bool bossRoomsExitTypeIncluded() => roomSelectionEnabled(); [Reactive] [ConditionallyIncludeInFlags] From 181f784406f5f7139afe89f1fea921e2863d7602 Mon Sep 17 00:00:00 2001 From: initsu Date: Wed, 8 Apr 2026 22:37:07 +0200 Subject: [PATCH 24/56] Create SpellsViewModel --- .../ViewModels/RandomizerViewModel.cs | 2 ++ .../ViewModels/Tabs/SpellsViewModel.cs | 26 +++++++++++++++++++ CrossPlatformUI/Views/RandomizerView.axaml | 2 +- CrossPlatformUI/Views/Tabs/SpellsView.axaml | 23 ++++++++-------- 4 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 CrossPlatformUI/ViewModels/Tabs/SpellsViewModel.cs diff --git a/CrossPlatformUI/ViewModels/RandomizerViewModel.cs b/CrossPlatformUI/ViewModels/RandomizerViewModel.cs index 8cf82394a..b984e5919 100644 --- a/CrossPlatformUI/ViewModels/RandomizerViewModel.cs +++ b/CrossPlatformUI/ViewModels/RandomizerViewModel.cs @@ -95,6 +95,7 @@ public RandomizerViewModel(MainViewModel main) HostScreen = Main; BiomesViewModel = new(Main); PalacesViewModel = new(Main); + SpellsViewModel = new(Main); ItemsViewModel = new(Main); HintsViewModel = new(Main, this); CustomizeViewModel = new(Main); @@ -330,6 +331,7 @@ private void AddValidationRules() [JsonIgnore] public BiomesViewModel BiomesViewModel { get; } public PalacesViewModel PalacesViewModel { get; } + public SpellsViewModel SpellsViewModel { get; } public ItemsViewModel ItemsViewModel { get; } public HintsViewModel HintsViewModel { get; } public CustomizeViewModel CustomizeViewModel { get; } diff --git a/CrossPlatformUI/ViewModels/Tabs/SpellsViewModel.cs b/CrossPlatformUI/ViewModels/Tabs/SpellsViewModel.cs new file mode 100644 index 000000000..4993e8c74 --- /dev/null +++ b/CrossPlatformUI/ViewModels/Tabs/SpellsViewModel.cs @@ -0,0 +1,26 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using ReactiveUI; + +namespace CrossPlatformUI.ViewModels.Tabs; + +[RequiresUnreferencedCode("ReactiveUI uses reflection")] +public class SpellsViewModel : ReactiveObject, IActivatableViewModel +{ + public ViewModelActivator Activator { get; } + public MainViewModel Main { get; } + + public SpellsViewModel(MainViewModel main) + { + Main = main; + Activator = new(); + + this.WhenActivated(OnActivate); + } + + internal void OnActivate(CompositeDisposable disposables) + { + } +} diff --git a/CrossPlatformUI/Views/RandomizerView.axaml b/CrossPlatformUI/Views/RandomizerView.axaml index e92492c3b..e21596498 100644 --- a/CrossPlatformUI/Views/RandomizerView.axaml +++ b/CrossPlatformUI/Views/RandomizerView.axaml @@ -193,7 +193,7 @@ - + diff --git a/CrossPlatformUI/Views/Tabs/SpellsView.axaml b/CrossPlatformUI/Views/Tabs/SpellsView.axaml index 2abd5d472..d7905fc41 100644 --- a/CrossPlatformUI/Views/Tabs/SpellsView.axaml +++ b/CrossPlatformUI/Views/Tabs/SpellsView.axaml @@ -6,42 +6,43 @@ xmlns:rc="clr-namespace:Z2Randomizer.RandomizerCore;assembly=RandomizerCore" xmlns:ui="clr-namespace:CrossPlatformUI" xmlns:vm="clr-namespace:CrossPlatformUI.ViewModels" + xmlns:vmt="clr-namespace:CrossPlatformUI.ViewModels.Tabs" xmlns:lang="clr-namespace:Z2Randomizer.CrossPlatformUI.Lang" mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="600" - x:DataType="vm:MainViewModel" - x:Class="CrossPlatformUI.Views.Tabs.SpellsView"> + x:Class="CrossPlatformUI.Views.Tabs.SpellsView" + x:DataType="vmt:SpellsViewModel"> @@ -50,26 +51,26 @@ Theme="{StaticResource MaterialOutlineComboBox}" assists:ComboBoxAssist.Label="Fire Spell" ItemsSource="{Binding Source={x:Static rc:Enums.FireOptionList}}" - SelectedItem="{Binding Config.FireOption, Converter={x:Static ui:Util.EnumConvert}}" + SelectedItem="{Binding Main.Config.FireOption, Converter={x:Static ui:Util.EnumConvert}}" > From cf43ec7aa94d687beb7c10729d09fc30a89376df Mon Sep 17 00:00:00 2001 From: initsu Date: Sun, 7 Jun 2026 13:22:34 +0200 Subject: [PATCH 25/56] Hide custom music in browser build as it can't be used --- .../Views/Tabs/CustomizeView.axaml | 90 ++++++++++--------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/CrossPlatformUI/Views/Tabs/CustomizeView.axaml b/CrossPlatformUI/Views/Tabs/CustomizeView.axaml index 0b9265e01..4114f29a5 100644 --- a/CrossPlatformUI/Views/Tabs/CustomizeView.axaml +++ b/CrossPlatformUI/Views/Tabs/CustomizeView.axaml @@ -61,52 +61,54 @@ - + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + From 72af387ad68f380ecaa269a666915141541e6054 Mon Sep 17 00:00:00 2001 From: initsu Date: Wed, 10 Jun 2026 06:33:54 +0200 Subject: [PATCH 26/56] UI flag update performance improvement --- .../FlagsSerializeGenerator.cs | 5 +++- .../ViewModels/RandomizerViewModel.cs | 23 ++++++++++++++----- RandomizerCore/RandomizerConfiguration.cs | 7 ++++++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/CoreSourceGenerator/FlagsSerializeGenerator.cs b/CoreSourceGenerator/FlagsSerializeGenerator.cs index b8335becf..683f04bca 100644 --- a/CoreSourceGenerator/FlagsSerializeGenerator.cs +++ b/CoreSourceGenerator/FlagsSerializeGenerator.cs @@ -323,7 +323,10 @@ private static void GenerateReactiveProperty(StringBuilder sb, ReactiveFieldInfo sb.AppendLine($"{indent} {{"); sb.AppendLine($"{indent} {field.FieldName} = value;"); sb.AppendLine($"{indent} OnPropertyChanged(nameof({field.PropertyName}));"); - sb.AppendLine($"{indent} OnPropertyChanged(\"Flags\");"); + sb.AppendLine($"{indent} if (!_inDeserializeFlags)"); + sb.AppendLine($"{indent} {{"); + sb.AppendLine($"{indent} OnPropertyChanged(\"Flags\");"); + sb.AppendLine($"{indent} }}"); sb.AppendLine($"{indent} }}"); sb.AppendLine($"{indent} }}"); sb.AppendLine($"{indent} }}{defaultValue}"); diff --git a/CrossPlatformUI/ViewModels/RandomizerViewModel.cs b/CrossPlatformUI/ViewModels/RandomizerViewModel.cs index b984e5919..48b659c52 100644 --- a/CrossPlatformUI/ViewModels/RandomizerViewModel.cs +++ b/CrossPlatformUI/ViewModels/RandomizerViewModel.cs @@ -227,16 +227,27 @@ private void OnActivate(CompositeDisposable disposables) // flag updates from RandomizerConfiguration always overwrites our flag input Main.FlagsObservable - .Do(x => FlagsValidSubject.OnNext(true)) - .Subscribe(flags => FlagInput = flags) + .Subscribe(flags => + { + FlagInput = flags; + FlagsValidSubject.OnNext(true); + }) .DisposeWith(disposables); - this.WhenAnyValue(x => x.FlagInput) + var validatedInputObservable = + this.WhenAnyValue(viewModel => viewModel.FlagInput) .WithLatestFrom(Main.FlagsObservable, (Input, Current) => (Input, Current, IsValid: Input == Current || IsFlagStringValid(Input))) - .Do(x => FlagsValidSubject.OnNext(x.IsValid)) - .Where(x => x.IsValid && x.Input != x.Current) - .Subscribe(x => Main.Config.DeserializeFlags(x.Input)) + .Replay(1) + .RefCount(); + + validatedInputObservable + .Subscribe(tuple => FlagsValidSubject.OnNext(tuple.IsValid)) + .DisposeWith(disposables); + + validatedInputObservable + .Where(tuple => tuple.IsValid && tuple.Input != tuple.Current) + .Subscribe(tuple => Main.Config.DeserializeFlags(tuple.Input)) .DisposeWith(disposables); Main.Config.PropertyChanged += (sender, args) => diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index 159c52c59..f6b57b87a 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -826,9 +826,16 @@ private bool palaceStylesAnyMetastyleSelected() private string? seed; // public string Seed { get => seed ?? ""; set => SetField(ref seed, value); } + [IgnoreInFlags] + private bool _inDeserializeFlags = false; + public void DeserializeFlags(string flags) { + // avoid emitting property changed for Flags during deserialization + _inDeserializeFlags = true; Deserialize(flags?.Trim() ?? ""); + _inDeserializeFlags = false; + OnPropertyChanged("Flags"); } public String SerializeFlags() { From 651764c1c7c5f3d000e36d3372a72285b213d324 Mon Sep 17 00:00:00 2001 From: initsu Date: Wed, 10 Jun 2026 06:34:35 +0200 Subject: [PATCH 27/56] Multi-line flag & seed paste support Sending the pasted text through the textbox makes it only possible to use the first line. I change it up to access the clipboard directly to make it work when the flags and seeds are on separate lines. --- .../ViewModels/RandomizerViewModel.cs | 52 +++++++------------ CrossPlatformUI/Views/RandomizerView.axaml | 2 +- CrossPlatformUI/Views/RandomizerView.axaml.cs | 35 ++++++++----- 3 files changed, 40 insertions(+), 49 deletions(-) diff --git a/CrossPlatformUI/ViewModels/RandomizerViewModel.cs b/CrossPlatformUI/ViewModels/RandomizerViewModel.cs index 48b659c52..e33ddadf5 100644 --- a/CrossPlatformUI/ViewModels/RandomizerViewModel.cs +++ b/CrossPlatformUI/ViewModels/RandomizerViewModel.cs @@ -31,25 +31,8 @@ public class RandomizerViewModel : ReactiveValidationObject, IRoutableViewModel, private static bool IsFlagStringValid(string flags) => FlagPasteParser.IsValidFlagString(flags); - private string flagInput = ""; - [JsonIgnore] - public string FlagInput - { - get => flagInput; - set - { - var trimmedValue = value?.Trim() ?? ""; - var (extractedFlags, extractedSeed) = FlagPasteParser.Parse(trimmedValue); - - if (Main is not null && !string.IsNullOrEmpty(extractedSeed)) - { - Main.Config.Seed = extractedSeed; - } - - this.RaiseAndSetIfChanged(ref flagInput, extractedFlags ?? trimmedValue); - } - } + public string FlagInput { get; set { field = value.Trim(); this.RaisePropertyChanged(); } } = ""; [JsonIgnore] public string Seed @@ -229,25 +212,26 @@ private void OnActivate(CompositeDisposable disposables) Main.FlagsObservable .Subscribe(flags => { - FlagInput = flags; FlagsValidSubject.OnNext(true); + FlagInput = flags; }) .DisposeWith(disposables); - var validatedInputObservable = - this.WhenAnyValue(viewModel => viewModel.FlagInput) - .WithLatestFrom(Main.FlagsObservable, - (Input, Current) => (Input, Current, IsValid: Input == Current || IsFlagStringValid(Input))) - .Replay(1) - .RefCount(); - - validatedInputObservable - .Subscribe(tuple => FlagsValidSubject.OnNext(tuple.IsValid)) - .DisposeWith(disposables); - - validatedInputObservable - .Where(tuple => tuple.IsValid && tuple.Input != tuple.Current) - .Subscribe(tuple => Main.Config.DeserializeFlags(tuple.Input)) + this.WhenAnyValue(viewModel => viewModel.FlagInput) + .WithLatestFrom(Main.FlagsObservable, (Input, Current) => (Input, Current)) + .Subscribe(tuple => + { + var isNew = tuple.Input != tuple.Current; + if (isNew) + { + bool isValid = IsFlagStringValid(tuple.Input); + FlagsValidSubject.OnNext(isValid); + if (isValid) + { + Main.Config.DeserializeFlags(tuple.Input); + } + } + }) .DisposeWith(disposables); Main.Config.PropertyChanged += (sender, args) => @@ -260,7 +244,7 @@ private void OnActivate(CompositeDisposable disposables) } }; - this.ValidationRule(x => x.FlagInput, FlagsValidSubject, "Invalid Flags"); + this.ValidationRule(viewModel => viewModel.FlagInput, FlagsValidSubject, "Invalid Flags"); AddValidationRules(); } diff --git a/CrossPlatformUI/Views/RandomizerView.axaml b/CrossPlatformUI/Views/RandomizerView.axaml index e21596498..5bc93f8af 100644 --- a/CrossPlatformUI/Views/RandomizerView.axaml +++ b/CrossPlatformUI/Views/RandomizerView.axaml @@ -124,7 +124,7 @@ - +