From 1aab7c71c41c20f633043fbb1533fe1cabf735de Mon Sep 17 00:00:00 2001 From: initsu Date: Sun, 12 Jul 2026 07:49:36 +0200 Subject: [PATCH 01/43] 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 89559ed0d..81baf3984 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; @@ -1962,28 +1959,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 fab3a21f1e5a00917c445cb35d42ad6e93639ff7 Mon Sep 17 00:00:00 2001 From: initsu Date: Sun, 31 May 2026 22:54:05 +0200 Subject: [PATCH 02/43] 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 644b69e3d..c077f3ebe 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 51cc1e5be..3be1bc808 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 d2c6b421d..8597ee90e 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 3365a9e56..0b53794aa 100644 --- a/RandomizerCore/EnumTypes.cs +++ b/RandomizerCore/EnumTypes.cs @@ -403,9 +403,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)] @@ -414,8 +412,6 @@ public enum ClimateEnum GREAT_LAKES, [Description("Scrubland"), DefaultWeight(1)] SCRUBLAND, - [Description("Scrubland"), DefaultWeight(1)] - DM_SCRUBLAND, [Description("Random"), Metastyle] RANDOM } @@ -426,9 +422,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, }; } @@ -437,9 +431,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, }; } @@ -448,9 +440,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 c2bcbdeb2..b8244f3fa 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 5a8b46a66..892fc7cf6 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -363,7 +363,7 @@ .. rom.LoadLocations(LocationID.EAST_TOWN_OLD_KASUTO, 4, terrains), hiddenKasutoLocation = townAtNewKasuto; //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); //climate.DisallowTerrain(Terrain.LAVA); diff --git a/RandomizerCore/Overworld/WestHyrule.cs b/RandomizerCore/Overworld/WestHyrule.cs index 2d206da32..089d4206d 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -301,7 +301,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 853d35cc4..1b3a4ad4d 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -791,7 +791,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 81baf3984..ca4b88b32 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 daf3d82e58f0f42059fd0b5c8dd0bb047a1178da Mon Sep 17 00:00:00 2001 From: initsu Date: Wed, 10 Jun 2026 14:00:37 +0200 Subject: [PATCH 03/43] 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 c216296cfb2904ef4cd261ea9e6a094e01151c71 Mon Sep 17 00:00:00 2001 From: initsu Date: Wed, 10 Jun 2026 16:15:27 +0200 Subject: [PATCH 04/43] 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 b669e3d5e79153f9accd3314c1409c5933bf10de Mon Sep 17 00:00:00 2001 From: initsu Date: Tue, 10 Feb 2026 11:09:43 +0100 Subject: [PATCH 05/43] 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 6635e3e7ef30c332177f526eb6db77439ccd08c2 Mon Sep 17 00:00:00 2001 From: initsu Date: Sat, 30 May 2026 11:25:01 +0200 Subject: [PATCH 06/43] 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 0bee12262eaa8afa3d9032891a6d83a5bc8e59ca Mon Sep 17 00:00:00 2001 From: initsu Date: Mon, 13 Jul 2026 04:44:57 +0200 Subject: [PATCH 07/43] 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 5a6e74ab3ffbd59d467d11e5145b70252ce4cb5f Mon Sep 17 00:00:00 2001 From: initsu Date: Sat, 13 Jun 2026 22:29:01 +0200 Subject: [PATCH 08/43] 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 ab471383fb3e84e7cae4b871ab9b9c3a3b37dc4d Mon Sep 17 00:00:00 2001 From: initsu Date: Thu, 28 May 2026 15:34:13 +0200 Subject: [PATCH 09/43] 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 b8244f3fa..901def598 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 892fc7cf6..46c2c4631 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -406,7 +406,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) { @@ -511,7 +511,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 a55458a1d..02f38290f 100644 --- a/RandomizerCore/Overworld/MazeIsland.cs +++ b/RandomizerCore/Overworld/MazeIsland.cs @@ -120,7 +120,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); @@ -146,7 +146,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 089d4206d..ee7aac6b9 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -375,7 +375,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> @@ -433,7 +433,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) locationAtSariaSouth.CanShuffle = false; locationAtSariaNorth.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 1b3a4ad4d..6947b4558 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -26,7 +26,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; @@ -1534,7 +1534,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; @@ -1965,7 +1965,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; @@ -2006,7 +2006,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; @@ -2042,7 +2042,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); @@ -2122,7 +2122,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); @@ -2163,7 +2163,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 fc2ecbaf4df33c2f1bd00dd67abd3a6dbc1f8879 Mon Sep 17 00:00:00 2001 From: initsu Date: Sat, 23 May 2026 11:48:05 +0200 Subject: [PATCH 10/43] 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 0e09c2b12..2635a9ff5 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -619,7 +619,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 8f01c64f7..8c47180ca 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -356,7 +356,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC OverworldEnemyShuffler.Shuffle(worlds, assembler, ROMData, props.MixLargeAndSmallEnemies, props.GeneratorsAlwaysMatch, r); } - if (props.CombineFire) + if (props.LinkedFireSpell != null) { List? customSpellOrder = props.IncludeSpellsInShuffle ? null @@ -365,7 +365,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC //This makes the assumption that is currently true that each "town" has exactly one item. //If we later restructure towns to be omni-towns to get rid of fake towns, this will be untrue .Select(l => l.Collectables[0]).ToList(); - ROMData.CombineFireSpell(assembler, customSpellOrder, r); + ROMData.CombineFireSpell(assembler, props.LinkedFireSpell.Value, customSpellOrder); } Dictionary spellMap = new() @@ -2447,7 +2447,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")); } @@ -2864,7 +2864,7 @@ what is visible in the dark and also doesn't add enough to be worth it imo. //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 4c345ea36..41fa4238b 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -2048,7 +2048,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 @@ -2058,8 +2058,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 ca4b88b32..546f16382 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 d947cf240..deb7f6c8e 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 4c8f0243da98c7cfff084bf415afefea38684155 Mon Sep 17 00:00:00 2001 From: initsu Date: Thu, 28 May 2026 11:26:47 +0200 Subject: [PATCH 11/43] 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 41fa4238b..e061b773e 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -924,17 +924,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"); } /// @@ -953,6 +954,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 @@ -976,8 +979,6 @@ asl a asl a jmp CheckStatNormally """); - a.Org(0xa8a7); - a.Label("LevelCaps"); } public void UseExtendedBanksForPalaceRooms(Assembler a) From 6cf31a1dc5e5c4a8cdcd1795064e633a8adbf6c2 Mon Sep 17 00:00:00 2001 From: initsu Date: Sat, 18 Apr 2026 10:25:12 +0200 Subject: [PATCH 12/43] 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 da1ef8516fbb83b725daf729f0f0386e1645ad5c Mon Sep 17 00:00:00 2001 From: initsu Date: Mon, 6 Jul 2026 02:48:44 +0200 Subject: [PATCH 13/43] 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 8c47180ca..9fb8eca8f 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -718,7 +718,7 @@ private void ShuffleItems() else { westHyrule.pbagCave.Collectables = [Collectable.LARGE_BAG]; - eastHyrule.pbagCave1.Collectables = [Collectable.LARGE_BAG]; + eastHyrule.pbagCave1.Collectables = [Collectable.XL_BAG]; eastHyrule.pbagCave2.Collectables = [Collectable.XL_BAG]; } From 3d602bdf1e1eb2031002005a9e38046a3f511088 Mon Sep 17 00:00:00 2001 From: initsu Date: Sun, 10 May 2026 18:49:04 +0200 Subject: [PATCH 14/43] Refactor requireables to be a Set instead of List/Array --- RandomizerCore/Hyrule.cs | 8 ++++---- RandomizerCore/Overworld/DeathMountain.cs | 2 +- RandomizerCore/Overworld/EastHyrule.cs | 2 +- RandomizerCore/Overworld/MazeIsland.cs | 2 +- RandomizerCore/Overworld/WestHyrule.cs | 2 +- RandomizerCore/Overworld/World.cs | 8 ++++---- RandomizerCore/Requirements.cs | 14 ++++++-------- RandomizerCore/Sidescroll/Palace.cs | 13 ++++++------- RandomizerCore/Sidescroll/Palaces.cs | 8 ++++---- RandomizerCore/Sidescroll/Room.cs | 2 +- RandomizerCore/Sidescroll/RoomPool.cs | 2 +- Tests/RequirementsTests.cs | 8 +++++--- 12 files changed, 35 insertions(+), 36 deletions(-) diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 9fb8eca8f..aef1b191f 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -1455,7 +1455,7 @@ private bool IsEverythingReachable(Dictionary itemGet) previousReachableLocationsCount = reachableLocationsCount; previousGettableItemsCount = gettableItemsCount; gettableItemsCount = UpdateItemGets(props); - List requireables = GetRequireables(props); + IReadOnlySet requireables = GetRequireables(props); westHyrule.UpdateVisit(requireables); deathMountain.UpdateVisit(requireables); eastHyrule.UpdateVisit(requireables); @@ -1640,7 +1640,7 @@ private void ShortenWizards() /// Whether any items were marked accessable private int UpdateItemGets(RandomizerProperties props) { - List requireables; + IReadOnlySet requireables; accessibleMagicContainers = props.StartMagicContainers; accessibleHeartContainers = props.StartHearts; Location newKasuto = eastHyrule.AllLocations.First(i => i.ActualTown == Town.NEW_KASUTO); @@ -1705,9 +1705,9 @@ private int UpdateItemGets(RandomizerProperties props) return gottenItems.Count; } - public List GetRequireables(RandomizerProperties props) + public IReadOnlySet GetRequireables(RandomizerProperties props) { - List requireables = []; + HashSet requireables = []; foreach(Collectable item in ItemGet.Keys) { diff --git a/RandomizerCore/Overworld/DeathMountain.cs b/RandomizerCore/Overworld/DeathMountain.cs index 901def598..886b83f5e 100644 --- a/RandomizerCore/Overworld/DeathMountain.cs +++ b/RandomizerCore/Overworld/DeathMountain.cs @@ -1103,7 +1103,7 @@ private bool MakeCaldera(bool canWalkOnWaterWithBoots) /// /// Updates the visitation matrix and location reachability /// - public override void UpdateVisit(List requireables) + public override void UpdateVisit(IReadOnlySet requireables) { UpdateReachable(requireables); diff --git a/RandomizerCore/Overworld/EastHyrule.cs b/RandomizerCore/Overworld/EastHyrule.cs index 46c2c4631..177083ef4 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -1669,7 +1669,7 @@ private bool RandomizeHiddenPalace(ROM rom, bool shuffleHidden, bool hiddenKasut } } - public override void UpdateVisit(List requireables) + public override void UpdateVisit(IReadOnlySet requireables) { UpdateReachable(requireables); diff --git a/RandomizerCore/Overworld/MazeIsland.cs b/RandomizerCore/Overworld/MazeIsland.cs index 02f38290f..4b207372a 100644 --- a/RandomizerCore/Overworld/MazeIsland.cs +++ b/RandomizerCore/Overworld/MazeIsland.cs @@ -897,7 +897,7 @@ private void DrawRiver(int fromY, int fromX, int toY, int toX, bool openWest, bo map[toY, toX] = openEast ? Terrain.WALKABLEWATER : Terrain.MOUNTAIN; } } - public override void UpdateVisit(List requireables) + public override void UpdateVisit(IReadOnlySet requireables) { bool changed = true; while (changed) diff --git a/RandomizerCore/Overworld/WestHyrule.cs b/RandomizerCore/Overworld/WestHyrule.cs index ee7aac6b9..bd14e0cb3 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -1385,7 +1385,7 @@ private void DrawMountains() } - public override void UpdateVisit(List requireables) + public override void UpdateVisit(IReadOnlySet requireables) { visitation[northPalace.Y, northPalace.Xpos] = true; UpdateReachable(requireables); diff --git a/RandomizerCore/Overworld/World.cs b/RandomizerCore/Overworld/World.cs index 6947b4558..dd7447d4d 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -1849,8 +1849,8 @@ protected bool DrawOcean(Direction direction, Terrain oceanTerrain) } return true; } - - protected void UpdateReachable(List requireables) + + protected void UpdateReachable(IReadOnlySet requireables) { List starts = GetPathingStarts(); @@ -1882,7 +1882,7 @@ protected virtual void OnUpdateReachableTrigger() } //This signature has gotten out of control, consider a refactor - protected void UpdateReachable(ref bool[,] covered, int start_y, int start_x, List requireables) + protected void UpdateReachable(ref bool[,] covered, int start_y, int start_x, IReadOnlySet requireables) { Stack<(int, int)> to_visit = new(); // push the initial coord to the visitation stack @@ -3214,7 +3214,7 @@ public void SynchronizeLinkedLocations() } } - public abstract void UpdateVisit(List requireables); + public abstract void UpdateVisit(IReadOnlySet requireables); public abstract IEnumerable RequiredLocations(bool hiddenPalace, bool hiddenKasuto); diff --git a/RandomizerCore/Requirements.cs b/RandomizerCore/Requirements.cs index 54534f6d1..e358d7a2c 100644 --- a/RandomizerCore/Requirements.cs +++ b/RandomizerCore/Requirements.cs @@ -100,24 +100,23 @@ public override string ToString() return Serialize(); } - public bool AreSatisfiedBy(IEnumerable requireables, bool enforceImplicitRequirements = true) + public bool AreSatisfiedBy(IReadOnlySet requireables, bool enforceImplicitRequirements = true) { if(IndividualRequirements.Length + CompositeRequirements.Length == 0) { return true; } var individualRequirementsSatisfied = false; - var requirementTypes = requireables as RequirementType[] ?? requireables.ToArray(); foreach (var requirement in IndividualRequirements) { - if (requirementTypes.Contains(requirement)) + if (requireables.Contains(requirement)) { individualRequirementsSatisfied = true; if (enforceImplicitRequirements && ImplicitRequirements.ContainsKey(requirement)) { foreach(RequirementType implicitRequirement in ImplicitRequirements[requirement]) { - if(!requirementTypes.Contains(implicitRequirement)) + if(!requireables.Contains(implicitRequirement)) { individualRequirementsSatisfied = false; continue; @@ -158,14 +157,13 @@ public bool AreSatisfiedBy(IEnumerable requireables, bool enfor return individualRequirementsSatisfied || compositeRequirementSatisfied; } - public bool AreSatisfiedBy(IEnumerable requireables, StatRandomizer statRoll) + public bool AreSatisfiedBy(IReadOnlySet requireables, StatRandomizer statRoll) { if (IndividualRequirements.Length + CompositeRequirements.Length == 0) { return true; } var individualRequirementsSatisfied = false; - var requirementTypes = requireables as RequirementType[] ?? requireables.ToArray(); statRoll.AssertHasRandomized(); @@ -177,7 +175,7 @@ bool StatAdjustedContainerRequirementSatisfied(RequirementType requirement) var requiredLevel = ImplicitMagicLevelRequirements[requirement]; var magicCost = statRoll.GetSpellCost(collectable, requiredLevel); var containerRequirement = MagicContainerRequirementFromCost(magicCost); - return requirementTypes.Contains(containerRequirement); + return requireables.Contains(containerRequirement); } else { @@ -187,7 +185,7 @@ bool StatAdjustedContainerRequirementSatisfied(RequirementType requirement) foreach (var requirement in IndividualRequirements) { - if (requirementTypes.Contains(requirement)) + if (requireables.Contains(requirement)) { individualRequirementsSatisfied = true; if (!StatAdjustedContainerRequirementSatisfied(requirement)) diff --git a/RandomizerCore/Sidescroll/Palace.cs b/RandomizerCore/Sidescroll/Palace.cs index 26647f65b..8fd139317 100644 --- a/RandomizerCore/Sidescroll/Palace.cs +++ b/RandomizerCore/Sidescroll/Palace.cs @@ -1037,7 +1037,7 @@ public void RandomizeEnemies(RandomizerProperties props, Random r) } } - public bool CanClearAllRooms(IEnumerable requireables, Collectable palaceItem) + public bool CanClearAllRooms(IReadOnlySet requireables, Collectable palaceItem) { //If the palace's item can be reached with the current items, it can be used to clear the rest of the palace. RequirementType? palaceItemRequirement = palaceItem.AsRequirement(); @@ -1047,8 +1047,8 @@ public bool CanClearAllRooms(IEnumerable requireables, Collecta //the shuffle will eventually put the item into the reachable place. if (CanReachAnItemRoom(requireables)) { - requireables = new List(requireables); - ((List)requireables).Add((RequirementType)palaceItemRequirement); + HashSet palaceRequireables = [.. requireables, palaceItemRequirement.Value]; + requireables = palaceRequireables; } else return false; } @@ -1135,7 +1135,7 @@ public bool HasDisallowedDrop(bool palacesContinueAfterBoss, PalaceDropStyle dro return false; } - public bool CanReachAnItemRoom(IEnumerable requireables) + public bool CanReachAnItemRoom(IReadOnlySet requireables) { List pendingRooms = new() { AllRooms.First(i => i.IsEntrance) }; List coveredRooms = new(); @@ -1175,10 +1175,9 @@ public bool CanReachAnItemRoom(IEnumerable requireables) return false; } - public List GetGettableItems(IEnumerable initialRequireables) + public List GetGettableItems(IReadOnlySet initialRequireables) { - List requireables = []; - requireables.AddRange(initialRequireables); + HashSet requireables = new(initialRequireables); List pendingRooms = new() { AllRooms.First(i => i.IsEntrance) }; List coveredRooms = []; List previousGettableItems = []; diff --git a/RandomizerCore/Sidescroll/Palaces.cs b/RandomizerCore/Sidescroll/Palaces.cs index 91de75a27..9aa62dec6 100644 --- a/RandomizerCore/Sidescroll/Palaces.cs +++ b/RandomizerCore/Sidescroll/Palaces.cs @@ -304,7 +304,7 @@ private static bool AtLeastOnePalaceCanHaveGlove(RandomizerProperties props, Lis { return true; } - List requireables = + HashSet requireables = [ RequirementType.KEY, RequirementType.UPSTAB, @@ -329,12 +329,12 @@ private static bool CanGetGlove(RandomizerProperties props, Palace palace2) { if (!props.ShufflePalaceItems) { - List requireables = [..RequirementTypeExtensions.UpToXContainers(props.StartMagicContainers)]; + HashSet requireables = [.. RequirementTypeExtensions.UpToXContainers(props.StartMagicContainers)]; //If shuffle overworld items is on, we assume you can get all the items / spells //as all progression items will eventually shuffle into spots that work if (props.ShuffleOverworldItems) { - requireables = [RequirementType.KEY, ..RequirementTypeExtensions.UpToXContainers(8)]; + requireables = [RequirementType.KEY, .. RequirementTypeExtensions.UpToXContainers(8)]; } //Otherwise if it's vanilla items we can't get the magic key, because we could need glove for boots for flute to get to new kasuto @@ -357,7 +357,7 @@ private static bool CanGetRaft(RandomizerProperties props, bool raftIsRequired, //or it will send the logic into an uinrecoverable nosedive since the palaces can't re-generate if (!props.ShufflePalaceItems && raftIsRequired) { - List requireables; + HashSet requireables; //If shuffle overworld items is on, we assume you can get all the items / spells //as all progression items will eventually shuffle into spots that work if (props.ShuffleOverworldItems) diff --git a/RandomizerCore/Sidescroll/Room.cs b/RandomizerCore/Sidescroll/Room.cs index dd6fce0ec..a6b5ddcd2 100644 --- a/RandomizerCore/Sidescroll/Room.cs +++ b/RandomizerCore/Sidescroll/Room.cs @@ -454,7 +454,7 @@ public string PrintUnsatisfiedExits() } return sb.ToString(); } - public bool IsTraversable(IEnumerable requireables) + public bool IsTraversable(IReadOnlySet requireables) { return Requirements.AreSatisfiedBy(requireables); } diff --git a/RandomizerCore/Sidescroll/RoomPool.cs b/RandomizerCore/Sidescroll/RoomPool.cs index 2bddd9d39..5782884c7 100644 --- a/RandomizerCore/Sidescroll/RoomPool.cs +++ b/RandomizerCore/Sidescroll/RoomPool.cs @@ -154,7 +154,7 @@ public RoomPool(PalaceRooms palaceRooms, int palaceNumber, RandomizerProperties if (!props.BlockersAnywhere) { - RequirementType[] allowedBlockers = Palaces.ALLOWED_BLOCKERS_BY_PALACE[palaceNumber - 1]; + HashSet allowedBlockers = [.. Palaces.ALLOWED_BLOCKERS_BY_PALACE[palaceNumber - 1]]; RemoveRooms(room => !room.IsTraversable(allowedBlockers)); } diff --git a/Tests/RequirementsTests.cs b/Tests/RequirementsTests.cs index d91a27c06..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 = @"[]"; @@ -22,11 +23,12 @@ public void TestEmptyJsonConstructor() Assert.AreEqual(json, serialized); } + [TestMethod] public void TestSingleRequirement() { - Requirements requirements = new Requirements(new RequirementType[] { RequirementType.JUMP }); - RequirementType[] requireables = new RequirementType[] { }; + Requirements requirements = new Requirements([RequirementType.JUMP]); + RequirementType[] requireables = []; - Assert.IsFalse(requirements.AreSatisfiedBy(new RequirementType[] { })); + Assert.IsFalse(requirements.AreSatisfiedBy(new HashSet())); } } From e545c6d0f5ede71dc730ca2511f478c42f176835 Mon Sep 17 00:00:00 2001 From: initsu Date: Thu, 28 May 2026 18:05:23 +0200 Subject: [PATCH 15/43] 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 dd7447d4d..0f564c424 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -1012,83 +1012,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; @@ -1099,89 +1085,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 { @@ -1424,6 +1391,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 274bd6e79aa788cfd1180b775b9ea974cdf8d744 Mon Sep 17 00:00:00 2001 From: initsu Date: Thu, 28 May 2026 18:41:00 +0200 Subject: [PATCH 16/43] 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 0f564c424..5173dfee9 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -1017,10 +1017,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]) && @@ -1090,9 +1090,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 6bdea6fb3d2d0908d3977789b76b447b06dad718 Mon Sep 17 00:00:00 2001 From: initsu Date: Mon, 1 Jun 2026 11:40:23 +0200 Subject: [PATCH 17/43] Fix rare endless Reconstructed generation time --- RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs | 2 +- RandomizerCore/Sidescroll/PalaceGenerator.cs | 2 +- RandomizerCore/Sidescroll/Palaces.cs | 3 ++- .../Sidescroll/ReconstructedLoopyPalaceGenerator.cs | 4 ++-- .../Sidescroll/ReconstructedPalaceGenerator.cs | 12 ++++++++++-- .../SequentialPlacementCoordinatePalaceGenerator.cs | 2 +- .../ShapeFirstCoordinatePalaceGenerator.cs | 2 +- RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs | 2 +- .../Sidescroll/VanillaShufflePalaceGenerator.cs | 4 ++-- 9 files changed, 21 insertions(+), 12 deletions(-) diff --git a/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs b/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs index 57c6545ce..c08c6403b 100644 --- a/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs @@ -13,7 +13,7 @@ internal class ChaosPalaceGenerator : PalaceGenerator private const int CONNECTION_ATTEMPT_LIMIT = 200; private static int debug = 0; - internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber) + internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt) { debug++; bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); diff --git a/RandomizerCore/Sidescroll/PalaceGenerator.cs b/RandomizerCore/Sidescroll/PalaceGenerator.cs index 707ba44c2..a7d8253ff 100644 --- a/RandomizerCore/Sidescroll/PalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/PalaceGenerator.cs @@ -16,7 +16,7 @@ public abstract class PalaceGenerator protected static readonly IEqualityComparer byteArrayEqualityComparer = new Util.StandardByteArrayEqualityComparer(); - internal abstract Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber); + internal abstract Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt); protected static bool AllowDuplicatePrevention(RandomizerProperties props, int palaceNumber) { diff --git a/RandomizerCore/Sidescroll/Palaces.cs b/RandomizerCore/Sidescroll/Palaces.cs index 9aa62dec6..fe859d1a2 100644 --- a/RandomizerCore/Sidescroll/Palaces.cs +++ b/RandomizerCore/Sidescroll/Palaces.cs @@ -91,9 +91,10 @@ public async Task> CreatePalaces(Random r, RandomizerProperties pro roomPool = new(palaceRooms, currentPalace, props); } Palace palace; + int attempts = 0; do { - palace = await palaceGenerator.GeneratePalace(props, roomPool, r, sizes[currentPalace - 1], currentPalace); + palace = await palaceGenerator.GeneratePalace(props, roomPool, r, sizes[currentPalace - 1], currentPalace, attempts++); } while (!palace.IsValid); palace.BossRoom!.Enemies = (byte[])roomPool.VanillaBossRoom.Enemies.Clone(); PalaceGenerator.DebugCheckDuplicates(props, palace); diff --git a/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs b/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs index 16fc9749e..a431c836d 100644 --- a/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs @@ -8,12 +8,12 @@ namespace Z2Randomizer.RandomizerCore.Sidescroll; public class ReconstructedLoopyPalaceGenerator(CancellationToken ct) : ReconstructedPalaceGenerator(ct) { - internal override Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber) + internal override Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt) { rooms.RemoveRooms(room => room.HasDrop); rooms.RemoveRooms(room => !room.IsEntrance && !room.IsBossRoom && !room.HasItem && RoomExitTypeExtensions.DEADENDS.Contains(room.CategorizeExits())); - return base.GeneratePalace(props, rooms, r, roomCount, palaceNumber); + return base.GeneratePalace(props, rooms, r, roomCount, palaceNumber, attempt); } public override void Consolidate(List openRooms, RandomizerProperties props, int palaceNumber) diff --git a/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs b/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs index 4ad33d683..bcb4bc77c 100644 --- a/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs @@ -14,8 +14,15 @@ public class ReconstructedPalaceGenerator(CancellationToken ct) : PalaceGenerato static int debug = 0; private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber) + internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt) { + if (roomCount > 40 && attempt > 500) + { + // eventually try smaller palaces to avoid deadlock when + // room pool + room count together create unconsolidatable palaces + roomCount = Math.Max(40, roomCount - (attempt / 500)); + } + int tries = 0; debug++; @@ -210,8 +217,9 @@ internal override async Task GeneratePalace(RandomizerProperties props, /* if(palace.Number == 7 && palace.AllRooms.Count == roomCount && palace.AllRooms.Count(i => i.CountOpenExits() > 0) < 4) { - Debug.WriteLine(""); + Debug.WriteLine("Unable to consolidate Reconstructed rooms:"); palace.AllRooms.Where(i => i.CountOpenExits() > 0).ToList().ForEach(i => Debug.WriteLine(i.OpenExitsDebug())); + Debug.WriteLine(""); } */ palace.IsValid = false; diff --git a/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs index 2a4ee2304..1e78ac710 100644 --- a/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs @@ -26,7 +26,7 @@ public class SequentialPlacementCoordinatePalaceGenerator : CoordinatePalaceGene private int palaceNumber; private RoomPool? roomPool; - internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNum) + internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNum, int attempt) { this.roomCount = roomCount; this.palaceNumber = palaceNum; diff --git a/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs index 28c1d7d43..d333497d1 100644 --- a/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs @@ -13,7 +13,7 @@ namespace Z2Randomizer.RandomizerCore.Sidescroll; public abstract class ShapeFirstCoordinatePalaceGenerator() : CoordinatePalaceGenerator() { - internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber) + internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt) { bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); Palace palace = new(palaceNumber); diff --git a/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs b/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs index 113f9b6e1..22bf62307 100644 --- a/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs @@ -10,7 +10,7 @@ public class VanillaPalaceGenerator() : PalaceGenerator { private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber) + internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt) { VanillaRoomPool roomPool = (VanillaRoomPool)rooms; if(roomPool.BossRooms.Count != 1 diff --git a/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs b/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs index 6e192bc1e..d7a47d55e 100644 --- a/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs @@ -5,9 +5,9 @@ namespace Z2Randomizer.RandomizerCore.Sidescroll; public class VanillaShufflePalaceGenerator() : VanillaPalaceGenerator() { - internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber) + internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt) { - Palace palace = await base.GeneratePalace(props, rooms, r, roomCount, palaceNumber); + Palace palace = await base.GeneratePalace(props, rooms, r, roomCount, palaceNumber, attempt); palace.ResetRooms(); palace.ShuffleRooms(r); From b2087ce443e4225cf67fdcf3828244f6334fd256 Mon Sep 17 00:00:00 2001 From: initsu Date: Wed, 29 Apr 2026 04:39:35 +0200 Subject: [PATCH 18/43] Move shared World.UpdateVisit case to base class --- RandomizerCore/Overworld/EastHyrule.cs | 22 ---------------------- RandomizerCore/Overworld/WestHyrule.cs | 19 +------------------ RandomizerCore/Overworld/World.cs | 25 ++++++++++++++++++++++++- 3 files changed, 25 insertions(+), 41 deletions(-) diff --git a/RandomizerCore/Overworld/EastHyrule.cs b/RandomizerCore/Overworld/EastHyrule.cs index 177083ef4..e2c8a098c 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -1669,28 +1669,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.ConnectionRequirements.AreSatisfiedBy(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 bd14e0cb3..2010e8e84 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -1388,24 +1388,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.ConnectionRequirements.AreSatisfiedBy(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 5173dfee9..50378a933 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -3201,7 +3201,30 @@ public void SynchronizeLinkedLocations() } } - public abstract void UpdateVisit(IReadOnlySet requireables); + /// + /// 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.ConnectionRequirements.AreSatisfiedBy(requireables)) + { + Location connectedLocation = connections[location]; + connectedLocation.Reachable = true; + visitation[connectedLocation.Y, connectedLocation.Xpos] = true; + } + } + } + } + } public abstract IEnumerable RequiredLocations(bool hiddenPalace, bool hiddenKasuto); From d11861047bdd43fe859dc436395fd5da69e25aef Mon Sep 17 00:00:00 2001 From: initsu Date: Mon, 26 Jan 2026 03:24:21 +0100 Subject: [PATCH 19/43] 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 aef1b191f..3b1305582 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -1408,6 +1408,7 @@ private async Task FillPalaceRooms(AsmModule sideviewModule) sideviewModule.Byt(itemBits); } + /* this shouldn't be needed anymore try { ROM testRom = new(ROMData); @@ -1430,6 +1431,7 @@ private async Task FillPalaceRooms(AsmModule sideviewModule) logger.Error(e, "Failed to build assembly patches"); throw; } + */ return true; } From 16691829460ed5688b1941dda92a9bc88ec69600 Mon Sep 17 00:00:00 2001 From: initsu Date: Fri, 10 Jul 2026 01:10:56 +0200 Subject: [PATCH 20/43] 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 1a2dbc393..4868ef5ce 100644 --- a/CrossPlatformUI/Lang/Resources.resx +++ b/CrossPlatformUI/Lang/Resources.resx @@ -766,8 +766,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 546f16382..fc7a099c4 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 b19d88c0c1ee986eb5261b7bc360559cdb1bfadd Mon Sep 17 00:00:00 2001 From: initsu Date: Wed, 8 Apr 2026 22:37:07 +0200 Subject: [PATCH 21/43] 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 2d6411feba1296e27901073471a644a35aa3b951 Mon Sep 17 00:00:00 2001 From: initsu Date: Sun, 7 Jun 2026 13:22:34 +0200 Subject: [PATCH 22/43] 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 2562d28144cf914e48627073fd630489bd676b4a Mon Sep 17 00:00:00 2001 From: initsu Date: Wed, 10 Jun 2026 06:33:54 +0200 Subject: [PATCH 23/43] 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 fc7a099c4..6c742860a 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 3d15250ae40bef7707f143222c5ac8e6c7b4b46a Mon Sep 17 00:00:00 2001 From: initsu Date: Wed, 10 Jun 2026 06:34:35 +0200 Subject: [PATCH 24/43] 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 @@ - +