From 4ce54982b534dbe4f63875b0393493734de0eeda Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Tue, 3 Feb 2026 21:04:15 -0600 Subject: [PATCH 01/13] let's go --- src/Blocktavius.AppDQB2/ProjectVM.cs | 22 ++++++++ .../Generators/Hills/CornerPusherHill.cs | 30 +++++++---- src/Blocktavius.Core/WIP.cs | 52 +++++++++++++++++++ 3 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 src/Blocktavius.Core/WIP.cs diff --git a/src/Blocktavius.AppDQB2/ProjectVM.cs b/src/Blocktavius.AppDQB2/ProjectVM.cs index cc2c379..8eeb9ab 100644 --- a/src/Blocktavius.AppDQB2/ProjectVM.cs +++ b/src/Blocktavius.AppDQB2/ProjectVM.cs @@ -3,6 +3,7 @@ using Blocktavius.AppDQB2.Services; using Blocktavius.Core; using Blocktavius.DQB2; +using Blocktavius.DQB2.Mutations; using GongSolutions.Wpf.DragDrop; using System; using System.Collections.Generic; @@ -302,6 +303,12 @@ private void ExpandChunks(IReadOnlySet expansion) return null; } + if (this.SelectedScript == Scripts.FirstOrDefault()) // NOMERGE!! + { + DropTheHammer(workingStage); + return workingStage; + } + var context = new StageRebuildContext(workingStage); var mutation = this.SelectedScript?.BuildMutation(context); if (mutation != null) @@ -312,6 +319,21 @@ private void ExpandChunks(IReadOnlySet expansion) return workingStage; } + private static void DropTheHammer(IMutableStage stage) + { + var prng = PRNG.Deserialize("1-2-3-67-67-67"); + + var hill = WIP.Blah(prng.AdvanceAndClone()); + var mut = new PutHillMutation() + { + Block = 21, + Sampler = hill.TranslateTo(new XZ(1093, 1118)), + YFloor = 1, + }; + + stage.Mutate(mut); + } + public void OnImagesSelected(ImageChooserDialog.VM result) { var chunkGridLayer = Layers.LastOrDefault() as ChunkGridLayer; diff --git a/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs b/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs index 08a26a7..bf4ac47 100644 --- a/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs +++ b/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs @@ -44,10 +44,21 @@ public static I2DSampler BuildHill(Settings settings, Shell shell) { throw new ArgumentException("Shell must not be a hole"); } - return BuildHill(settings, Layer.FirstLayer(shell, settings.MaxElevation, settings), shell.IslandArea.AsArea()); + + var area = new ExpandableArea(shell); + BuildHill(settings, area); + + return area.GetSampler(ExpansionId.MaxValue, settings.MaxElevation) + .Project(tuple => tuple.Item1 ? tuple.Item2 : -1); } - private static I2DSampler BuildHill(Settings settings, Layer firstLayer, IArea origArea) + internal static void BuildHill(Settings settings, ExpandableArea area) + { + var layer = Layer.FirstLayer(area, settings); + BuildHill(settings, layer); + } + + private static void BuildHill(Settings settings, Layer firstLayer) { var layers = new Stack(); layers.Push(firstLayer); @@ -62,9 +73,6 @@ private static I2DSampler BuildHill(Settings settings, Layer firstLayer, IA var area = finalLayer.area; var finalExpansion = finalLayer.pendingExpansion.Select(kvp => (kvp.Key, kvp.Value.Elevation)).ToList(); area.Expand(finalExpansion); - - return area.GetSampler(ExpansionId.MaxValue, settings.MaxElevation) - .Project(tuple => tuple.Item1 ? tuple.Item2 : -1); } sealed class Layer @@ -120,20 +128,20 @@ private Layer(Layer prev) UpdateMisses(pendingExpansion, shellItems, elevation); } - private Layer(Shell shell, int elevation, Settings settings) + private Layer(ExpandableArea area, Settings settings) { - this.area = new ExpandableArea(shell); - this.shellItems = new ShellItemRing(shell.ShellItems); + this.area = area; + this.shellItems = new ShellItemRing(area.CurrentShell()); this.pendingExpansion = new(); - this.elevation = elevation; + this.elevation = settings.MaxElevation; this.settings = settings; UpdateMisses(pendingExpansion, shellItems, elevation); } - public static Layer FirstLayer(Shell shell, int elevation, Settings settings) + public static Layer FirstLayer(ExpandableArea area, Settings settings) { - var layer = new Layer(shell, elevation, settings); + var layer = new Layer(area, settings); // Overwrite legacy initial miss counts if percentage range is valid. decimal minPercent = Math.Clamp(settings.MinInitialMissPercent, 0m, 1m); diff --git a/src/Blocktavius.Core/WIP.cs b/src/Blocktavius.Core/WIP.cs new file mode 100644 index 0000000..2ab0ec4 --- /dev/null +++ b/src/Blocktavius.Core/WIP.cs @@ -0,0 +1,52 @@ +using Blocktavius.Core.Generators.Hills; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blocktavius.Core; + +public static class WIP +{ + public static I2DSampler Blah(PRNG prng) + { + const int maxElevation = 30; + + var area = InitialShell(prng, maxElevation); + CornerPusherHill.BuildHill(new CornerPusherHill.Settings() + { + Prng = prng.Clone(), + MinElevation = 1, + MaxElevation = maxElevation, + }, area); + + return area.GetSampler(ExpansionId.MaxValue, maxElevation).Project(t => t.Item1 ? t.Item2 : -1); + } + + private static ExpandableArea InitialShell(PRNG prng, int fillValue) + { + //var initArea = new Rect(XZ.Zero, XZ.Zero.Add(1, 1)).AsArea(); + var initArea = new Rect(XZ.Zero, XZ.Zero.Add(20, 50)).AsArea(); + var initShell = ShellLogic.ComputeShells(initArea).Single(); + + var area = new ExpandableArea(initShell); + var shell = area.CurrentShell(); + //while (shell.Count < 600) + + var size = initArea.Bounds.start; + while (size.X + size.Z + Math.Min(size.X, size.Z) < 200) + { + int i = prng.NextInt32(shell.Count); + var item = shell[i]; + if (item.CornerType != CornerType.Outside) // outside corners would cause "not connected" exceptions + { + area.Expand([(shell[i].XZ, fillValue)]); + shell = area.CurrentShell(); + size = area.GetSampler(ExpansionId.MaxValue, fillValue).Bounds.Size; + } + } + + return area; + } +} From 50b1f67689bcf0b0a5b27387645e7f344bf0d307 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Tue, 3 Feb 2026 21:49:40 -0600 Subject: [PATCH 02/13] chisel --- src/Blocktavius.AppDQB2/ProjectVM.cs | 39 +- .../Generators/Hills/CornerPusherHill.cs | 332 ++++++++++-------- src/Blocktavius.Core/WIP.cs | 46 ++- .../Mutations/PutHillMutation.cs | 28 ++ 4 files changed, 283 insertions(+), 162 deletions(-) diff --git a/src/Blocktavius.AppDQB2/ProjectVM.cs b/src/Blocktavius.AppDQB2/ProjectVM.cs index 8eeb9ab..bc6100f 100644 --- a/src/Blocktavius.AppDQB2/ProjectVM.cs +++ b/src/Blocktavius.AppDQB2/ProjectVM.cs @@ -321,13 +321,46 @@ private void ExpandChunks(IReadOnlySet expansion) private static void DropTheHammer(IMutableStage stage) { + /* +(define (chisel->mask chisel) + (case chisel + [(none) 0] + ; * 1/3/5/7 - diagonal chisel N/E/S/W, matches (blueprint.chisel_status << 4) + ; * 2/4/6/8 - diagonal chisel SW/SE/NW/NE + ; * 9/a/b/c - concave chisel NW/SW/SE/NE + [(flat-lo) #xe000] + [(flat-hi) #xf000])) + */ + var prng = PRNG.Deserialize("1-2-3-67-67-67"); var hill = WIP.Blah(prng.AdvanceAndClone()); - var mut = new PutHillMutation() + var hill2 = hill.Project(item => + { + ushort blockId; + if (item.Kind == WIP.HillItemKind.Plateau) + { + blockId = 4; + } + else if (item.Kind == WIP.HillItemKind.Chisel) + { + blockId = 21 | 0xe000; + } + else if (item.Kind == WIP.HillItemKind.Cliff) + { + blockId = 21; + } + else + { + blockId = 0; + return (-1, blockId); + } + return (item.Elevation, blockId); + }); + + var mut = new PutHillMutation2() { - Block = 21, - Sampler = hill.TranslateTo(new XZ(1093, 1118)), + Sampler = hill2.TranslateTo(new XZ(1093, 1118)), YFloor = 1, }; diff --git a/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs b/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs index bf4ac47..003123f 100644 --- a/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs +++ b/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs @@ -46,202 +46,232 @@ public static I2DSampler BuildHill(Settings settings, Shell shell) } var area = new ExpandableArea(shell); - BuildHill(settings, area); + var algo = new IntAlgorithm(); + algo.BuildHill(settings, area); return area.GetSampler(ExpansionId.MaxValue, settings.MaxElevation) .Project(tuple => tuple.Item1 ? tuple.Item2 : -1); } - internal static void BuildHill(Settings settings, ExpandableArea area) + internal static void BuildHill(Settings settings, ExpandableArea area, Func converter) { - var layer = Layer.FirstLayer(area, settings); - BuildHill(settings, layer); + var algo = new GenericAlgorithm() { Converter = converter }; + algo.BuildHill(settings, area); } - private static void BuildHill(Settings settings, Layer firstLayer) + abstract class Algorithm { - var layers = new Stack(); - layers.Push(firstLayer); - - int needLayers = settings.MaxElevation - settings.MinElevation + 1; - while (layers.Count < needLayers) + public void BuildHill(Settings settings, ExpandableArea area) { - layers.Push(layers.Peek().NextLayer(settings.Prng)); + var layer = Layer.FirstLayer(area, settings, this); + BuildHill(settings, layer); } - var finalLayer = layers.First(); - var area = finalLayer.area; - var finalExpansion = finalLayer.pendingExpansion.Select(kvp => (kvp.Key, kvp.Value.Elevation)).ToList(); - area.Expand(finalExpansion); - } + protected abstract IEnumerable<(XZ, T)> ConvertExpansion(IEnumerable<(XZ, int)> expansion); - sealed class Layer - { - public sealed class PendingShellItem + private void BuildHill(Settings settings, Layer firstLayer) { - public int MissCount { get; set; } // mutable - - /// - /// We retain the elevation this item was *introduced* at to maintain - /// compatability with a previous version of this algorithm. - /// (Also because I can't figure out why it gets more jagged when we break compatability.) - /// - public int Elevation { get; init; } - } + var layers = new Stack(); + layers.Push(firstLayer); - /// - /// A "miss" occurs whenever we decide not to push a shell item out for the next layer. - /// This dictionary tracks consecutive miss counts; it resets to 0 when we push that XZ. - /// Permitting a larger number of consecutive misses creates a more vertical (steeper) hill. - /// - public readonly Dictionary pendingExpansion; - - public readonly ExpandableArea area; - public readonly ShellItemRing shellItems; - public readonly int elevation; - public readonly Settings settings; - - private static void UpdateMisses(Dictionary pendingItems, IEnumerable shellItems, int elevation) - { - // reduce inside corners weight from 3:1 down to 2:1 - foreach (var item in shellItems.Where(i => i.CornerType != CornerType.Inside)) + int needLayers = settings.MaxElevation - settings.MinElevation + 1; + while (layers.Count < needLayers) { - if (pendingItems.TryGetValue(item.XZ, out var pendingItem)) - { - pendingItem.MissCount++; - } - else - { - pendingItems[item.XZ] = new PendingShellItem { MissCount = 1, Elevation = elevation }; - } + layers.Push(layers.Peek().NextLayer(settings.Prng)); } - } - - private Layer(Layer prev) - { - this.area = prev.area; - this.pendingExpansion = prev.pendingExpansion; - this.shellItems = new ShellItemRing(area.CurrentShell()); - this.elevation = prev.elevation - 1; - this.settings = prev.settings; - UpdateMisses(pendingExpansion, shellItems, elevation); + var finalLayer = layers.First(); + var area = finalLayer.area; + var finalExpansion = finalLayer.pendingExpansion.Select(kvp => (kvp.Key, kvp.Value.Elevation)).ToList(); + area.Expand(ConvertExpansion(finalExpansion)); } - private Layer(ExpandableArea area, Settings settings) + sealed class Layer { - this.area = area; - this.shellItems = new ShellItemRing(area.CurrentShell()); - this.pendingExpansion = new(); - this.elevation = settings.MaxElevation; - this.settings = settings; + public sealed class PendingShellItem + { + public int MissCount { get; set; } // mutable + + /// + /// We retain the elevation this item was *introduced* at to maintain + /// compatability with a previous version of this algorithm. + /// (Also because I can't figure out why it gets more jagged when we break compatability.) + /// + public int Elevation { get; init; } + } - UpdateMisses(pendingExpansion, shellItems, elevation); - } + /// + /// A "miss" occurs whenever we decide not to push a shell item out for the next layer. + /// This dictionary tracks consecutive miss counts; it resets to 0 when we push that XZ. + /// Permitting a larger number of consecutive misses creates a more vertical (steeper) hill. + /// + public readonly Dictionary pendingExpansion; - public static Layer FirstLayer(ExpandableArea area, Settings settings) - { - var layer = new Layer(area, settings); - - // Overwrite legacy initial miss counts if percentage range is valid. - decimal minPercent = Math.Clamp(settings.MinInitialMissPercent, 0m, 1m); - decimal maxPercent = Math.Clamp(settings.MaxInitialMissPercent, 0m, 1m); - int minMisses = Convert.ToInt32(minPercent * settings.MaxConsecutiveMisses); - int maxMisses = Convert.ToInt32(maxPercent * settings.MaxConsecutiveMisses); - bool reseed = (minMisses > 0 || maxMisses > 0) - && maxMisses >= minMisses - && settings.MinInitialMissPercent >= 0 - && settings.MaxInitialMissPercent >= 0; - - if (reseed) + public readonly Algorithm algorithm; + public readonly ExpandableArea area; + public readonly ShellItemRing shellItems; + public readonly int elevation; + public readonly Settings settings; + + private static void UpdateMisses(Dictionary pendingItems, IEnumerable shellItems, int elevation) { - var prng = settings.Prng.Clone(); - foreach (var kvp in layer.pendingExpansion) + // reduce inside corners weight from 3:1 down to 2:1 + foreach (var item in shellItems.Where(i => i.CornerType != CornerType.Inside)) { - kvp.Value.MissCount = prng.NextInt32(minMisses, maxMisses + 1); + if (pendingItems.TryGetValue(item.XZ, out var pendingItem)) + { + pendingItem.MissCount++; + } + else + { + pendingItems[item.XZ] = new PendingShellItem { MissCount = 1, Elevation = elevation }; + } } } - return layer; - } - - public Layer NextLayer(PRNG prng) - { - var prevLayer = this.shellItems; - - int maxConsecutiveMisses = settings.MaxConsecutiveMisses; - - List<(XZ, int)> expansion = new(); - - // Starting from a random point on the shell, do one lap. - // While one lap not completed: - // * choose random run length, subject to constraints - // * do or don't push out those shell items for next layer - // * flip the "do or don't push" flag for the next run - // - // To implement the "start from a random point on the shell" we - // * choose a random start index - // * loop from startIndex to startIndex + Count - // * use modulo during list indexing since our loopingIndex will (very likely) go past the end of the list - int startIndex = prng.NextInt32(prevLayer.Count); - int endIndex = startIndex + prevLayer.Count; - int loopingIndex = startIndex; - bool push = false; - do + private Layer(Layer prev) { - push = !push; - if (push) - { - - const int minRunLength = 2; - const int maxRunLength = 7; + this.algorithm = prev.algorithm; + this.area = prev.area; + this.pendingExpansion = prev.pendingExpansion; + this.shellItems = new ShellItemRing(area.CurrentShell()); + this.elevation = prev.elevation - 1; + this.settings = prev.settings; + + UpdateMisses(pendingExpansion, shellItems, elevation); + } - int mustTakeNow = prevLayer.OneLapFrom(loopingIndex) - .TakeWhile(i => pendingExpansion[i.XZ].MissCount >= maxConsecutiveMisses) - .Take(maxRunLength) - .Count(); + private Layer(ExpandableArea area, Settings settings, Algorithm algorithm) + { + this.algorithm = algorithm; + this.area = area; + this.shellItems = new ShellItemRing(area.CurrentShell()); + this.pendingExpansion = new(); + this.elevation = settings.MaxElevation; + this.settings = settings; + + UpdateMisses(pendingExpansion, shellItems, elevation); + } - int runLength = prng.NextInt32(Math.Max(mustTakeNow, minRunLength), 1 + Math.Max(mustTakeNow, maxRunLength)); - for (int i = 0; i < runLength; i++) + public static Layer FirstLayer(ExpandableArea area, Settings settings, Algorithm algorithm) + { + var layer = new Layer(area, settings, algorithm); + + // Overwrite legacy initial miss counts if percentage range is valid. + decimal minPercent = Math.Clamp(settings.MinInitialMissPercent, 0m, 1m); + decimal maxPercent = Math.Clamp(settings.MaxInitialMissPercent, 0m, 1m); + int minMisses = Convert.ToInt32(minPercent * settings.MaxConsecutiveMisses); + int maxMisses = Convert.ToInt32(maxPercent * settings.MaxConsecutiveMisses); + bool reseed = (minMisses > 0 || maxMisses > 0) + && maxMisses >= minMisses + && settings.MinInitialMissPercent >= 0 + && settings.MaxInitialMissPercent >= 0; + + if (reseed) + { + var prng = settings.Prng.Clone(); + foreach (var kvp in layer.pendingExpansion) { - var prevItem = prevLayer[loopingIndex % prevLayer.Count]; - loopingIndex++; - int elev = pendingExpansion[prevItem.XZ].Elevation; - expansion.Add((prevItem.XZ, elev)); + kvp.Value.MissCount = prng.NextInt32(minMisses, maxMisses + 1); } } - else - { - const int minRunLength = 2; - //const int maxRunLength = 50; - int maxRunLength = Math.Min(prevLayer.Count / 2, 50); - int mustStopAt = prevLayer.OneLapFrom(loopingIndex) - .TakeWhile(i => pendingExpansion[i.XZ].MissCount < maxConsecutiveMisses) - .Take(maxRunLength) - .Count(); + return layer; + } - int runLength; - if (mustStopAt < minRunLength) + public Layer NextLayer(PRNG prng) + { + var prevLayer = this.shellItems; + + int maxConsecutiveMisses = settings.MaxConsecutiveMisses; + + List<(XZ, int)> expansion = new(); + + // Starting from a random point on the shell, do one lap. + // While one lap not completed: + // * choose random run length, subject to constraints + // * do or don't push out those shell items for next layer + // * flip the "do or don't push" flag for the next run + // + // To implement the "start from a random point on the shell" we + // * choose a random start index + // * loop from startIndex to startIndex + Count + // * use modulo during list indexing since our loopingIndex will (very likely) go past the end of the list + int startIndex = prng.NextInt32(prevLayer.Count); + int endIndex = startIndex + prevLayer.Count; + int loopingIndex = startIndex; + bool push = false; + do + { + push = !push; + if (push) { - runLength = mustStopAt; + + const int minRunLength = 2; + const int maxRunLength = 7; + + int mustTakeNow = prevLayer.OneLapFrom(loopingIndex) + .TakeWhile(i => pendingExpansion[i.XZ].MissCount >= maxConsecutiveMisses) + .Take(maxRunLength) + .Count(); + + int runLength = prng.NextInt32(Math.Max(mustTakeNow, minRunLength), 1 + Math.Max(mustTakeNow, maxRunLength)); + for (int i = 0; i < runLength; i++) + { + var prevItem = prevLayer[loopingIndex % prevLayer.Count]; + loopingIndex++; + int elev = pendingExpansion[prevItem.XZ].Elevation; + expansion.Add((prevItem.XZ, elev)); + } } else { - runLength = prng.NextInt32(minRunLength, 1 + Math.Min(mustStopAt, maxRunLength)); + const int minRunLength = 2; + //const int maxRunLength = 50; + int maxRunLength = Math.Min(prevLayer.Count / 2, 50); + + int mustStopAt = prevLayer.OneLapFrom(loopingIndex) + .TakeWhile(i => pendingExpansion[i.XZ].MissCount < maxConsecutiveMisses) + .Take(maxRunLength) + .Count(); + + int runLength; + if (mustStopAt < minRunLength) + { + runLength = mustStopAt; + } + else + { + runLength = prng.NextInt32(minRunLength, 1 + Math.Min(mustStopAt, maxRunLength)); + } + loopingIndex += runLength; } - loopingIndex += runLength; } - } - while (loopingIndex < endIndex); + while (loopingIndex < endIndex); - foreach (var item in expansion) - { - pendingExpansion.Remove(item.Item1); + foreach (var item in expansion) + { + pendingExpansion.Remove(item.Item1); + } + area.Expand(algorithm.ConvertExpansion(expansion)); + + return new Layer(this); } - area.Expand(expansion); + } + } + + class IntAlgorithm : Algorithm + { + protected override IEnumerable<(XZ, int)> ConvertExpansion(IEnumerable<(XZ, int)> expansion) => expansion; + } - return new Layer(this); + class GenericAlgorithm : Algorithm + { + public required Func Converter { get; init; } + + protected override IEnumerable<(XZ, T)> ConvertExpansion(IEnumerable<(XZ, int)> expansion) + { + return expansion.Select(a => (a.Item1, Converter(a.Item2))); } } } diff --git a/src/Blocktavius.Core/WIP.cs b/src/Blocktavius.Core/WIP.cs index 2ab0ec4..0802a85 100644 --- a/src/Blocktavius.Core/WIP.cs +++ b/src/Blocktavius.Core/WIP.cs @@ -9,28 +9,50 @@ namespace Blocktavius.Core; public static class WIP { - public static I2DSampler Blah(PRNG prng) + public static I2DSampler Blah(PRNG prng) { const int maxElevation = 30; - var area = InitialShell(prng, maxElevation); - CornerPusherHill.BuildHill(new CornerPusherHill.Settings() + var plateauItem = new HillItem { Elevation = maxElevation, Kind = HillItemKind.Plateau }; + + var area = BuildPlateau(prng, plateauItem); + AddChisel(area, maxElevation); + + var settings = new CornerPusherHill.Settings() { Prng = prng.Clone(), MinElevation = 1, - MaxElevation = maxElevation, - }, area); + MaxElevation = maxElevation - 1, + }; + CornerPusherHill.BuildHill(settings, area, y => new HillItem { Elevation = y, Kind = HillItemKind.Cliff }); - return area.GetSampler(ExpansionId.MaxValue, maxElevation).Project(t => t.Item1 ? t.Item2 : -1); + return area.GetSampler(ExpansionId.MaxValue, plateauItem) + .Project(t => t.Item1 ? t.Item2 : HillItem.Nothing); } - private static ExpandableArea InitialShell(PRNG prng, int fillValue) + public enum HillItemKind + { + None, + Plateau, + Chisel, + Cliff, + } + + public readonly record struct HillItem + { + public int Elevation { get; init; } + public HillItemKind Kind { get; init; } + + public static readonly HillItem Nothing = new() { Elevation = -1, Kind = HillItemKind.None }; + } + + private static ExpandableArea BuildPlateau(PRNG prng, HillItem fillValue) { //var initArea = new Rect(XZ.Zero, XZ.Zero.Add(1, 1)).AsArea(); var initArea = new Rect(XZ.Zero, XZ.Zero.Add(20, 50)).AsArea(); var initShell = ShellLogic.ComputeShells(initArea).Single(); - var area = new ExpandableArea(initShell); + var area = new ExpandableArea(initShell); var shell = area.CurrentShell(); //while (shell.Count < 600) @@ -43,10 +65,18 @@ private static ExpandableArea InitialShell(PRNG prng, int fillValue) { area.Expand([(shell[i].XZ, fillValue)]); shell = area.CurrentShell(); + // NOMERGE - this should be CurrentBounds or something: size = area.GetSampler(ExpansionId.MaxValue, fillValue).Bounds.Size; } } return area; } + + private static void AddChisel(ExpandableArea area, int elevation) + { + var shell = area.CurrentShell(); + var value = new HillItem { Elevation = elevation, Kind = HillItemKind.Chisel }; + area.Expand(shell.Select(item => (item.XZ, value))); + } } diff --git a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs index 99fc664..385fff4 100644 --- a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs +++ b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs @@ -33,3 +33,31 @@ internal override void Apply(IMutableStage stage) } } } + +public sealed class PutHillMutation2 : StageMutation +{ + public required I2DSampler<(int, ushort)> Sampler { get; init; } + public int? YFloor { get; init; } = null; + + internal override void Apply(IMutableStage stage) + { + int yFloor = this.YFloor ?? 1; + + foreach (var chunk in Enumerate(Sampler.Bounds, stage)) + { + foreach (var xz in chunk.Offset.Bounds.Intersection(Sampler.Bounds).Enumerate()) + { + var (elevation, block) = Sampler.Sample(xz); + if (elevation > 0) + { + var unchiseled = (ushort)Block.MakeCanonical(block); + for (int y = yFloor; y < elevation; y++) + { + chunk.SetBlock(new Point(xz, y), unchiseled); + } + chunk.SetBlock(new Point(xz, elevation), block); + } + } + } + } +} From e6d7876f17b6f73dded3f7831a07cb96b5ac68ee Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Wed, 4 Feb 2026 20:37:41 -0600 Subject: [PATCH 03/13] This is looking pretty nice already! --- src/Blocktavius.AppDQB2/ProjectVM.cs | 154 +++++++++++++++--- src/Blocktavius.Core/WIP.cs | 33 ++-- .../Mutations/PutHillMutation.cs | 30 ++++ 3 files changed, 187 insertions(+), 30 deletions(-) diff --git a/src/Blocktavius.AppDQB2/ProjectVM.cs b/src/Blocktavius.AppDQB2/ProjectVM.cs index bc6100f..b2840cf 100644 --- a/src/Blocktavius.AppDQB2/ProjectVM.cs +++ b/src/Blocktavius.AppDQB2/ProjectVM.cs @@ -332,39 +332,153 @@ private static void DropTheHammer(IMutableStage stage) [(flat-hi) #xf000])) */ + var availableSpace = new Rect(new XZ(900, 900), new XZ(1200, 1200)); + + const int groundMinY = 13; + stage.Mutate(new ClearEverythingMutation() { StartY = groundMinY }); + var prng = PRNG.Deserialize("1-2-3-67-67-67"); - var hill = WIP.Blah(prng.AdvanceAndClone()); - var hill2 = hill.Project(item => + stage.Mutate(MakeGround(availableSpace, groundMinY)); + + var arranged = Arrange(HillRequests(), availableSpace, prng); + + foreach (var (position, request) in arranged) { - ushort blockId; - if (item.Kind == WIP.HillItemKind.Plateau) + var hill = WIP.Blah(prng.AdvanceAndClone(), request); + var hill2 = hill.Project(item => { - blockId = 4; - } - else if (item.Kind == WIP.HillItemKind.Chisel) + ushort blockId; + if (item.Kind == WIP.HillItemKind.Plateau) + { + blockId = 4; + } + else if (item.Kind == WIP.HillItemKind.Chisel) + { + blockId = 21 | 0xe000; + } + else if (item.Kind == WIP.HillItemKind.Cliff) + { + blockId = 21; + } + else + { + blockId = 0; + return (-1, blockId); + } + return (item.Elevation, blockId); + }); + + var mut = new PutHillMutation2() { - blockId = 21 | 0xe000; + Sampler = hill2.TranslateTo(position.start), + YFloor = 1, + }; + + stage.Mutate(mut); + } + } + + private static PutHillMutation MakeGround(Rect rect, int y) + { + var sampler = new ConstantSampler { Bounds = rect, Value = y }; + return new PutHillMutation() + { + Block = 3, + Sampler = sampler, + YFloor = 1, + }; + } + + sealed record HillSpec + { + public required int Elevation { get; init; } + public required int Size { get; init; } + } + + private static IEnumerable HillRequests() + { + // random rotation should be applied here, or at least before Arrange() + yield return new WIP.HillRequest { Elevation = 50, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(120, 80)) }; + yield return new WIP.HillRequest { Elevation = 48, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(80, 120)) }; + yield return new WIP.HillRequest { Elevation = 40, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(60, 45)) }; + yield return new WIP.HillRequest { Elevation = 32, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(80, 150)) }; + yield return new WIP.HillRequest { Elevation = 30, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 80)) }; + yield return new WIP.HillRequest { Elevation = 26, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(100, 100)) }; + yield return new WIP.HillRequest { Elevation = 24, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(70, 140)) }; + yield return new WIP.HillRequest { Elevation = 20, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; + yield return new WIP.HillRequest { Elevation = 18, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; + } + + private static List<(Rect translatedPosition, WIP.HillRequest request)> Arrange(IEnumerable requests, Rect fullSpace, PRNG prng) + { + var positions = requests.Select(request => (position: request.SeedSize, request)).ToList(); + var best = (positions, overlap: int.MaxValue); // lower values are better, zero is perfect + + for (int attempt = 0; attempt < 5; attempt++) + { + int totalOverlap = 0; + + for (int i = 0; i < positions.Count; i++) + { + var item = positions[i]; + var range = new Rect(fullSpace.start, fullSpace.end.Subtract(item.request.SeedSize.Size)); + var x = prng.NextInt32(range.start.X, range.end.X); + var z = prng.NextInt32(range.start.Z, range.end.Z); + var start = new XZ(x, z); + var pos = new Rect(start, start.Add(item.request.SeedSize.Size)); + + var overlappers = positions.Take(i).Select(x => x.position).ToList(); + + var current = (overlap: int.MaxValue, pos); + foreach (var dir in Direction.CardinalDirections()) + { + if (current.overlap == 0) { break; } + var pushed = BestPush(pos, overlappers, dir.Step, fullSpace); + if (pushed.Item1 < current.overlap) + { + current = pushed; + } + } + + positions[i] = (current.pos, item.request); + totalOverlap += current.overlap; } - else if (item.Kind == WIP.HillItemKind.Cliff) + + if (totalOverlap < best.overlap) { - blockId = 21; + best = (positions.ToList(), totalOverlap); } - else + } + + return best.positions; + } + + private static (int, Rect) BestPush(Rect rect, IReadOnlyList overlappers, XZ pushDir, Rect fullSpace) + { + int overlap(Rect rect) + { + int o = 0; + foreach (var other in overlappers) { - blockId = 0; - return (-1, blockId); + var i = other.Intersection(rect); + o += i.Size.X * i.Size.Z; } - return (item.Elevation, blockId); - }); + return o; + } - var mut = new PutHillMutation2() + var best = (overlap(rect), rect); + while (best.Item1 > 0 && fullSpace.Intersection(rect).Size == rect.Size) { - Sampler = hill2.TranslateTo(new XZ(1093, 1118)), - YFloor = 1, - }; + rect = new Rect(rect.start.Add(pushDir), rect.end.Add(pushDir)); + var o = overlap(rect); + if (o < best.Item1) + { + best = (o, rect); + } + } - stage.Mutate(mut); + return best; } public void OnImagesSelected(ImageChooserDialog.VM result) diff --git a/src/Blocktavius.Core/WIP.cs b/src/Blocktavius.Core/WIP.cs index 0802a85..3c4d38f 100644 --- a/src/Blocktavius.Core/WIP.cs +++ b/src/Blocktavius.Core/WIP.cs @@ -9,13 +9,20 @@ namespace Blocktavius.Core; public static class WIP { - public static I2DSampler Blah(PRNG prng) + public sealed record HillRequest { - const int maxElevation = 30; + public required int Elevation { get; init; } + public required Rect SeedSize { get; init; } + public decimal ExpansionRatio { get; init; } = 1.4m; - var plateauItem = new HillItem { Elevation = maxElevation, Kind = HillItemKind.Plateau }; + internal HillItem PlateauItem => new HillItem { Elevation = this.Elevation, Kind = HillItemKind.Plateau }; + } + + public static I2DSampler Blah(PRNG prng, HillRequest request) + { + int maxElevation = request.Elevation; - var area = BuildPlateau(prng, plateauItem); + var area = BuildPlateau(prng, request); AddChisel(area, maxElevation); var settings = new CornerPusherHill.Settings() @@ -26,7 +33,7 @@ public static I2DSampler Blah(PRNG prng) }; CornerPusherHill.BuildHill(settings, area, y => new HillItem { Elevation = y, Kind = HillItemKind.Cliff }); - return area.GetSampler(ExpansionId.MaxValue, plateauItem) + return area.GetSampler(ExpansionId.MaxValue, request.PlateauItem) .Project(t => t.Item1 ? t.Item2 : HillItem.Nothing); } @@ -46,18 +53,22 @@ public readonly record struct HillItem public static readonly HillItem Nothing = new() { Elevation = -1, Kind = HillItemKind.None }; } - private static ExpandableArea BuildPlateau(PRNG prng, HillItem fillValue) + private static ExpandableArea BuildPlateau(PRNG prng, HillRequest request) { + var fillValue = request.PlateauItem; + //var initArea = new Rect(XZ.Zero, XZ.Zero.Add(1, 1)).AsArea(); - var initArea = new Rect(XZ.Zero, XZ.Zero.Add(20, 50)).AsArea(); + //var initArea = new Rect(XZ.Zero, XZ.Zero.Add(20, 50)).AsArea(); + var initArea = request.SeedSize.AsArea(); var initShell = ShellLogic.ComputeShells(initArea).Single(); var area = new ExpandableArea(initShell); var shell = area.CurrentShell(); //while (shell.Count < 600) - var size = initArea.Bounds.start; - while (size.X + size.Z + Math.Min(size.X, size.Z) < 200) + int targetSize = Convert.ToInt32(request.ExpansionRatio * GetSize(request.SeedSize)); + var size = initArea.Bounds; + while (GetSize(size) < targetSize) { int i = prng.NextInt32(shell.Count); var item = shell[i]; @@ -66,13 +77,15 @@ private static ExpandableArea BuildPlateau(PRNG prng, HillItem fillVal area.Expand([(shell[i].XZ, fillValue)]); shell = area.CurrentShell(); // NOMERGE - this should be CurrentBounds or something: - size = area.GetSampler(ExpansionId.MaxValue, fillValue).Bounds.Size; + size = area.GetSampler(ExpansionId.MaxValue, fillValue).Bounds; } } return area; } + private static int GetSize(Rect rect) => rect.Size.X * rect.Size.Z; + private static void AddChisel(ExpandableArea area, int elevation) { var shell = area.CurrentShell(); diff --git a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs index 385fff4..9af3c80 100644 --- a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs +++ b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs @@ -61,3 +61,33 @@ internal override void Apply(IMutableStage stage) } } } + +public sealed class ClearEverythingMutation : StageMutation +{ + public int StartY { get; init; } = 1; + + internal override void Apply(IMutableStage stage) + { + int startY = this.StartY; + + foreach (var offset in stage.ChunksInUse) + { + if (!stage.TryGetChunk(offset, out var chunk)) + { + continue; + } + + foreach (var xz in chunk.Offset.Bounds.Enumerate()) + { + if (chunk.GetBlock(new Point(xz, 0)) == 0) // TODO this is unsound, I think + { + continue; + } + for (int y = StartY; y < DQB2Constants.MaxElevation; y++) + { + chunk.SetBlock(new Point(xz, y), 0); + } + } + } + } +} From eb77d3fd3a9ba490d38a7645274b75964636f2e1 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Wed, 4 Feb 2026 21:55:10 -0600 Subject: [PATCH 04/13] clean up CornerPusherHill --- .../Generators/Hills/CornerPusherHill.cs | 349 ++++++++---------- 1 file changed, 164 insertions(+), 185 deletions(-) diff --git a/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs b/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs index 003123f..daed59e 100644 --- a/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs +++ b/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Blocktavius.Core.Generators.Hills; @@ -38,6 +35,9 @@ public sealed record Settings public decimal MaxInitialMissPercent { get; init; } = 1.0m; } + /// + /// Builds a hill using integer elevations. + /// public static I2DSampler BuildHill(Settings settings, Shell shell) { if (shell.IsHole) @@ -46,232 +46,211 @@ public static I2DSampler BuildHill(Settings settings, Shell shell) } var area = new ExpandableArea(shell); - var algo = new IntAlgorithm(); - algo.BuildHill(settings, area); + BuildHill(settings, area, i => i); return area.GetSampler(ExpansionId.MaxValue, settings.MaxElevation) .Project(tuple => tuple.Item1 ? tuple.Item2 : -1); } + /// + /// Generic implementation for building a hill, converting integer elevations to type . + /// internal static void BuildHill(Settings settings, ExpandableArea area, Func converter) { - var algo = new GenericAlgorithm() { Converter = converter }; - algo.BuildHill(settings, area); - } + // This dictionary tracks items on the shell that are candidates for being "pushed" out. + var pendingExpansion = new Dictionary(); - abstract class Algorithm - { - public void BuildHill(Settings settings, ExpandableArea area) + // Initialize the first layer and its miss counts. + InitializeFirstLayer(settings, area.CurrentShell(), pendingExpansion); + + // Iteratively build each layer from the top down. + int numLayers = settings.MaxElevation - settings.MinElevation; + var ring = new ShellItemRing(area.CurrentShell()); + for (int i = 0; i < numLayers; i++) { - var layer = Layer.FirstLayer(area, settings, this); - BuildHill(settings, layer); + int currentElevation = settings.MaxElevation - i; + var layer = new Layer(ring, pendingExpansion, settings); + + // Calculate and apply the expansion for the current layer. + var expansion = layer.CalculateExpansion(); + if (expansion.Count > 0) + { + area.Expand(expansion.Select(e => (e.XZ, converter(e.Elevation)))); + foreach (var (xz, _) in expansion) + { + pendingExpansion.Remove(xz); + } + } + + // Update miss counts for the items that will carry over to the next layer. + ring = new ShellItemRing(area.CurrentShell()); + UpdateMisses(pendingExpansion, ring, currentElevation - 1); } - protected abstract IEnumerable<(XZ, T)> ConvertExpansion(IEnumerable<(XZ, int)> expansion); + // Twist ending? The "pending" expansion is actually expansion we already committed to. + // This was mostly so that the miss count dictionary and the shell items line up nicely, + // but may also be a consequence of preserving legacy behavior. + var finalExpansion = pendingExpansion.Select(kvp => (kvp.Key, converter(kvp.Value.Elevation))); + area.Expand(finalExpansion); + } - private void BuildHill(Settings settings, Layer firstLayer) - { - var layers = new Stack(); - layers.Push(firstLayer); + private static void InitializeFirstLayer(Settings settings, IReadOnlyList initialShellItems, Dictionary pendingExpansion) + { + UpdateMisses(pendingExpansion, new ShellItemRing(initialShellItems), settings.MaxElevation); + + // Overwrite legacy initial miss counts if percentage range is valid. + decimal minPercent = Math.Clamp(settings.MinInitialMissPercent, 0m, 1m); + decimal maxPercent = Math.Clamp(settings.MaxInitialMissPercent, 0m, 1m); + int minMisses = Convert.ToInt32(minPercent * settings.MaxConsecutiveMisses); + int maxMisses = Convert.ToInt32(maxPercent * settings.MaxConsecutiveMisses); - int needLayers = settings.MaxElevation - settings.MinElevation + 1; - while (layers.Count < needLayers) + bool reseed = (minMisses > 0 || maxMisses > 0) + && maxMisses >= minMisses + && settings.MinInitialMissPercent >= 0 + && settings.MaxInitialMissPercent >= 0; + + if (reseed) + { + var prng = settings.Prng.Clone(); + foreach (var kvp in pendingExpansion) { - layers.Push(layers.Peek().NextLayer(settings.Prng)); + kvp.Value.MissCount = prng.NextInt32(minMisses, maxMisses + 1); } - - var finalLayer = layers.First(); - var area = finalLayer.area; - var finalExpansion = finalLayer.pendingExpansion.Select(kvp => (kvp.Key, kvp.Value.Elevation)).ToList(); - area.Expand(ConvertExpansion(finalExpansion)); } + } - sealed class Layer + private static void UpdateMisses(Dictionary pendingItems, IEnumerable shellItems, int elevation) + { + // reduce inside corners weight from 3:1 down to 2:1 + foreach (var item in shellItems.Where(i => i.CornerType != CornerType.Inside)) { - public sealed class PendingShellItem + if (pendingItems.TryGetValue(item.XZ, out var pendingItem)) + { + pendingItem.MissCount++; + } + else { - public int MissCount { get; set; } // mutable - - /// - /// We retain the elevation this item was *introduced* at to maintain - /// compatability with a previous version of this algorithm. - /// (Also because I can't figure out why it gets more jagged when we break compatability.) - /// - public int Elevation { get; init; } + pendingItems[item.XZ] = new Layer.PendingShellItem { MissCount = 1, Elevation = elevation }; } + } + } + + /// + /// Represents the logic for calculating the expansion of a single elevation layer. + /// + private sealed class Layer + { + public sealed class PendingShellItem + { + public int MissCount { get; set; } // mutable /// - /// A "miss" occurs whenever we decide not to push a shell item out for the next layer. - /// This dictionary tracks consecutive miss counts; it resets to 0 when we push that XZ. - /// Permitting a larger number of consecutive misses creates a more vertical (steeper) hill. + /// We retain the elevation this item was *introduced* at to maintain + /// compatability with a previous version of this algorithm. + /// (Also because I can't figure out why it gets more jagged when we break compatability.) /// - public readonly Dictionary pendingExpansion; + public int Elevation { get; init; } + } - public readonly Algorithm algorithm; - public readonly ExpandableArea area; - public readonly ShellItemRing shellItems; - public readonly int elevation; - public readonly Settings settings; + // Constants for run length calculations + // Should convert these to Settings and experiment... + private const int MinPushRunLength = 2; + private const int MaxPushRunLength = 7; + private const int MinSkipRunLength = 2; + private const int MaxSkipRunLengthDivisor = 2; + private const int MaxSkipRunLengthCap = 50; - private static void UpdateMisses(Dictionary pendingItems, IEnumerable shellItems, int elevation) - { - // reduce inside corners weight from 3:1 down to 2:1 - foreach (var item in shellItems.Where(i => i.CornerType != CornerType.Inside)) - { - if (pendingItems.TryGetValue(item.XZ, out var pendingItem)) - { - pendingItem.MissCount++; - } - else - { - pendingItems[item.XZ] = new PendingShellItem { MissCount = 1, Elevation = elevation }; - } - } - } + private readonly ShellItemRing shellRing; + private readonly Dictionary pendingExpansion; + private readonly Settings settings; + private readonly PRNG prng; - private Layer(Layer prev) - { - this.algorithm = prev.algorithm; - this.area = prev.area; - this.pendingExpansion = prev.pendingExpansion; - this.shellItems = new ShellItemRing(area.CurrentShell()); - this.elevation = prev.elevation - 1; - this.settings = prev.settings; - - UpdateMisses(pendingExpansion, shellItems, elevation); - } + public Layer(ShellItemRing shell, Dictionary pendingExpansion, Settings settings) + { + this.shellRing = shell; + this.pendingExpansion = pendingExpansion; + this.settings = settings; + this.prng = settings.Prng; + } - private Layer(ExpandableArea area, Settings settings, Algorithm algorithm) + /// + /// Calculates the list of shell items to be pushed out for this layer. + /// + public List<(XZ XZ, int Elevation)> CalculateExpansion() + { + var expansion = new List<(XZ, int)>(); + if (shellRing.Count == 0) { - this.algorithm = algorithm; - this.area = area; - this.shellItems = new ShellItemRing(area.CurrentShell()); - this.pendingExpansion = new(); - this.elevation = settings.MaxElevation; - this.settings = settings; - - UpdateMisses(pendingExpansion, shellItems, elevation); + return expansion; } - public static Layer FirstLayer(ExpandableArea area, Settings settings, Algorithm algorithm) - { - var layer = new Layer(area, settings, algorithm); - - // Overwrite legacy initial miss counts if percentage range is valid. - decimal minPercent = Math.Clamp(settings.MinInitialMissPercent, 0m, 1m); - decimal maxPercent = Math.Clamp(settings.MaxInitialMissPercent, 0m, 1m); - int minMisses = Convert.ToInt32(minPercent * settings.MaxConsecutiveMisses); - int maxMisses = Convert.ToInt32(maxPercent * settings.MaxConsecutiveMisses); - bool reseed = (minMisses > 0 || maxMisses > 0) - && maxMisses >= minMisses - && settings.MinInitialMissPercent >= 0 - && settings.MaxInitialMissPercent >= 0; - - if (reseed) - { - var prng = settings.Prng.Clone(); - foreach (var kvp in layer.pendingExpansion) - { - kvp.Value.MissCount = prng.NextInt32(minMisses, maxMisses + 1); - } - } - - return layer; - } + // Starting from a random point on the shell, do one lap, alternating between + // pushing and skipping runs of shell items. + int startIndex = prng.NextInt32(shellRing.Count); + int endIndex = startIndex + shellRing.Count; + int currentIndex = startIndex; + bool push = false; - public Layer NextLayer(PRNG prng) + do { - var prevLayer = this.shellItems; - - int maxConsecutiveMisses = settings.MaxConsecutiveMisses; - - List<(XZ, int)> expansion = new(); - - // Starting from a random point on the shell, do one lap. - // While one lap not completed: - // * choose random run length, subject to constraints - // * do or don't push out those shell items for next layer - // * flip the "do or don't push" flag for the next run - // - // To implement the "start from a random point on the shell" we - // * choose a random start index - // * loop from startIndex to startIndex + Count - // * use modulo during list indexing since our loopingIndex will (very likely) go past the end of the list - int startIndex = prng.NextInt32(prevLayer.Count); - int endIndex = startIndex + prevLayer.Count; - int loopingIndex = startIndex; - bool push = false; - do - { - push = !push; - if (push) - { + push = !push; + int runLength; - const int minRunLength = 2; - const int maxRunLength = 7; - - int mustTakeNow = prevLayer.OneLapFrom(loopingIndex) - .TakeWhile(i => pendingExpansion[i.XZ].MissCount >= maxConsecutiveMisses) - .Take(maxRunLength) - .Count(); - - int runLength = prng.NextInt32(Math.Max(mustTakeNow, minRunLength), 1 + Math.Max(mustTakeNow, maxRunLength)); - for (int i = 0; i < runLength; i++) - { - var prevItem = prevLayer[loopingIndex % prevLayer.Count]; - loopingIndex++; - int elev = pendingExpansion[prevItem.XZ].Elevation; - expansion.Add((prevItem.XZ, elev)); - } - } - else + if (push) + { + runLength = CalculatePushRunLength(currentIndex); + for (int i = 0; i < runLength; i++) { - const int minRunLength = 2; - //const int maxRunLength = 50; - int maxRunLength = Math.Min(prevLayer.Count / 2, 50); - - int mustStopAt = prevLayer.OneLapFrom(loopingIndex) - .TakeWhile(i => pendingExpansion[i.XZ].MissCount < maxConsecutiveMisses) - .Take(maxRunLength) - .Count(); - - int runLength; - if (mustStopAt < minRunLength) - { - runLength = mustStopAt; - } - else - { - runLength = prng.NextInt32(minRunLength, 1 + Math.Min(mustStopAt, maxRunLength)); - } - loopingIndex += runLength; + var item = shellRing[currentIndex % shellRing.Count]; + currentIndex++; + int elev = pendingExpansion[item.XZ].Elevation; + expansion.Add((item.XZ, elev)); } } - while (loopingIndex < endIndex); - - foreach (var item in expansion) + else { - pendingExpansion.Remove(item.Item1); + runLength = CalculateSkipRunLength(currentIndex); + currentIndex += runLength; } - area.Expand(algorithm.ConvertExpansion(expansion)); - - return new Layer(this); } + while (currentIndex < endIndex); + + return expansion; } - } - class IntAlgorithm : Algorithm - { - protected override IEnumerable<(XZ, int)> ConvertExpansion(IEnumerable<(XZ, int)> expansion) => expansion; - } + private int CalculatePushRunLength(int currentIndex) + { + int maxConsecutiveMisses = settings.MaxConsecutiveMisses; - class GenericAlgorithm : Algorithm - { - public required Func Converter { get; init; } + // Determine how many items *must* be pushed because they have exceeded their miss count. + int mustTakeNow = shellRing.OneLapFrom(currentIndex) + .TakeWhile(i => pendingExpansion[i.XZ].MissCount >= maxConsecutiveMisses) + .Take(MaxPushRunLength) + .Count(); - protected override IEnumerable<(XZ, T)> ConvertExpansion(IEnumerable<(XZ, int)> expansion) + int minRun = Math.Max(mustTakeNow, MinPushRunLength); + int maxRun = Math.Max(mustTakeNow, MaxPushRunLength); + + return prng.NextInt32(minRun, 1 + maxRun); + } + + private int CalculateSkipRunLength(int currentIndex) { - return expansion.Select(a => (a.Item1, Converter(a.Item2))); + int maxConsecutiveMisses = settings.MaxConsecutiveMisses; + int maxRunLength = Math.Min(shellRing.Count / MaxSkipRunLengthDivisor, MaxSkipRunLengthCap); + + // Determine how many items we can skip before hitting one that *must* be pushed. + int mustStopAt = shellRing.OneLapFrom(currentIndex) + .TakeWhile(i => pendingExpansion[i.XZ].MissCount < maxConsecutiveMisses) + .Take(maxRunLength) + .Count(); + + if (mustStopAt < MinSkipRunLength) + { + return mustStopAt; + } + + return prng.NextInt32(MinSkipRunLength, 1 + Math.Min(mustStopAt, maxRunLength)); } } } From 1025326c2b5796543504434db3161f40c7ccfa25 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Wed, 4 Feb 2026 21:58:56 -0600 Subject: [PATCH 05/13] remove unnecessary modulo (that's what a Ring is for) --- src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs b/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs index daed59e..d32ae5f 100644 --- a/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs +++ b/src/Blocktavius.Core/Generators/Hills/CornerPusherHill.cs @@ -201,7 +201,7 @@ public Layer(ShellItemRing shell, Dictionary pendingExpans runLength = CalculatePushRunLength(currentIndex); for (int i = 0; i < runLength; i++) { - var item = shellRing[currentIndex % shellRing.Count]; + var item = shellRing[currentIndex]; currentIndex++; int elev = pendingExpansion[item.XZ].Elevation; expansion.Add((item.XZ, elev)); From 675301be26eb2cc7298532393de390df3e9c4495 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 5 Feb 2026 12:07:41 -0600 Subject: [PATCH 06/13] refactor --- src/Blocktavius.AppDQB2/ProjectVM.cs | 164 +------------------- src/Blocktavius.AppDQB2/TERRAGEN.cs | 215 +++++++++++++++++++++++++++ 2 files changed, 216 insertions(+), 163 deletions(-) create mode 100644 src/Blocktavius.AppDQB2/TERRAGEN.cs diff --git a/src/Blocktavius.AppDQB2/ProjectVM.cs b/src/Blocktavius.AppDQB2/ProjectVM.cs index b2840cf..b03b4e7 100644 --- a/src/Blocktavius.AppDQB2/ProjectVM.cs +++ b/src/Blocktavius.AppDQB2/ProjectVM.cs @@ -305,7 +305,7 @@ private void ExpandChunks(IReadOnlySet expansion) if (this.SelectedScript == Scripts.FirstOrDefault()) // NOMERGE!! { - DropTheHammer(workingStage); + TERRAGEN.DropTheHammer(workingStage); return workingStage; } @@ -319,168 +319,6 @@ private void ExpandChunks(IReadOnlySet expansion) return workingStage; } - private static void DropTheHammer(IMutableStage stage) - { - /* -(define (chisel->mask chisel) - (case chisel - [(none) 0] - ; * 1/3/5/7 - diagonal chisel N/E/S/W, matches (blueprint.chisel_status << 4) - ; * 2/4/6/8 - diagonal chisel SW/SE/NW/NE - ; * 9/a/b/c - concave chisel NW/SW/SE/NE - [(flat-lo) #xe000] - [(flat-hi) #xf000])) - */ - - var availableSpace = new Rect(new XZ(900, 900), new XZ(1200, 1200)); - - const int groundMinY = 13; - stage.Mutate(new ClearEverythingMutation() { StartY = groundMinY }); - - var prng = PRNG.Deserialize("1-2-3-67-67-67"); - - stage.Mutate(MakeGround(availableSpace, groundMinY)); - - var arranged = Arrange(HillRequests(), availableSpace, prng); - - foreach (var (position, request) in arranged) - { - var hill = WIP.Blah(prng.AdvanceAndClone(), request); - var hill2 = hill.Project(item => - { - ushort blockId; - if (item.Kind == WIP.HillItemKind.Plateau) - { - blockId = 4; - } - else if (item.Kind == WIP.HillItemKind.Chisel) - { - blockId = 21 | 0xe000; - } - else if (item.Kind == WIP.HillItemKind.Cliff) - { - blockId = 21; - } - else - { - blockId = 0; - return (-1, blockId); - } - return (item.Elevation, blockId); - }); - - var mut = new PutHillMutation2() - { - Sampler = hill2.TranslateTo(position.start), - YFloor = 1, - }; - - stage.Mutate(mut); - } - } - - private static PutHillMutation MakeGround(Rect rect, int y) - { - var sampler = new ConstantSampler { Bounds = rect, Value = y }; - return new PutHillMutation() - { - Block = 3, - Sampler = sampler, - YFloor = 1, - }; - } - - sealed record HillSpec - { - public required int Elevation { get; init; } - public required int Size { get; init; } - } - - private static IEnumerable HillRequests() - { - // random rotation should be applied here, or at least before Arrange() - yield return new WIP.HillRequest { Elevation = 50, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(120, 80)) }; - yield return new WIP.HillRequest { Elevation = 48, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(80, 120)) }; - yield return new WIP.HillRequest { Elevation = 40, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(60, 45)) }; - yield return new WIP.HillRequest { Elevation = 32, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(80, 150)) }; - yield return new WIP.HillRequest { Elevation = 30, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 80)) }; - yield return new WIP.HillRequest { Elevation = 26, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(100, 100)) }; - yield return new WIP.HillRequest { Elevation = 24, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(70, 140)) }; - yield return new WIP.HillRequest { Elevation = 20, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; - yield return new WIP.HillRequest { Elevation = 18, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; - } - - private static List<(Rect translatedPosition, WIP.HillRequest request)> Arrange(IEnumerable requests, Rect fullSpace, PRNG prng) - { - var positions = requests.Select(request => (position: request.SeedSize, request)).ToList(); - var best = (positions, overlap: int.MaxValue); // lower values are better, zero is perfect - - for (int attempt = 0; attempt < 5; attempt++) - { - int totalOverlap = 0; - - for (int i = 0; i < positions.Count; i++) - { - var item = positions[i]; - var range = new Rect(fullSpace.start, fullSpace.end.Subtract(item.request.SeedSize.Size)); - var x = prng.NextInt32(range.start.X, range.end.X); - var z = prng.NextInt32(range.start.Z, range.end.Z); - var start = new XZ(x, z); - var pos = new Rect(start, start.Add(item.request.SeedSize.Size)); - - var overlappers = positions.Take(i).Select(x => x.position).ToList(); - - var current = (overlap: int.MaxValue, pos); - foreach (var dir in Direction.CardinalDirections()) - { - if (current.overlap == 0) { break; } - var pushed = BestPush(pos, overlappers, dir.Step, fullSpace); - if (pushed.Item1 < current.overlap) - { - current = pushed; - } - } - - positions[i] = (current.pos, item.request); - totalOverlap += current.overlap; - } - - if (totalOverlap < best.overlap) - { - best = (positions.ToList(), totalOverlap); - } - } - - return best.positions; - } - - private static (int, Rect) BestPush(Rect rect, IReadOnlyList overlappers, XZ pushDir, Rect fullSpace) - { - int overlap(Rect rect) - { - int o = 0; - foreach (var other in overlappers) - { - var i = other.Intersection(rect); - o += i.Size.X * i.Size.Z; - } - return o; - } - - var best = (overlap(rect), rect); - while (best.Item1 > 0 && fullSpace.Intersection(rect).Size == rect.Size) - { - rect = new Rect(rect.start.Add(pushDir), rect.end.Add(pushDir)); - var o = overlap(rect); - if (o < best.Item1) - { - best = (o, rect); - } - } - - return best; - } - public void OnImagesSelected(ImageChooserDialog.VM result) { var chunkGridLayer = Layers.LastOrDefault() as ChunkGridLayer; diff --git a/src/Blocktavius.AppDQB2/TERRAGEN.cs b/src/Blocktavius.AppDQB2/TERRAGEN.cs new file mode 100644 index 0000000..982c6e4 --- /dev/null +++ b/src/Blocktavius.AppDQB2/TERRAGEN.cs @@ -0,0 +1,215 @@ +using Blocktavius.Core; +using Blocktavius.DQB2; +using Blocktavius.DQB2.Mutations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blocktavius.AppDQB2; + +static class TERRAGEN +{ + public static void DropTheHammer(IMutableStage stage) + { + /* +(define (chisel->mask chisel) + (case chisel + [(none) 0] + ; * 1/3/5/7 - diagonal chisel N/E/S/W, matches (blueprint.chisel_status << 4) + ; * 2/4/6/8 - diagonal chisel SW/SE/NW/NE + ; * 9/a/b/c - concave chisel NW/SW/SE/NE + [(flat-lo) #xe000] + [(flat-hi) #xf000])) + */ + + var availableSpace = new Rect(new XZ(900, 900), new XZ(1200, 1200)); + + const int groundMinY = 13; + stage.Mutate(new ClearEverythingMutation() { StartY = groundMinY }); + + var prng = PRNG.Deserialize("1-2-3-67-67-67"); + + stage.Mutate(MakeGround(availableSpace, groundMinY)); + + List components = new(); + + components.AddRange(HillRequests().Select(r => new HillComponent { HillRequest = r })); + + var arranged = Arrange(components, availableSpace, prng); + + foreach (var (position, component) in arranged) + { + var context = new TerraformMutationContext + { + ArrangedPosition = position, + PRNG = prng, + Stage = stage, + }; + component.Mutate(context); + } + } + + private static PutHillMutation MakeGround(Rect rect, int y) + { + var sampler = new ConstantSampler { Bounds = rect, Value = y }; + return new PutHillMutation() + { + Block = 3, + Sampler = sampler, + YFloor = 1, + }; + } + + sealed record HillSpec + { + public required int Elevation { get; init; } + public required int Size { get; init; } + } + + interface IArrangable + { + Rect SeedSize { get; } + } + + sealed record TerraformMutationContext + { + public required IMutableStage Stage { get; init; } + public required PRNG PRNG { get; init; } + public required Rect ArrangedPosition { get; init; } + } + + interface ITerraformComponent : IArrangable + { + void Mutate(TerraformMutationContext context); + } + + class HillComponent : ITerraformComponent + { + public required WIP.HillRequest HillRequest { get; init; } + + public Rect SeedSize => HillRequest.SeedSize; + + public void Mutate(TerraformMutationContext context) + { + var hill = WIP.Blah(context.PRNG.AdvanceAndClone(), this.HillRequest); + var hill2 = hill.Project(item => + { + ushort blockId; + if (item.Kind == WIP.HillItemKind.Plateau) + { + blockId = 4; + } + else if (item.Kind == WIP.HillItemKind.Chisel) + { + blockId = 21 | 0xe000; + } + else if (item.Kind == WIP.HillItemKind.Cliff) + { + blockId = 21; + } + else + { + blockId = 0; + return (-1, blockId); + } + return (item.Elevation, blockId); + }); + + var mut = new PutHillMutation2() + { + Sampler = hill2.TranslateTo(context.ArrangedPosition.start), + YFloor = 1, + }; + + context.Stage.Mutate(mut); + } + } + + private static IEnumerable HillRequests() + { + // random rotation should be applied here, or at least before Arrange() + yield return new WIP.HillRequest { Elevation = 50, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(120, 80)) }; + yield return new WIP.HillRequest { Elevation = 48, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(80, 120)) }; + yield return new WIP.HillRequest { Elevation = 40, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(60, 45)) }; + yield return new WIP.HillRequest { Elevation = 32, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(80, 150)) }; + yield return new WIP.HillRequest { Elevation = 30, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 80)) }; + yield return new WIP.HillRequest { Elevation = 26, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(100, 100)) }; + yield return new WIP.HillRequest { Elevation = 24, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(70, 140)) }; + yield return new WIP.HillRequest { Elevation = 20, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; + yield return new WIP.HillRequest { Elevation = 18, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; + } + + private static List<(Rect translatedPosition, TComponent request)> Arrange(IEnumerable requests, Rect fullSpace, PRNG prng) + where TComponent : IArrangable + { + var positions = requests.Select(request => (position: request.SeedSize, request)).ToList(); + var best = (positions, overlap: int.MaxValue); // lower values are better, zero is perfect + + for (int attempt = 0; attempt < 5; attempt++) + { + int totalOverlap = 0; + + for (int i = 0; i < positions.Count; i++) + { + var item = positions[i]; + var range = new Rect(fullSpace.start, fullSpace.end.Subtract(item.request.SeedSize.Size)); + var x = prng.NextInt32(range.start.X, range.end.X); + var z = prng.NextInt32(range.start.Z, range.end.Z); + var start = new XZ(x, z); + var pos = new Rect(start, start.Add(item.request.SeedSize.Size)); + + var overlappers = positions.Take(i).Select(x => x.position).ToList(); + + var current = (overlap: int.MaxValue, pos); + foreach (var dir in Direction.CardinalDirections()) + { + if (current.overlap == 0) { break; } + var pushed = BestPush(pos, overlappers, dir.Step, fullSpace); + if (pushed.Item1 < current.overlap) + { + current = pushed; + } + } + + positions[i] = (current.pos, item.request); + totalOverlap += current.overlap; + } + + if (totalOverlap < best.overlap) + { + best = (positions.ToList(), totalOverlap); + } + } + + return best.positions; + } + + private static (int, Rect) BestPush(Rect rect, IReadOnlyList overlappers, XZ pushDir, Rect fullSpace) + { + int overlap(Rect rect) + { + int o = 0; + foreach (var other in overlappers) + { + var i = other.Intersection(rect); + o += i.Size.X * i.Size.Z; + } + return o; + } + + var best = (overlap(rect), rect); + while (best.Item1 > 0 && fullSpace.Intersection(rect).Size == rect.Size) + { + rect = new Rect(rect.start.Add(pushDir), rect.end.Add(pushDir)); + var o = overlap(rect); + if (o < best.Item1) + { + best = (o, rect); + } + } + + return best; + } +} From fce44456eae0235a6811ed78550719306d712041 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 5 Feb 2026 12:42:20 -0600 Subject: [PATCH 07/13] Lakes! --- src/Blocktavius.AppDQB2/ProjectVM.cs | 1 - src/Blocktavius.AppDQB2/TERRAGEN.cs | 50 +++++++++++++++++-- .../Mutations/PutHillMutation.cs | 45 +++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/src/Blocktavius.AppDQB2/ProjectVM.cs b/src/Blocktavius.AppDQB2/ProjectVM.cs index b03b4e7..78a4f19 100644 --- a/src/Blocktavius.AppDQB2/ProjectVM.cs +++ b/src/Blocktavius.AppDQB2/ProjectVM.cs @@ -3,7 +3,6 @@ using Blocktavius.AppDQB2.Services; using Blocktavius.Core; using Blocktavius.DQB2; -using Blocktavius.DQB2.Mutations; using GongSolutions.Wpf.DragDrop; using System; using System.Collections.Generic; diff --git a/src/Blocktavius.AppDQB2/TERRAGEN.cs b/src/Blocktavius.AppDQB2/TERRAGEN.cs index 982c6e4..df597f0 100644 --- a/src/Blocktavius.AppDQB2/TERRAGEN.cs +++ b/src/Blocktavius.AppDQB2/TERRAGEN.cs @@ -34,8 +34,8 @@ public static void DropTheHammer(IMutableStage stage) stage.Mutate(MakeGround(availableSpace, groundMinY)); List components = new(); - - components.AddRange(HillRequests().Select(r => new HillComponent { HillRequest = r })); + components.Add(new LakeComponent { HillRequest = HillRequests().First() }); + components.AddRange(HillRequests().Skip(1).Select(r => new HillComponent { HillRequest = r })); var arranged = Arrange(components, availableSpace, prng); @@ -91,7 +91,7 @@ class HillComponent : ITerraformComponent public Rect SeedSize => HillRequest.SeedSize; - public void Mutate(TerraformMutationContext context) + public virtual void Mutate(TerraformMutationContext context) { var hill = WIP.Blah(context.PRNG.AdvanceAndClone(), this.HillRequest); var hill2 = hill.Project(item => @@ -127,9 +127,51 @@ public void Mutate(TerraformMutationContext context) } } + class LakeComponent : HillComponent + { + public override void Mutate(TerraformMutationContext context) + { + base.Mutate(context); + + int plateauElevation = HillRequest.Elevation; + const int depth = 8; + + var center = context.ArrangedPosition.start.Add(context.ArrangedPosition.Size.Unscale(new XZ(2, 2))); + var lakeRequest = new WIP.HillRequest + { + Elevation = plateauElevation, + SeedSize = new Rect(center.Add(-3, -3), center.Add(3, 3)), + ExpansionRatio = 3m, + }; + + var sampler = WIP.Blah(context.PRNG, lakeRequest); + var lakebed = sampler.Project(item => + { + if (item.Elevation < 1) { return -1; } + int elevation = plateauElevation - depth + (plateauElevation - item.Elevation); + if (elevation >= plateauElevation) { return -1; } + return elevation; + }); + + var mut = new PutLakeMutation() + { + LakebedElevation = lakebed, + Liquid = LiquidFamily.ClearWater, + TopLayerAmount = LiquidAmountIndex.SurfaceLow, + TopLayerY = plateauElevation, + }; + + context.Stage.Mutate(mut); + } + } + private static IEnumerable HillRequests() { // random rotation should be applied here, or at least before Arrange() + + // First one will be a lake: + yield return new WIP.HillRequest { Elevation = 18, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; + yield return new WIP.HillRequest { Elevation = 50, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(120, 80)) }; yield return new WIP.HillRequest { Elevation = 48, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(80, 120)) }; yield return new WIP.HillRequest { Elevation = 40, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(60, 45)) }; @@ -138,7 +180,7 @@ public void Mutate(TerraformMutationContext context) yield return new WIP.HillRequest { Elevation = 26, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(100, 100)) }; yield return new WIP.HillRequest { Elevation = 24, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(70, 140)) }; yield return new WIP.HillRequest { Elevation = 20, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; - yield return new WIP.HillRequest { Elevation = 18, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; + //yield return new WIP.HillRequest { Elevation = 18, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; } private static List<(Rect translatedPosition, TComponent request)> Arrange(IEnumerable requests, Rect fullSpace, PRNG prng) diff --git a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs index 9af3c80..144da0f 100644 --- a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs +++ b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs @@ -91,3 +91,48 @@ internal override void Apply(IMutableStage stage) } } } + +public sealed class PutLakeMutation : StageMutation +{ + public required I2DSampler LakebedElevation { get; init; } + public required LiquidFamily Liquid { get; init; } + public required LiquidAmountIndex TopLayerAmount { get; init; } + public required int TopLayerY { get; init; } + + internal override void Apply(IMutableStage stage) + { + ushort subsurfaceId = Liquid.BlockIdSubsurface; + ushort topLayerId = TopLayerAmount switch + { + LiquidAmountIndex.Subsurface => subsurfaceId, + LiquidAmountIndex.SurfaceHigh => Liquid.BlockIdSurfaceHigh, + _ => Liquid.BlockIdSurfaceLow, + }; + + // TESTING: + //subsurfaceId = 0; + //topLayerId = 0; + + foreach (var offset in stage.ChunksInUse) + { + if (!stage.TryGetChunk(offset, out var chunk)) + { + continue; + } + + var intersection = LakebedElevation.Bounds.Intersection(offset.Bounds); + foreach (var xz in intersection.Enumerate()) + { + int lakebed = LakebedElevation.Sample(xz); + if (lakebed > 0 && lakebed < TopLayerY) + { + chunk.SetBlock(new Point(xz, TopLayerY), topLayerId); + for (int y = TopLayerY - 1; y > lakebed; y--) + { + chunk.SetBlock(new Point(xz, y), subsurfaceId); + } + } + } + } + } +} From 47e30aa8c4519323645666130c28addfbf131b22 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 5 Feb 2026 13:04:24 -0600 Subject: [PATCH 08/13] PatchHoles --- src/Blocktavius.Core/WIP.cs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/Blocktavius.Core/WIP.cs b/src/Blocktavius.Core/WIP.cs index 3c4d38f..6d4de04 100644 --- a/src/Blocktavius.Core/WIP.cs +++ b/src/Blocktavius.Core/WIP.cs @@ -14,6 +14,7 @@ public sealed record HillRequest public required int Elevation { get; init; } public required Rect SeedSize { get; init; } public decimal ExpansionRatio { get; init; } = 1.4m; + public int Steepness { get; init; } = 11; // default steepness from CornerPusher internal HillItem PlateauItem => new HillItem { Elevation = this.Elevation, Kind = HillItemKind.Plateau }; } @@ -24,12 +25,14 @@ public static I2DSampler Blah(PRNG prng, HillRequest request) var area = BuildPlateau(prng, request); AddChisel(area, maxElevation); + PatchHoles(area, request); var settings = new CornerPusherHill.Settings() { Prng = prng.Clone(), MinElevation = 1, MaxElevation = maxElevation - 1, + MaxConsecutiveMisses = request.Steepness, }; CornerPusherHill.BuildHill(settings, area, y => new HillItem { Elevation = y, Kind = HillItemKind.Cliff }); @@ -92,4 +95,38 @@ private static void AddChisel(ExpandableArea area, int elevation) var value = new HillItem { Elevation = elevation, Kind = HillItemKind.Chisel }; area.Expand(shell.Select(item => (item.XZ, value))); } + + private static void PatchHoles(ExpandableArea area, HillRequest request) + { + HashSet holes = new(); + + var sampler = area.GetSampler(ExpansionId.MaxValue, request.PlateauItem); + foreach (var xz in sampler.Bounds.Enumerate()) + { + // At this point, any XZ that is empty and next to a plateau item must be a hole, + // because we have surrounded the plateau with the chisel layer. + bool isEmpty = !sampler.Sample(xz).Item1; + bool hasPlateauNeighbor = xz.CardinalNeighbors() + .Any(neighbor => sampler.Sample(neighbor).Item2.Kind == HillItemKind.Plateau); + + if (isEmpty && hasPlateauNeighbor) + { + // flood fill + var queue = new Queue(); + queue.Enqueue(xz); + while (queue.TryDequeue(out var floodXZ) && holes.Add(floodXZ)) + { + foreach (var neighbor in floodXZ.CardinalNeighbors()) + { + if (!sampler.Sample(neighbor).Item1) + { + queue.Enqueue(neighbor); + } + } + } + } + } + + area.Expand(holes.Select(xz => (xz, request.PlateauItem))); + } } From 12ebe063c57f303067f4aae13019e7a7a6d98b1c Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 5 Feb 2026 16:10:48 -0600 Subject: [PATCH 09/13] center correctly --- src/Blocktavius.AppDQB2/TERRAGEN.cs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Blocktavius.AppDQB2/TERRAGEN.cs b/src/Blocktavius.AppDQB2/TERRAGEN.cs index df597f0..5f33fdd 100644 --- a/src/Blocktavius.AppDQB2/TERRAGEN.cs +++ b/src/Blocktavius.AppDQB2/TERRAGEN.cs @@ -85,6 +85,18 @@ interface ITerraformComponent : IArrangable void Mutate(TerraformMutationContext context); } + static I2DSampler TranslateToCenter(this I2DSampler sampler, Rect target) + { + var center = target.start.Add(target.Size.Unscale(new XZ(2, 2))); + return TranslateToCenter(sampler, center); + } + + static I2DSampler TranslateToCenter(I2DSampler sampler, XZ target) + { + var corner = target.Add(sampler.Bounds.Size.Unscale(new XZ(-2, -2))); + return sampler.TranslateTo(corner); + } + class HillComponent : ITerraformComponent { public required WIP.HillRequest HillRequest { get; init; } @@ -117,9 +129,11 @@ public virtual void Mutate(TerraformMutationContext context) return (item.Elevation, blockId); }); + hill2 = hill2.TranslateToCenter(context.ArrangedPosition); + var mut = new PutHillMutation2() { - Sampler = hill2.TranslateTo(context.ArrangedPosition.start), + Sampler = hill2, YFloor = 1, }; @@ -136,15 +150,17 @@ public override void Mutate(TerraformMutationContext context) int plateauElevation = HillRequest.Elevation; const int depth = 8; - var center = context.ArrangedPosition.start.Add(context.ArrangedPosition.Size.Unscale(new XZ(2, 2))); var lakeRequest = new WIP.HillRequest { Elevation = plateauElevation, - SeedSize = new Rect(center.Add(-3, -3), center.Add(3, 3)), - ExpansionRatio = 3m, + SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(6, 6)), + ExpansionRatio = 8m, + Steepness = 5, }; var sampler = WIP.Blah(context.PRNG, lakeRequest); + sampler = sampler.TranslateToCenter(context.ArrangedPosition); + var lakebed = sampler.Project(item => { if (item.Elevation < 1) { return -1; } @@ -172,6 +188,7 @@ public override void Mutate(TerraformMutationContext context) // First one will be a lake: yield return new WIP.HillRequest { Elevation = 18, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; + ///* TEMP SPEEDUP yield return new WIP.HillRequest { Elevation = 50, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(120, 80)) }; yield return new WIP.HillRequest { Elevation = 48, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(80, 120)) }; yield return new WIP.HillRequest { Elevation = 40, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(60, 45)) }; @@ -181,6 +198,7 @@ public override void Mutate(TerraformMutationContext context) yield return new WIP.HillRequest { Elevation = 24, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(70, 140)) }; yield return new WIP.HillRequest { Elevation = 20, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; //yield return new WIP.HillRequest { Elevation = 18, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; + //*/ } private static List<(Rect translatedPosition, TComponent request)> Arrange(IEnumerable requests, Rect fullSpace, PRNG prng) From 4f15488c3e2c3e94dbdeed7dfbf590b5ef743803 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 5 Feb 2026 16:55:26 -0600 Subject: [PATCH 10/13] looking quite nice --- src/Blocktavius.AppDQB2/TERRAGEN.cs | 54 ++++++++++++++++--- .../Mutations/PutHillMutation.cs | 16 +++++- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/Blocktavius.AppDQB2/TERRAGEN.cs b/src/Blocktavius.AppDQB2/TERRAGEN.cs index 5f33fdd..5024520 100644 --- a/src/Blocktavius.AppDQB2/TERRAGEN.cs +++ b/src/Blocktavius.AppDQB2/TERRAGEN.cs @@ -11,6 +11,33 @@ namespace Blocktavius.AppDQB2; static class TERRAGEN { + private static Rect InferAvailableSpace(IStage stage) + { + var offsets = stage.ChunksInUse.ToList(); + + // drop dock offsets (trailing horizontal rows having count < 3) + while (offsets.Count > 0) + { + var last = offsets.Last(); + if (offsets.Where(o => o.OffsetZ == last.OffsetZ).Count() < 3) + { + offsets.RemoveAt(offsets.Count - 1); + } + else + { + break; + } + } + + var totalBounds = new Rect.BoundsFinder(); + foreach (var offset in offsets) + { + totalBounds.Include(offset.RawUnscaledOffset); + } + var bounds = totalBounds.CurrentBounds() ?? Rect.Zero; + return new Rect(bounds.start.Scale(32), bounds.end.Scale(32)); + } + public static void DropTheHammer(IMutableStage stage) { /* @@ -24,10 +51,10 @@ public static void DropTheHammer(IMutableStage stage) [(flat-hi) #xf000])) */ - var availableSpace = new Rect(new XZ(900, 900), new XZ(1200, 1200)); + var availableSpace = InferAvailableSpace(stage); - const int groundMinY = 13; - stage.Mutate(new ClearEverythingMutation() { StartY = groundMinY }); + const int groundMinY = 7; + stage.Mutate(new ClearEverythingMutation() { StartY = groundMinY, Where = availableSpace }); var prng = PRNG.Deserialize("1-2-3-67-67-67"); @@ -49,6 +76,12 @@ public static void DropTheHammer(IMutableStage stage) }; component.Mutate(context); } + + stage.Mutate(new RepairSeaMutation() + { + ColumnCleanupMode = ColumnCleanupMode.ExpandBedrock, + SeaLevel = 11, + }); } private static PutHillMutation MakeGround(Rect rect, int y) @@ -56,9 +89,10 @@ private static PutHillMutation MakeGround(Rect rect, int y) var sampler = new ConstantSampler { Bounds = rect, Value = y }; return new PutHillMutation() { - Block = 3, + Block = 146, // seaside sand Sampler = sampler, YFloor = 1, + RespectExistingBedrock = true, }; } @@ -105,6 +139,12 @@ class HillComponent : ITerraformComponent public virtual void Mutate(TerraformMutationContext context) { + // chalk=8, chunky chalk=9, clodstone=21 (or 115?) + // dolomite light=130, dark=131, chert=149 (or 174?), chunky chert=153 + // umber=209, lumpy umber=241 + // umber sandstone! = 210 + const ushort wallBlockId = 241; + var hill = WIP.Blah(context.PRNG.AdvanceAndClone(), this.HillRequest); var hill2 = hill.Project(item => { @@ -115,11 +155,11 @@ public virtual void Mutate(TerraformMutationContext context) } else if (item.Kind == WIP.HillItemKind.Chisel) { - blockId = 21 | 0xe000; + blockId = wallBlockId | 0xe000; } else if (item.Kind == WIP.HillItemKind.Cliff) { - blockId = 21; + blockId = wallBlockId; } else { @@ -188,7 +228,7 @@ public override void Mutate(TerraformMutationContext context) // First one will be a lake: yield return new WIP.HillRequest { Elevation = 18, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(150, 150)) }; - ///* TEMP SPEEDUP + //* TEMP SPEEDUP yield return new WIP.HillRequest { Elevation = 50, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(120, 80)) }; yield return new WIP.HillRequest { Elevation = 48, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(80, 120)) }; yield return new WIP.HillRequest { Elevation = 40, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(60, 45)) }; diff --git a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs index 144da0f..f48c7f3 100644 --- a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs +++ b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs @@ -12,10 +12,12 @@ public sealed class PutHillMutation : StageMutation public required I2DSampler Sampler { get; init; } public required ushort Block { get; init; } public int? YFloor { get; init; } = null; + public bool RespectExistingBedrock { get; init; } = false; internal override void Apply(IMutableStage stage) { int yFloor = this.YFloor ?? 1; + bool checkBedrock = RespectExistingBedrock; foreach (var chunk in Enumerate(Sampler.Bounds, stage)) { @@ -24,6 +26,10 @@ internal override void Apply(IMutableStage stage) var elevation = Sampler.Sample(xz); if (elevation > 0) { + if (checkBedrock && chunk.GetBlock(new Point(xz, 0)) == 0) + { + continue; + } for (int y = yFloor; y <= elevation; y++) { chunk.SetBlock(new Point(xz, y), Block); @@ -65,6 +71,7 @@ internal override void Apply(IMutableStage stage) public sealed class ClearEverythingMutation : StageMutation { public int StartY { get; init; } = 1; + public Rect? Where { get; init; } = null; internal override void Apply(IMutableStage stage) { @@ -77,7 +84,9 @@ internal override void Apply(IMutableStage stage) continue; } - foreach (var xz in chunk.Offset.Bounds.Enumerate()) + var where = this.Where ?? offset.Bounds; + + foreach (var xz in chunk.Offset.Bounds.Intersection(where).Enumerate()) { if (chunk.GetBlock(new Point(xz, 0)) == 0) // TODO this is unsound, I think { @@ -131,6 +140,11 @@ internal override void Apply(IMutableStage stage) { chunk.SetBlock(new Point(xz, y), subsurfaceId); } + + for (int y = lakebed; y > 0; y--) + { + chunk.SetBlock(new Point(xz, y), 21); + } } } } From 8d4245de7f0bbad17186ba415002a78d3efc2d8e Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 5 Feb 2026 20:05:51 -0600 Subject: [PATCH 11/13] Liquid Roof (minimap update trick) --- src/Blocktavius.AppDQB2/TERRAGEN.cs | 4 + src/Blocktavius.Core/Rect.cs | 9 +- .../LiquidRoof/LiquidRoofPlan.cs | 143 ++++++++++++++++++ 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/Blocktavius.DQB2/LiquidRoof/LiquidRoofPlan.cs diff --git a/src/Blocktavius.AppDQB2/TERRAGEN.cs b/src/Blocktavius.AppDQB2/TERRAGEN.cs index 5024520..f56dff5 100644 --- a/src/Blocktavius.AppDQB2/TERRAGEN.cs +++ b/src/Blocktavius.AppDQB2/TERRAGEN.cs @@ -1,5 +1,6 @@ using Blocktavius.Core; using Blocktavius.DQB2; +using Blocktavius.DQB2.LiquidRoof; using Blocktavius.DQB2.Mutations; using System; using System.Collections.Generic; @@ -82,6 +83,9 @@ public static void DropTheHammer(IMutableStage stage) ColumnCleanupMode = ColumnCleanupMode.ExpandBedrock, SeaLevel = 11, }); + + var minimapUpdater = LiquidRoofPlan.Create(stage); + stage.Mutate(minimapUpdater.GetMutation()); } private static PutHillMutation MakeGround(Rect rect, int y) diff --git a/src/Blocktavius.Core/Rect.cs b/src/Blocktavius.Core/Rect.cs index 203f43d..cd3d134 100644 --- a/src/Blocktavius.Core/Rect.cs +++ b/src/Blocktavius.Core/Rect.cs @@ -159,12 +159,19 @@ public sealed class BoundsFinder private int xMax = int.MinValue; private int zMax = int.MinValue; - public void Include(XZ xz) + public BoundsFinder Include(XZ xz) { xMin = Math.Min(xMin, xz.X); zMin = Math.Min(zMin, xz.Z); xMax = Math.Max(xMax, xz.X); zMax = Math.Max(zMax, xz.Z); + return this; + } + + public BoundsFinder IncludeAll(IEnumerable xzs) + { + foreach (var xz in xzs) { Include(xz); } + return this; } public Rect? CurrentBounds() diff --git a/src/Blocktavius.DQB2/LiquidRoof/LiquidRoofPlan.cs b/src/Blocktavius.DQB2/LiquidRoof/LiquidRoofPlan.cs new file mode 100644 index 0000000..b88d0e9 --- /dev/null +++ b/src/Blocktavius.DQB2/LiquidRoof/LiquidRoofPlan.cs @@ -0,0 +1,143 @@ +using Blocktavius.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blocktavius.DQB2.LiquidRoof; + +/// +/// Creates a liquid roof; touching it with a block in-game will update the minimap. +/// NOMERGE - Write a snapshot test before merging this. +/// +/// +/// Separated into a "plan" and "apply" phases. +/// The plan is created by , which locks in the elevation +/// to be used for each roof segment. +/// Applying the mutation (returned by ) will only +/// overwrite Points which are empty. +/// This allows you to place more blocks between the plan and apply which may +/// pierce through the roof, which could be good for sparse tall things like trees. +/// +public sealed class LiquidRoofPlan +{ + private readonly IReadOnlyList entries; + + private LiquidRoofPlan(IReadOnlyList entries) + { + this.entries = entries; + } + + sealed record Entry + { + public required IReadOnlyList Offsets { get; init; } + public required I2DSampler Area { get; init; } + public required int Elevation { get; init; } + } + + public StageMutation GetMutation() => new PutRoofMutation { Plan = this }; + + public static LiquidRoofPlan Create(IStage stage) + { + const int entryDimension = 3; // 3x3 chunks per entry + const int yBoost = 2; // how far off the (maximum) ground should we put the roof? + + var entries = new List(); + + var offsetRect = new Rect.BoundsFinder() + .IncludeAll(stage.ChunksInUse.Select(o => o.RawUnscaledOffset)) + .CurrentBounds() ?? Rect.Zero; + + for (int jumpZ = offsetRect.start.Z; jumpZ <= offsetRect.end.Z; jumpZ += entryDimension) + { + for (int jumpX = offsetRect.start.X; jumpX <= offsetRect.end.X; jumpX += entryDimension) + { + // create an Entry + int elevation = -1; + List offsets = new(); + for (int z = jumpZ; z < jumpZ + entryDimension; z++) + { + for (int x = jumpX; x < jumpX + entryDimension; x++) + { + var offset = new ChunkOffset(x, z); + if (stage.TryReadChunk(offset, out var chunk)) + { + offsets.Add(offset); + elevation = Math.Max(elevation, GetMaxElevation(chunk)); + } + } + } + if (offsets.Count > 0) + { + var areaRect = new Rect.BoundsFinder() + .IncludeAll(offsets.Select(o => o.Bounds.start)) + .IncludeAll(offsets.Select(o => o.Bounds.end)) + .CurrentBounds() ?? Rect.Zero; + + // create a 1-voxel buffer between regions + areaRect = areaRect.Expand(-1); + + // make sure we don't exceed max elevation + elevation = Math.Min(elevation + yBoost, DQB2Constants.MaxElevation - 1); + + entries.Add(new Entry + { + Area = areaRect.AsArea().AsSampler(), + Elevation = elevation, + Offsets = offsets, + }); + } + } + } + + return new LiquidRoofPlan(entries); + } + + private static int GetMaxElevation(IChunk chunk) + { + int max = 0; + foreach (var xz in chunk.Offset.Bounds.Enumerate()) + { + for (int y = DQB2Constants.MaxElevation - 1; y > 0; y--) + { + if (chunk.GetBlock(new Point(xz, y)) != 0) + { + max = Math.Max(max, y); + break; + } + } + } + return max; + } + + class PutRoofMutation : StageMutation + { + public required LiquidRoofPlan Plan { get; init; } + + internal override void Apply(IMutableStage stage) + { + ushort block = 183; // poison runoff, very visible + + foreach (var entry in Plan.entries) + { + var area = entry.Area; + + foreach (var offset in entry.Offsets) + { + if (stage.TryGetChunk(offset, out var chunk)) + { + foreach (var xz in offset.Bounds.Enumerate().Where(area.InArea)) + { + var p = new Point(xz, entry.Elevation); + if (chunk.GetBlock(p) == 0 && chunk.GetBlock(new Point(xz, 0)) != 0) + { + chunk.SetBlock(p, block); + } + } + } + } + } + } + } +} From fb3cde7f83693a8cb7e53a9ec127b261416dcb16 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Fri, 6 Feb 2026 15:34:36 -0600 Subject: [PATCH 12/13] clean up Liquid Roof --- src/Blocktavius.Core/Rect.cs | 12 ++ src/Blocktavius.DQB2/DQB2Constants.cs | 15 +- .../LiquidRoof/LiquidRoofOptions.cs | 43 +++++ .../LiquidRoof/LiquidRoofPlan.cs | 155 +++++++++++------- 4 files changed, 163 insertions(+), 62 deletions(-) create mode 100644 src/Blocktavius.DQB2/LiquidRoof/LiquidRoofOptions.cs diff --git a/src/Blocktavius.Core/Rect.cs b/src/Blocktavius.Core/Rect.cs index cd3d134..fd08bd1 100644 --- a/src/Blocktavius.Core/Rect.cs +++ b/src/Blocktavius.Core/Rect.cs @@ -117,6 +117,16 @@ public IEnumerable Enumerate() } } + public IEnumerable EnumerateFiltered(I2DSampler filter) + { + return Enumerate().Where(filter.InArea); + } + + public IEnumerable Enumerate(I2DSampler? filter) + { + return filter == null ? Enumerate() : EnumerateFiltered(filter); + } + public static Rect GetBounds(IEnumerable xzs) { int xMin = int.MaxValue; @@ -152,6 +162,8 @@ public Rect Intersection(Rect other) return new Rect(new XZ(x0, z0), new XZ(x1, z1)); } + public bool Intersects(Rect other) => !Intersection(other).IsZero; + public sealed class BoundsFinder { private int xMin = int.MaxValue; diff --git a/src/Blocktavius.DQB2/DQB2Constants.cs b/src/Blocktavius.DQB2/DQB2Constants.cs index 0cfbe4f..ac2b823 100644 --- a/src/Blocktavius.DQB2/DQB2Constants.cs +++ b/src/Blocktavius.DQB2/DQB2Constants.cs @@ -1,4 +1,5 @@ -using System; +using Blocktavius.Core; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -27,4 +28,16 @@ public static class BlockId internal static Block ToBlock(this ushort block) => Block.Lookup(block); internal static LiquidFamilyIndex GetLiquidFamilyIndex(this ushort blockId) => blockId.ToBlock().LiquidFamilyIndex; + + /// + /// Returns true if the chunk has a nonzero/nonempty block at Y=0 for the given . + /// + /// + /// The nonzero block is probably bedrock, but any nonzero block is sufficient. + /// + internal static bool HasFoundationAt(this IChunk chunk, XZ xz) + { + var blockId = chunk.GetBlock(new Point(xz, 0)); + return Block.MakeCanonical(blockId) != 0; + } } diff --git a/src/Blocktavius.DQB2/LiquidRoof/LiquidRoofOptions.cs b/src/Blocktavius.DQB2/LiquidRoof/LiquidRoofOptions.cs new file mode 100644 index 0000000..8417a10 --- /dev/null +++ b/src/Blocktavius.DQB2/LiquidRoof/LiquidRoofOptions.cs @@ -0,0 +1,43 @@ +using Blocktavius.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blocktavius.DQB2.LiquidRoof; + +public sealed record LiquidRoofOptions +{ + /// + /// The roof will be divided into segments of NxN chunks; + /// this property specifies that N. + /// The primary reason for segmentation is because the amount of in-game + /// lag increases with larger segments. At extreme sizes, the segment + /// might not even be fully removed when you touch it. + /// + public int SegmentSizeInChunks { get; init; } = 3; + + /// + /// In each segment, the roof will be placed this many voxels + /// above the max elevation found in that segment. + /// + public int YBoost { get; init; } = 2; + + /// + /// Block ID to use for the roof. + /// Should be a "runoff" liquid ID which disappears when touched. + /// Defaults to Poison, a good choice due to its high visibility. + /// + /// + /// You could choose a non-liquid roof, but I can't imagine why. + /// + public ushort RoofBlockId { get; init; } = 183; + + /// + /// When not null, the roof may not exceed this area. + /// + public I2DSampler? FilterArea { get; init; } = null; + + public static LiquidRoofOptions Default { get; } = new(); +} diff --git a/src/Blocktavius.DQB2/LiquidRoof/LiquidRoofPlan.cs b/src/Blocktavius.DQB2/LiquidRoof/LiquidRoofPlan.cs index b88d0e9..9b193f0 100644 --- a/src/Blocktavius.DQB2/LiquidRoof/LiquidRoofPlan.cs +++ b/src/Blocktavius.DQB2/LiquidRoof/LiquidRoofPlan.cs @@ -8,107 +8,139 @@ namespace Blocktavius.DQB2.LiquidRoof; /// -/// Creates a liquid roof; touching it with a block in-game will update the minimap. +/// A clever minimap update trick, thanks to Sapphire. +/// Creates a liquid roof; touching it with a block in-game will cause the liquid to +/// be removed and the minimap to be updated. /// NOMERGE - Write a snapshot test before merging this. /// /// /// Separated into a "plan" and "apply" phases. -/// The plan is created by , which locks in the elevation +/// The plan is created by , which locks in the elevation /// to be used for each roof segment. /// Applying the mutation (returned by ) will only /// overwrite Points which are empty. -/// This allows you to place more blocks between the plan and apply which may -/// pierce through the roof, which could be good for sparse tall things like trees. +/// This allows you to place more blocks between the plan and apply which may pierce +/// through the roof, which could be desirable for sparse and tall things like trees. /// public sealed class LiquidRoofPlan { - private readonly IReadOnlyList entries; + private readonly IReadOnlyList segments; + private readonly LiquidRoofOptions options; - private LiquidRoofPlan(IReadOnlyList entries) + private LiquidRoofPlan(IReadOnlyList segments, LiquidRoofOptions options) { - this.entries = entries; + this.segments = segments; + this.options = options; } - sealed record Entry + sealed record RoofSegment { public required IReadOnlyList Offsets { get; init; } + + /// + /// Used to implement the buffer that separates segments. + /// public required I2DSampler Area { get; init; } + public required int Elevation { get; init; } } public StageMutation GetMutation() => new PutRoofMutation { Plan = this }; - public static LiquidRoofPlan Create(IStage stage) + public static LiquidRoofPlan Create(IStage stage) => Create(stage, LiquidRoofOptions.Default); + + public static LiquidRoofPlan Create(IStage stage, LiquidRoofOptions options) { - const int entryDimension = 3; // 3x3 chunks per entry - const int yBoost = 2; // how far off the (maximum) ground should we put the roof? + if (options.SegmentSizeInChunks < 1) + { + options = options with { SegmentSizeInChunks = LiquidRoofOptions.Default.SegmentSizeInChunks }; + } + + int segmentDimension = options.SegmentSizeInChunks; - var entries = new List(); + var segments = new List(); + var relevantChunks = stage.ChunksInUse + .Where(o => options.FilterArea == null || o.Bounds.Intersects(options.FilterArea.Bounds)) + .ToList(); + + // Caution: using unscaled XZs here var offsetRect = new Rect.BoundsFinder() - .IncludeAll(stage.ChunksInUse.Select(o => o.RawUnscaledOffset)) + .IncludeAll(relevantChunks.Select(o => o.RawUnscaledOffset)) .CurrentBounds() ?? Rect.Zero; - for (int jumpZ = offsetRect.start.Z; jumpZ <= offsetRect.end.Z; jumpZ += entryDimension) + for (int segStartZ = offsetRect.start.Z; segStartZ <= offsetRect.end.Z; segStartZ += segmentDimension) { - for (int jumpX = offsetRect.start.X; jumpX <= offsetRect.end.X; jumpX += entryDimension) + for (int segStartX = offsetRect.start.X; segStartX <= offsetRect.end.X; segStartX += segmentDimension) { - // create an Entry - int elevation = -1; - List offsets = new(); - for (int z = jumpZ; z < jumpZ + entryDimension; z++) + var segmentStart = new XZ(segStartX, segStartZ); + var segmentRect = new Rect(segmentStart, segmentStart.Add(segmentDimension, segmentDimension)); + if (TryCreateSegment(segmentRect, options, stage, out var segment)) { - for (int x = jumpX; x < jumpX + entryDimension; x++) - { - var offset = new ChunkOffset(x, z); - if (stage.TryReadChunk(offset, out var chunk)) - { - offsets.Add(offset); - elevation = Math.Max(elevation, GetMaxElevation(chunk)); - } - } + segments.Add(segment); } - if (offsets.Count > 0) - { - var areaRect = new Rect.BoundsFinder() - .IncludeAll(offsets.Select(o => o.Bounds.start)) - .IncludeAll(offsets.Select(o => o.Bounds.end)) - .CurrentBounds() ?? Rect.Zero; - - // create a 1-voxel buffer between regions - areaRect = areaRect.Expand(-1); + } + } - // make sure we don't exceed max elevation - elevation = Math.Min(elevation + yBoost, DQB2Constants.MaxElevation - 1); + return new LiquidRoofPlan(segments, options); + } - entries.Add(new Entry - { - Area = areaRect.AsArea().AsSampler(), - Elevation = elevation, - Offsets = offsets, - }); - } + private static bool TryCreateSegment(Rect offsetRect, LiquidRoofOptions options, IStage stage, out RoofSegment segment) + { + int elevation = -1; + List offsets = new(); + foreach (var xz in offsetRect.Enumerate()) + { + var offset = new ChunkOffset(xz.X, xz.Z); + if (stage.TryReadChunk(offset, out var chunk)) + { + offsets.Add(offset); + elevation = Math.Max(elevation, GetMaxElevation(chunk, options.FilterArea)); } } - return new LiquidRoofPlan(entries); + if (offsets.Count == 0) + { + segment = default!; + return false; + } + + var segmentArea = new Rect.BoundsFinder() + .IncludeAll(offsets.Select(o => o.Bounds.start)) + .IncludeAll(offsets.Select(o => o.Bounds.end)) + .CurrentBounds() ?? Rect.Zero; + + // Create a buffer between segments, otherwise segments with the same + // elevation would not be segmented from each other. + segmentArea = segmentArea.Expand(-1); + + // Don't exceed max elevation + elevation = Math.Min(elevation + options.YBoost, DQB2Constants.MaxElevation - 1); + + segment = new RoofSegment + { + Area = segmentArea.AsArea().AsSampler(), + Elevation = elevation, + Offsets = offsets, + }; + return true; } - private static int GetMaxElevation(IChunk chunk) + private static int GetMaxElevation(IChunk chunk, I2DSampler? filterArea) { - int max = 0; - foreach (var xz in chunk.Offset.Bounds.Enumerate()) + int maxElevation = 0; + foreach (var xz in chunk.Offset.Bounds.Enumerate(filterArea)) { for (int y = DQB2Constants.MaxElevation - 1; y > 0; y--) { - if (chunk.GetBlock(new Point(xz, y)) != 0) + if (!chunk.GetBlock(new Point(xz, y)).IsEmptyBlock()) { - max = Math.Max(max, y); + maxElevation = Math.Max(maxElevation, y); break; } } } - return max; + return maxElevation; } class PutRoofMutation : StageMutation @@ -117,22 +149,23 @@ class PutRoofMutation : StageMutation internal override void Apply(IMutableStage stage) { - ushort block = 183; // poison runoff, very visible + ushort block = Plan.options.RoofBlockId; + var filterArea = Plan.options.FilterArea; - foreach (var entry in Plan.entries) + foreach (var segment in Plan.segments) { - var area = entry.Area; + var segmentArea = segment.Area; - foreach (var offset in entry.Offsets) + foreach (var offset in segment.Offsets) { if (stage.TryGetChunk(offset, out var chunk)) { - foreach (var xz in offset.Bounds.Enumerate().Where(area.InArea)) + foreach (var xz in offset.Bounds.Enumerate(filterArea).Where(segmentArea.InArea)) { - var p = new Point(xz, entry.Elevation); - if (chunk.GetBlock(p) == 0 && chunk.GetBlock(new Point(xz, 0)) != 0) + var point = new Point(xz, segment.Elevation); + if (chunk.GetBlock(point).IsEmptyBlock() && chunk.HasFoundationAt(xz)) { - chunk.SetBlock(p, block); + chunk.SetBlock(point, block); } } } From 8e8748485df46d0d2f21a585739b821e9c7f74c7 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Fri, 6 Feb 2026 23:22:13 -0600 Subject: [PATCH 13/13] Trees! (And a particularly great random seed; I love this layout) --- src/Blocktavius.AppDQB2/MainWindow.xaml.cs | 2 +- .../PlanScriptDialog.xaml.cs | 2 +- src/Blocktavius.AppDQB2/ProjectVM.cs | 4 +- src/Blocktavius.AppDQB2/TERRAGEN.cs | 185 +++++++++++++++--- src/Blocktavius.Core/Direction.cs | 28 ++- src/Blocktavius.Core/ExpandableArea.cs | 2 + src/Blocktavius.Core/PRNG.cs | 2 + src/Blocktavius.Core/WIP.cs | 5 +- src/Blocktavius.DQB2/Block.cs | 15 ++ src/Blocktavius.DQB2/Chisel.cs | 39 ++++ src/Blocktavius.DQB2/DQB2Constants.cs | 16 ++ .../Mutations/PutHillMutation.cs | 103 ++++++++++ 12 files changed, 373 insertions(+), 30 deletions(-) create mode 100644 src/Blocktavius.DQB2/Chisel.cs diff --git a/src/Blocktavius.AppDQB2/MainWindow.xaml.cs b/src/Blocktavius.AppDQB2/MainWindow.xaml.cs index 3500fbc..db62d58 100644 --- a/src/Blocktavius.AppDQB2/MainWindow.xaml.cs +++ b/src/Blocktavius.AppDQB2/MainWindow.xaml.cs @@ -30,7 +30,7 @@ internal async void DoPreview() var vm = (this.DataContext as MainWindowVM)?.CurrentContent as ProjectVM; if (EyeOfRubissDriver != null && vm != null) { - var scriptedStage = await vm.TryRebuildStage(); + var scriptedStage = await vm.TryRebuildStage(preview: true); if (scriptedStage != null) { await EyeOfRubissDriver.WriteStageAsync(scriptedStage); diff --git a/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs b/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs index c4c08a1..a253f36 100644 --- a/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs +++ b/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs @@ -171,7 +171,7 @@ public PlanScriptVM(ProjectVM project) ScriptName = project.SelectedScript.GetScriptName() ?? ""; Title = $"Plan and Run -- {ScriptName}"; - rebuildTask = project.TryRebuildStage(); + rebuildTask = project.TryRebuildStage(preview: false); if (project.BackupsEnabled(out var backupDir)) { diff --git a/src/Blocktavius.AppDQB2/ProjectVM.cs b/src/Blocktavius.AppDQB2/ProjectVM.cs index 78a4f19..71ee8db 100644 --- a/src/Blocktavius.AppDQB2/ProjectVM.cs +++ b/src/Blocktavius.AppDQB2/ProjectVM.cs @@ -294,7 +294,7 @@ private void ExpandChunks(IReadOnlySet expansion) return stage; } - public async Task TryRebuildStage() + public async Task TryRebuildStage(bool preview) { var workingStage = await TryLoadMutableStage(expandChunks: true); if (workingStage == null) @@ -304,7 +304,7 @@ private void ExpandChunks(IReadOnlySet expansion) if (this.SelectedScript == Scripts.FirstOrDefault()) // NOMERGE!! { - TERRAGEN.DropTheHammer(workingStage); + TERRAGEN.DropTheHammer(workingStage, preview); return workingStage; } diff --git a/src/Blocktavius.AppDQB2/TERRAGEN.cs b/src/Blocktavius.AppDQB2/TERRAGEN.cs index f56dff5..c8863f2 100644 --- a/src/Blocktavius.AppDQB2/TERRAGEN.cs +++ b/src/Blocktavius.AppDQB2/TERRAGEN.cs @@ -12,7 +12,7 @@ namespace Blocktavius.AppDQB2; static class TERRAGEN { - private static Rect InferAvailableSpace(IStage stage) + private static (Rect, IReadOnlyList) InferAvailableSpace(IStage stage) { var offsets = stage.ChunksInUse.ToList(); @@ -36,37 +36,53 @@ private static Rect InferAvailableSpace(IStage stage) totalBounds.Include(offset.RawUnscaledOffset); } var bounds = totalBounds.CurrentBounds() ?? Rect.Zero; - return new Rect(bounds.start.Scale(32), bounds.end.Scale(32)); + return (new Rect(bounds.start.Scale(32), bounds.end.Scale(32)), offsets); } - public static void DropTheHammer(IMutableStage stage) + /// + /// Surrounds the given with extra chunks + /// + private static IReadOnlySet ExpandChunks(IEnumerable offsets) { - /* -(define (chisel->mask chisel) - (case chisel - [(none) 0] - ; * 1/3/5/7 - diagonal chisel N/E/S/W, matches (blueprint.chisel_status << 4) - ; * 2/4/6/8 - diagonal chisel SW/SE/NW/NE - ; * 9/a/b/c - concave chisel NW/SW/SE/NE - [(flat-lo) #xe000] - [(flat-hi) #xf000])) - */ - - var availableSpace = InferAvailableSpace(stage); + var expansion = new HashSet(); + foreach (var offset in offsets) + { + foreach (var dir in Direction.CardinalDirections().Concat(Direction.OrdinalDirections())) + { + var neighbor = offset.RawUnscaledOffset.Step(dir); + expansion.Add(new ChunkOffset(neighbor.X, neighbor.Z)); + } + } + return expansion; + } + + public static void DropTheHammer(IMutableStage stage, bool preview) + { + var (availableSpace, nonDockChunks) = InferAvailableSpace(stage); + stage.ExpandChunks(ExpandChunks(nonDockChunks)); const int groundMinY = 7; stage.Mutate(new ClearEverythingMutation() { StartY = groundMinY, Where = availableSpace }); - var prng = PRNG.Deserialize("1-2-3-67-67-67"); + var prng = PRNG.Deserialize("1-2-5-67-67-67"); stage.Mutate(MakeGround(availableSpace, groundMinY)); List components = new(); - components.Add(new LakeComponent { HillRequest = HillRequests().First() }); - components.AddRange(HillRequests().Skip(1).Select(r => new HillComponent { HillRequest = r })); + components.Add(new LakeComponent + { + HillRequest = HillRequests().First(), + Enforest = false + }); + components.AddRange(HillRequests().Skip(1).Index().Select(i => new HillComponent + { + HillRequest = i.Item, + Enforest = i.Index == 0, + })); var arranged = Arrange(components, availableSpace, prng); + arranged.Reverse(); // reorder: lowest elevation to highest foreach (var (position, component) in arranged) { var context = new TerraformMutationContext @@ -84,8 +100,15 @@ public static void DropTheHammer(IMutableStage stage) SeaLevel = 11, }); - var minimapUpdater = LiquidRoofPlan.Create(stage); - stage.Mutate(minimapUpdater.GetMutation()); + if (!preview) + { + var roofOptions = new LiquidRoofOptions + { + FilterArea = availableSpace.AsArea().AsSampler(), + }; + var minimapUpdater = LiquidRoofPlan.Create(stage, roofOptions); + stage.Mutate(minimapUpdater.GetMutation()); + } } private static PutHillMutation MakeGround(Rect rect, int y) @@ -141,6 +164,16 @@ class HillComponent : ITerraformComponent public Rect SeedSize => HillRequest.SeedSize; + public required bool Enforest { get; init; } + + protected virtual IEnumerable> GetForestAreas(PRNG prng, I2DSampler origPlateau) + { + if (Enforest) + { + yield return origPlateau; + } + } + public virtual void Mutate(TerraformMutationContext context) { // chalk=8, chunky chalk=9, clodstone=21 (or 115?) @@ -149,7 +182,9 @@ public virtual void Mutate(TerraformMutationContext context) // umber sandstone! = 210 const ushort wallBlockId = 241; - var hill = WIP.Blah(context.PRNG.AdvanceAndClone(), this.HillRequest); + var prng = context.PRNG.AdvanceAndClone(); + + var hill = WIP.Blah(prng, this.HillRequest, out var plateauArea); var hill2 = hill.Project(item => { ushort blockId; @@ -173,7 +208,9 @@ public virtual void Mutate(TerraformMutationContext context) return (item.Elevation, blockId); }); + var origStart = hill2.Bounds.start; hill2 = hill2.TranslateToCenter(context.ArrangedPosition); + var translateAmount = hill2.Bounds.start.Subtract(origStart); var mut = new PutHillMutation2() { @@ -182,11 +219,113 @@ public virtual void Mutate(TerraformMutationContext context) }; context.Stage.Mutate(mut); + + var forestAreas = GetForestAreas(prng, plateauArea).ToList(); + foreach (var forestArea in forestAreas) + { + AddForest(forestArea.Translate(translateAmount), context.Stage, prng); + } + } + + private void AddForest(I2DSampler area, IMutableStage stage, PRNG prng) + { + MutableArray2D hasTree = new MutableArray2D(area.Bounds, false); + List centers = new(); // centers of trees + double skipChance = 0.15; // chance of skipping the tree, while reserving its space and counting it as success + int skippedTreeCount = 0; // count of skipped trees + + int xStart = area.Bounds.start.X; + int xEnd = area.Bounds.end.X; + int zStart = area.Bounds.start.Z; + int zEnd = area.Bounds.end.Z; + + int targetCount = (area.Bounds.Size.X * area.Bounds.Size.Z) / 85; + int consecutiveFailures = 0; + while (centers.Count + skippedTreeCount < targetCount && consecutiveFailures < targetCount * 2) + { + var x = prng.NextInt32(xStart, xEnd); + var z = prng.NextInt32(zStart, zEnd); + var center = new XZ(x, z); + + var locs = TreeLocs(center); + if (!locs.All(area.InArea)) + { + continue; + } + + var paddedLocs = locs.Concat(PadLocs(center)); + if (paddedLocs.Any(hasTree.InArea)) + { + consecutiveFailures++; + continue; + } + + // reserve space no matter what... + consecutiveFailures = 0; + foreach (var xz in locs) + { + hasTree.Put(xz, true); + } + // ... but maybe skip this tree + if (prng.NextDouble() > skipChance) + { + centers.Add(center); + } + else + { + skippedTreeCount++; + } + } + + foreach (var center in centers) + { + stage.Mutate(new PutTreeMutation() + { + Center = center, + Elevation = this.HillRequest.Elevation + 1, + Prng = prng, + }); + } + } + + private static IEnumerable TreeLocs(XZ center) + { + yield return center; + foreach (var dir in Direction.CardinalDirections()) + { + for (int i = 1; i <= 2; i++) + { + yield return center.Step(dir, i); + } + } + + foreach (var dir in Direction.OrdinalDirections()) + { + yield return center.Step(dir); + } + } + + private static IEnumerable PadLocs(XZ center) + { + foreach (var dir in Direction.CardinalDirections()) + { + yield return center.Step(dir, 3); + yield return center.Step(dir, 3).Step(dir.TurnLeft90, 1); + yield return center.Step(dir, 2).Step(dir.TurnRight90, 1); + yield return center.Step(dir, 2).Step(dir.TurnRight90, 2); + yield return center.Step(dir, 1).Step(dir.TurnRight90, 2); + yield return center.Step(dir, 1).Step(dir.TurnRight90, 3); + } } } class LakeComponent : HillComponent { + protected override IEnumerable> GetForestAreas(PRNG prng, I2DSampler origPlateau) + { + return []; + } + public override void Mutate(TerraformMutationContext context) { base.Mutate(context); @@ -198,11 +337,11 @@ public override void Mutate(TerraformMutationContext context) { Elevation = plateauElevation, SeedSize = new Rect(XZ.Zero, XZ.Zero.Add(6, 6)), - ExpansionRatio = 8m, + ExpansionRatio = 20m, Steepness = 5, }; - var sampler = WIP.Blah(context.PRNG, lakeRequest); + var sampler = WIP.Blah(context.PRNG, lakeRequest, out _); sampler = sampler.TranslateToCenter(context.ArrangedPosition); var lakebed = sampler.Project(item => diff --git a/src/Blocktavius.Core/Direction.cs b/src/Blocktavius.Core/Direction.cs index b602477..aff71d5 100644 --- a/src/Blocktavius.Core/Direction.cs +++ b/src/Blocktavius.Core/Direction.cs @@ -25,8 +25,24 @@ public enum OrdinalDirection public sealed class Direction { + private const int indexAdjust = -1; + + public static class IndexConstants + { + public const int North = (int)CardinalDirection.North + indexAdjust; + public const int East = (int)CardinalDirection.East + indexAdjust; + public const int South = (int)CardinalDirection.South + indexAdjust; + public const int West = (int)CardinalDirection.West + indexAdjust; + + public const int NorthEast = (int)OrdinalDirection.NorthEast + indexAdjust; + public const int SouthEast = (int)OrdinalDirection.SouthEast + indexAdjust; + public const int SouthWest = (int)OrdinalDirection.SouthWest + indexAdjust; + public const int NorthWest = (int)OrdinalDirection.NorthWest + indexAdjust; + } + private readonly int index; private readonly string name; + public int Index => index; private Direction(CardinalDirection cardinal) { @@ -51,8 +67,8 @@ private Direction(OrdinalDirection ordinal) const int count = 8; private static readonly IReadOnlyList lookup; - private static int GetIndex(CardinalDirection direction) => (int)direction - 1; - private static int GetIndex(OrdinalDirection direction) => (int)direction - 1; + public static int GetIndex(CardinalDirection direction) => (int)direction + indexAdjust; + public static int GetIndex(OrdinalDirection direction) => (int)direction + indexAdjust; static Direction() { @@ -88,6 +104,14 @@ public static IEnumerable CardinalDirections() yield return West; } + public static IEnumerable OrdinalDirections() + { + yield return NorthEast; + yield return SouthEast; + yield return SouthWest; + yield return NorthWest; + } + private static Direction ParseAny(int index, T direction) { if (index < 0 || index >= count) diff --git a/src/Blocktavius.Core/ExpandableArea.cs b/src/Blocktavius.Core/ExpandableArea.cs index a186fe1..237bd74 100644 --- a/src/Blocktavius.Core/ExpandableArea.cs +++ b/src/Blocktavius.Core/ExpandableArea.cs @@ -51,6 +51,8 @@ public ExpandableArea(Shell shell) public IReadOnlyList CurrentShell() => currentShell; + public ExpansionId CurrentExpansionId() => currentExpansionId; + /// /// The given must not be part of the original area or any /// previous expansion. diff --git a/src/Blocktavius.Core/PRNG.cs b/src/Blocktavius.Core/PRNG.cs index 5f050d6..1a9b2f7 100644 --- a/src/Blocktavius.Core/PRNG.cs +++ b/src/Blocktavius.Core/PRNG.cs @@ -167,6 +167,8 @@ public int NextInt32(int start, int end) return start + NextInt32(end - start); } + public bool NextBool() => NextInt32(2) == 0; + public T RandomChoice(params T[] items) { int index = NextInt32(items.Length); diff --git a/src/Blocktavius.Core/WIP.cs b/src/Blocktavius.Core/WIP.cs index 6d4de04..77dd195 100644 --- a/src/Blocktavius.Core/WIP.cs +++ b/src/Blocktavius.Core/WIP.cs @@ -19,11 +19,14 @@ public sealed record HillRequest internal HillItem PlateauItem => new HillItem { Elevation = this.Elevation, Kind = HillItemKind.Plateau }; } - public static I2DSampler Blah(PRNG prng, HillRequest request) + public static I2DSampler Blah(PRNG prng, HillRequest request, out I2DSampler PlateauArea) { int maxElevation = request.Elevation; var area = BuildPlateau(prng, request); + + PlateauArea = area.GetArea(area.CurrentExpansionId()); + AddChisel(area, maxElevation); PatchHoles(area, request); diff --git a/src/Blocktavius.DQB2/Block.cs b/src/Blocktavius.DQB2/Block.cs index 494654e..b39c998 100644 --- a/src/Blocktavius.DQB2/Block.cs +++ b/src/Blocktavius.DQB2/Block.cs @@ -122,6 +122,7 @@ enum PackedBlockInfo : int { } const int Shift_LiquidFamily = 20; const int Shift_Immersion = 24; + const int Mask_Chisel = 0xF000; internal const int Mask_CanonicalBlockId = 0x7FF; internal const int CanonicalBlockCount = 0x800; @@ -133,6 +134,11 @@ enum PackedBlockInfo : int { } private readonly int val; + private Block(int val) + { + this.val = val; + } + private Block(ushort blockId, PackedBlockInfo blockInfo) { this.val = blockId | (int)blockInfo; @@ -165,6 +171,15 @@ public bool IsProp(out Prop prop) public bool IsEmptyBlock => BlockIdCanonical == 0; + public Chisel Chisel => (Chisel)(this.val & Mask_Chisel); + + public Block SetChisel(Chisel chisel) + { + int val = this.val & ~Mask_Chisel; + val |= (int)chisel & Mask_Chisel; + return new Block(val); + } + /// /// DOES NOT VALIDATE THE ARGS. /// Returns the canonical block ID for the given shell+liquid+immersion. diff --git a/src/Blocktavius.DQB2/Chisel.cs b/src/Blocktavius.DQB2/Chisel.cs new file mode 100644 index 0000000..cae4205 --- /dev/null +++ b/src/Blocktavius.DQB2/Chisel.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blocktavius.DQB2; + +/// +/// Diagonal and Concave values are in reference to the direction the builder +/// must be facing to apply that chisel in-game. +/// For example, DiagonalEast means "the builder uses the Diagonal chisel while +/// facing East" and a ball placed on the resulting slope would roll West. +/// +public enum Chisel : ushort +{ + None = 0, + + DiagonalNorth = 0x1000, + DiagonalEast = 0x7000, + DiagonalSouth = 0x5000, + DiagonalWest = 0x3000, + + DiagonalSouthWest = 0x4000, + DiagonalSouthEast = 0x6000, + DiagonalNorthWest = 0x2000, + DiagonalNorthEast = 0x8000, + + // UNVERIFIED AND PROBABLY WRONG: + /* + ConcaveNorthwest = 0x9000, + ConcaveSouthwest = 0xa000, + ConcaveSoutheast = 0xb000, + ConcaveNortheast = 0xc000, + */ + + TopHalf = 0xd000, + BottomHalf = 0xe000, +} diff --git a/src/Blocktavius.DQB2/DQB2Constants.cs b/src/Blocktavius.DQB2/DQB2Constants.cs index ac2b823..4a6c238 100644 --- a/src/Blocktavius.DQB2/DQB2Constants.cs +++ b/src/Blocktavius.DQB2/DQB2Constants.cs @@ -40,4 +40,20 @@ internal static bool HasFoundationAt(this IChunk chunk, XZ xz) var blockId = chunk.GetBlock(new Point(xz, 0)); return Block.MakeCanonical(blockId) != 0; } + + internal static Chisel GetDiagonalChisel(this Direction dir) + { + return dir.Index switch + { + Direction.IndexConstants.North => Chisel.DiagonalNorth, + Direction.IndexConstants.East => Chisel.DiagonalEast, + Direction.IndexConstants.South => Chisel.DiagonalSouth, + Direction.IndexConstants.West => Chisel.DiagonalWest, + Direction.IndexConstants.NorthEast => Chisel.DiagonalNorthEast, + Direction.IndexConstants.SouthEast => Chisel.DiagonalSouthEast, + Direction.IndexConstants.SouthWest => Chisel.DiagonalSouthWest, + Direction.IndexConstants.NorthWest => Chisel.DiagonalNorthWest, + _ => Chisel.None, + }; + } } diff --git a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs index f48c7f3..bc4e0ac 100644 --- a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs +++ b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs @@ -150,3 +150,106 @@ internal override void Apply(IMutableStage stage) } } } + +public sealed class PutTreeMutation : StageMutation +{ + public required XZ Center { get; init; } + public required int Elevation { get; init; } + public required PRNG Prng { get; init; } + + internal override void Apply(IMutableStage stage) + { + const ushort bark = 139; // "Bark" + const ushort leaves = 492; // "Teal Leaves" + + bool layer3Leaves = Prng.NextBool(); + bool tallCorner = Prng.NextBool(); + bool peak = Prng.NextBool(); + + // layer 1 + int y = Elevation; + PutBlock(stage, Center, y, bark); + foreach (var dir in Direction.CardinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, SetChisel(bark, dir)); + } + + // layer 2 + y++; + PutBlock(stage, Center, y, bark); + + // layer 3 + y++; + PutBlock(stage, Center, y, bark); + foreach (var dir in Direction.CardinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, SetChisel(leaves, Chisel.TopHalf)); + PutBlock(stage, Center.Step(dir, 2), y, leaves); + } + if (layer3Leaves) + { + foreach (var dir in Direction.OrdinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, SetChisel(leaves, Chisel.TopHalf)); + } + } + + // layer 4 + y++; + PutBlock(stage, Center, y, bark); + foreach (var dir in Direction.CardinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, leaves); + } + foreach (var dir in Direction.OrdinalDirections()) + { + ushort block = tallCorner ? leaves : SetChisel(leaves, dir); + PutBlock(stage, Center.Step(dir), y, block); + } + + // layer 5 + y++; + PutBlock(stage, Center, y, bark); + foreach (var dir in Direction.CardinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, leaves); + } + if (tallCorner) + { + foreach (var dir in Direction.OrdinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, SetChisel(leaves, dir)); + } + } + + // layer 6 + y++; + PutBlock(stage, Center, y, leaves); + foreach (var dir in Direction.CardinalDirections()) + { + PutBlock(stage, Center.Step(dir), y, SetChisel(leaves, dir)); + } + + // layer 7 + y++; + if (peak) + { + PutBlock(stage, Center, y, SetChisel(leaves, Chisel.BottomHalf)); + } + } + + private void PutBlock(IMutableStage stage, XZ xz, int y, ushort block) + { + if (stage.TryGetChunk(ChunkOffset.FromXZ(xz), out var chunk)) + { + chunk.SetBlock(new Point(xz, y), block); + } + } + + private static ushort SetChisel(ushort block, Direction dir) => SetChisel(block, dir.Turn180.GetDiagonalChisel()); + + private static ushort SetChisel(ushort block, Chisel chisel) + { + return Block.Lookup(block).SetChisel(chisel).BlockIdComplete; + } +}