From ff3f2bd3dfe015f6449e014040f230275d5acebb Mon Sep 17 00:00:00 2001 From: haven1433 Date: Sat, 18 Feb 2023 00:53:05 -0600 Subject: [PATCH] prototype for wave function collapse given an area of all the same block, replace all those blocks with blocks that match the surrounding non-same blocks based on what blocks should 'match'. Note that this is a single-pass algorithm right now, which means that it creates a bunch of bad blocks as it gets further from the edge and more conflicts are found. We may try to update this more in the future, or we may just go with something simpler like a maze generator using border blocks or 9-grid. But the results of this are tantalizing... --- src/HexManiac.Core/Core/SystemExtensions.cs | 14 ++ .../ViewModels/Map/BlockMapViewModel.cs | 47 ++++- .../ViewModels/Map/MapEditorViewModel.cs | 185 +++++++++++++++++- .../ViewModels/Map/MapRepointer.cs | 2 +- src/HexManiac.WPF/Controls/MapTab.xaml.cs | 1 + 5 files changed, 242 insertions(+), 7 deletions(-) diff --git a/src/HexManiac.Core/Core/SystemExtensions.cs b/src/HexManiac.Core/Core/SystemExtensions.cs index 2a58afa2..4d49fe34 100644 --- a/src/HexManiac.Core/Core/SystemExtensions.cs +++ b/src/HexManiac.Core/Core/SystemExtensions.cs @@ -290,6 +290,20 @@ namespace HavenSoft.HexManiac.Core { var index = rnd.Next(list.Count); return list[index]; } + + public static T Ensure(this IList list, Func predicate, T element) { + var existing = list.FirstOrDefault(predicate); + if (existing != null) return existing; + list.Add(element); + return element; + } + + public static V Ensure(this IDictionary dict, K key, Func valueFactory) { + if (dict.TryGetValue(key, out var result)) return result; + var value = valueFactory(); + dict[key] = value; + return value; + } } public static class NativeProcess { diff --git a/src/HexManiac.Core/ViewModels/Map/BlockMapViewModel.cs b/src/HexManiac.Core/ViewModels/Map/BlockMapViewModel.cs index e15fca9a..104d7452 100644 --- a/src/HexManiac.Core/ViewModels/Map/BlockMapViewModel.cs +++ b/src/HexManiac.Core/ViewModels/Map/BlockMapViewModel.cs @@ -995,10 +995,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { var address = start + ((yy + y) * width + xx + x) * 2; // var block = blockValues[xx % blockValues.GetLength(0), yy % blockValues.GetLength(1)]; var blockValue = model.ReadMultiByteValue(address, 2); - var originalBlockValue = blockValue; + lastDrawVal = blockValue; if (block >= 0) blockValue = (blockValue & 0xFC00) + block; if (collision >= 0) blockValue = (blockValue & 0x3FF) + (collision << 10); - if (blockValue != originalBlockValue) { + if (blockValue != lastDrawVal) { model.WriteMultiByteValue(address, 2, futureToken(), blockValue); changeCount++; } @@ -1112,6 +1112,49 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { ClearPixelCache(); } + public void PaintWaveFunction(ModelDelta token, double x, double y, Func wave) { + (x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale); + (x, y) = (x / 16, y / 16); + var layout = GetLayout(); + var (width, height) = (layout.GetValue("width"), layout.GetValue("height")); + var border = GetBorderThickness(layout); + var (xx, yy) = ((int)x - border.West, (int)y - border.North); + if (xx < 0 || yy < 0 || xx > width || yy > height) return; + var start = layout.GetAddress("blockmap"); + + // first pass: set all the effected spaces to 0 so they won't count + var toDraw = new Queue(); + toDraw.Enqueue(new(xx, yy)); + var drawn = new List(); + lock (pixelWriteLock) { + while (toDraw.Count > 0) { + var p = toDraw.Dequeue(); + if (drawn.Contains(p)) continue; + var address = start + (p.Y * width + (p.X - 1)) * 2; + if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X - 1, p.Y)); + address = start + (p.Y * width + (p.X + 1)) * 2; + if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X + 1, p.Y)); + address = start + ((p.Y - 1) * width + p.X) * 2; + if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X, p.Y - 1)); + address = start + ((p.Y + 1) * width + p.X) * 2; + if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X, p.Y + 1)); + address = start + (p.Y * width + p.X) * 2; + model.WriteMultiByteValue(address, 2, token, 0); + drawn.Add(p); + } + + // second pass: wave-fill in the reverse order (outside in) + drawn.Reverse(); + foreach (var p in drawn) { + var targetVal = wave(p.X, p.Y); + var address = start + (p.Y * width + p.X) * 2; + model.WriteMultiByteValue(address, 2, token, targetVal); + } + } + + ClearPixelCache(); + } + #endregion #region Events diff --git a/src/HexManiac.Core/ViewModels/Map/MapEditorViewModel.cs b/src/HexManiac.Core/ViewModels/Map/MapEditorViewModel.cs index 7693e903..bc335dd2 100644 --- a/src/HexManiac.Core/ViewModels/Map/MapEditorViewModel.cs +++ b/src/HexManiac.Core/ViewModels/Map/MapEditorViewModel.cs @@ -93,6 +93,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { public EventTemplate Templates => templates; + private int PrimaryBlocks { get; } private int PrimaryTiles { get; } private string hoverPoint, zoomLevel; @@ -312,6 +313,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { history.Bind(nameof(history.HasDataChange), (sender, e) => NotifyPropertyChanged(nameof(Name))); var isFRLG = model.IsFRLG(); PrimaryTiles = isFRLG ? 640 : 512; + PrimaryBlocks = PrimaryTiles; this.format = new Format(model); var map = new BlockMapViewModel(fileSystem, Tutorials, viewPort, format, 3, 0); @@ -625,15 +627,17 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { Point drawSource, lastDraw; private void DrawDown(double x, double y, PrimaryInteractionStart click) { interactionType = PrimaryInteractionType.Draw; - if (click == PrimaryInteractionStart.ControlClick) interactionType = PrimaryInteractionType.RectangleDraw; + if ((click & PrimaryInteractionStart.ControlClick) != 0) interactionType = PrimaryInteractionType.RectangleDraw; if (click == PrimaryInteractionStart.ShiftClick) interactionType = PrimaryInteractionType.Draw9Grid; var map = MapUnderCursor(x, y); - if (click == PrimaryInteractionStart.DoubleClick) { + if ((click & PrimaryInteractionStart.DoubleClick) != 0) { if (drawBlockIndex < 0 && collisionIndex < 0) { // nothing to paint interactionType = PrimaryInteractionType.None; } else { - if (map != null && !drawMultipleTiles) { + if (interactionType == PrimaryInteractionType.RectangleDraw && map != null) { + map.PaintWaveFunction(history.CurrentChange, x, y, RunWaveFunctionCollapseWithColision); + } else if (map != null && !drawMultipleTiles) { if (blockBag.Contains(drawBlockIndex)) { map.PaintBlockBag(history.CurrentChange, blockBag, collisionIndex, x, y); } else { @@ -1572,6 +1576,172 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { #endregion + #region Wave Function Collapse + + public record CollapseProbability(int Block) { public int Count { get; set; } }; + public record WaveNeighbors(List Left, List Right, List Up, List Down); + Dictionary waveFunctionPrimary; + Dictionary waveFunctionSecondary; + Dictionary waveFunctionMixed; + + private void CalculateWaveCollapseProbabilities() { + waveFunctionPrimary = new(); + waveFunctionSecondary = new(); + waveFunctionMixed = new(); + foreach (var bank in AllMapsModel.Create(model, () => history.CurrentChange)) { + foreach (var map in bank) { + var cells = map.Blocks; + var primary = map.Layout.PrimaryBlockset.Start; + var secondary = map.Layout.SecondaryBlockset.Start; + var mixed = ((long)primary << 32) + secondary; + var primaryWaveNeighbors = waveFunctionPrimary.Ensure(primary, () => new WaveNeighbors[PrimaryBlocks]); + var secondaryWaveNeighbors = waveFunctionSecondary.Ensure(secondary, () => new WaveNeighbors[TotalBlocks - PrimaryBlocks]); + var mixedWaveNeighbors = waveFunctionMixed.Ensure(mixed, () => new WaveNeighbors[TotalBlocks]); + + for (int x = 0; x < cells.Width; x++) { + for (int y = 0; y < cells.Height; y++) { + var center = cells[x, y]; + if (IsPrimaryBlock(center)) { + CheckPrimary(cells, x - 1, y, center, primaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Left); + CheckPrimary(cells, x + 1, y, center, primaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Right); + CheckPrimary(cells, x, y - 1, center, primaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Up); + CheckPrimary(cells, x, y + 1, center, primaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Down); + } else { + CheckSecondary(cells, x - 1, y, center, secondaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Left); + CheckSecondary(cells, x + 1, y, center, secondaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Right); + CheckSecondary(cells, x, y - 1, center, secondaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Up); + CheckSecondary(cells, x, y + 1, center, secondaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Down); + } + } + } + } + } + } + + private int RunWaveFunctionCollapseWithColision(int xx, int yy) { + var blockIndex = RunWaveFunctionCollapse(xx, yy); + var preferredCollision = GetPreferredCollision(blockIndex); + return (preferredCollision << 10) | blockIndex; + } + + private int RunWaveFunctionCollapse(int xx, int yy) { + if (waveFunctionPrimary == null) CalculateWaveCollapseProbabilities(); + var layout = new LayoutModel(PrimaryMap.GetLayout()); + var primary = layout.PrimaryBlockset.Start; + var secondary = layout.SecondaryBlockset.Start; + var mixed = ((long)primary << 32) + secondary; + var primaryWaveNeighbors = waveFunctionPrimary.Ensure(primary, () => new WaveNeighbors[PrimaryBlocks]); + var secondaryWaveNeighbors = waveFunctionSecondary.Ensure(secondary, () => new WaveNeighbors[TotalBlocks - PrimaryBlocks]); + var mixedWaveNeighbors = waveFunctionMixed.Ensure(mixed, () => new WaveNeighbors[TotalBlocks]); + + var cells = layout.BlockMap; + var probabilities = new List>(); + AddProbabilities(probabilities, cells, xx - 1, yy, primaryWaveNeighbors, secondaryWaveNeighbors, mixedWaveNeighbors, wv => wv.Right); + AddProbabilities(probabilities, cells, xx + 1, yy, primaryWaveNeighbors, secondaryWaveNeighbors, mixedWaveNeighbors, wv => wv.Left); + AddProbabilities(probabilities, cells, xx, yy - 1, primaryWaveNeighbors, secondaryWaveNeighbors, mixedWaveNeighbors, wv => wv.Down); + AddProbabilities(probabilities, cells, xx, yy + 1, primaryWaveNeighbors, secondaryWaveNeighbors, mixedWaveNeighbors, wv => wv.Up); + + // remove all the empty probability sets (they're not adding any restrictions) + for (int i = probabilities.Count - 1; i >= 0; i--) { + if (probabilities[i].Count == 0) probabilities.RemoveAt(i); + } + + // the block is constricted in options based on its neighbors + // if the neighbor can only be A/B based on the left and only B/C based on the top, it must be B. + while (probabilities.Count > 1) { + var last = probabilities[probabilities.Count - 1]; + var next = probabilities[probabilities.Count - 2]; + probabilities.RemoveAt(probabilities.Count - 1); + probabilities.RemoveAt(probabilities.Count - 1); + + var merged = new List(); + foreach (var cp1 in last) { + var cp2 = next.FirstOrDefault(match => match.Block == cp1.Block); + if (cp2 == null) continue; // no match + merged.Add(new(cp1.Block) { Count = cp1.Count + cp2.Count }); + } + if (merged.Count == 0) { + // couldn't find a match + // matching one is better than matching neither + probabilities.Add(rnd.Next(2) == 0 ? last : next); + } else { + probabilities.Add(merged); + } + } + + if (probabilities.Count == 0 || probabilities[0].Count == 0) { + // no restriction, pick any block + var (availableBlocks, _) = PrimaryMap.MapRepointer.EstimateBlockCount(layout.Element, true); + return rnd.Next(availableBlocks); + } else if (probabilities[0].Count == 1) { + // only one option, go with that + return probabilities[0][0].Block; + } + // pick one block from among the available blocks at random (weighted based on use) + var totalOptions = probabilities[0].Sum(cp => cp.Count); + var selection = rnd.Next(totalOptions); + var index = 0; + while (selection > probabilities[0][index].Count) { + selection -= probabilities[0][index].Count; + index += 1; + } + return probabilities[0][index].Block; + } + + private void AddProbabilities(List> probabilities, BlockCells cells, int xx, int yy, WaveNeighbors[] primaryWaveNeighbors, WaveNeighbors[] secondaryWaveNeighbors, WaveNeighbors[] mixedWaveNeighbors, Func> reverse) { + if (xx < 0 || yy < 0 || xx >= cells.Width || yy >= cells.Height) return; + var edge = cells[xx, yy].Tile; + if (edge == 0) return; + if (IsPrimaryBlock(cells[xx, yy])) { + var mergedProbabilities = new List(); + if (primaryWaveNeighbors[edge] != null) mergedProbabilities.AddRange(reverse(primaryWaveNeighbors[edge])); + if (mixedWaveNeighbors[edge] != null) mergedProbabilities.AddRange(reverse(mixedWaveNeighbors[edge])); + probabilities.Add(mergedProbabilities); + } else { + var mergedProbabilities = new List(); + if (secondaryWaveNeighbors[edge - PrimaryBlocks] != null) mergedProbabilities.AddRange(reverse(secondaryWaveNeighbors[edge - PrimaryBlocks])); + if (mixedWaveNeighbors[edge] != null) mergedProbabilities.AddRange(reverse(mixedWaveNeighbors[edge])); + probabilities.Add(mergedProbabilities); + } + } + + private void CheckPrimary(BlockCells cells, int x, int y, BlockCell center, WaveNeighbors[] primaryWaveNeighbors, WaveNeighbors[] mixedWaveNeighbors, Func> direction) { + var primaryElement = primaryWaveNeighbors[center.Tile]; + var mixedElement = mixedWaveNeighbors[center.Tile]; + if (x < 0 || x >= cells.Width || y < 0 || y >= cells.Height) return; + var edge = cells[x, y]; + if (IsPrimaryBlock(edge)) { + if (primaryElement == null) primaryElement = primaryWaveNeighbors[center.Tile] = new(new(), new(), new(), new()); + var collapse = direction(primaryElement).Ensure(cp => cp.Block == edge.Tile, new CollapseProbability(edge.Tile)); + collapse.Count += 1; + } else { + if (mixedElement == null) mixedElement = mixedWaveNeighbors[center.Tile] = new(new(), new(), new(), new()); + var collapse = direction(mixedElement).Ensure(cp => cp.Block == edge.Tile, new CollapseProbability(edge.Tile)); + collapse.Count += 1; + } + } + + private void CheckSecondary(BlockCells cells, int x, int y, BlockCell center, WaveNeighbors[] secondaryWaveNeighbors, WaveNeighbors[] mixedWaveNeighbors, Func> direction) { + var secondaryElement = secondaryWaveNeighbors[center.Tile - PrimaryBlocks]; + var mixedElement = mixedWaveNeighbors[center.Tile]; + if (x < 0 || x >= cells.Width || y < 0 || y >= cells.Height) return; + var edge = cells[x, y]; + if (IsPrimaryBlock(edge)) { + if (secondaryElement == null) secondaryElement = secondaryWaveNeighbors[center.Tile - PrimaryBlocks] = new(new(), new(), new(), new()); + var collapse = direction(secondaryElement).Ensure(cp => cp.Block == edge.Tile, new CollapseProbability(edge.Tile)); + collapse.Count += 1; + } else { + if (mixedElement == null) mixedElement = mixedWaveNeighbors[center.Tile] = new(new(), new(), new(), new()); + var collapse = direction(mixedElement).Ensure(cp => cp.Block == edge.Tile, new CollapseProbability(edge.Tile)); + collapse.Count += 1; + } + } + + private bool IsPrimaryBlock(BlockCell cell) => cell.Tile < PrimaryBlocks; + private int TotalBlocks => 1024; + + #endregion + private Dictionary preferredCollisionsPrimary; private Dictionary preferredCollisionsSecondary; private void CountCollisionForBlocks() { @@ -1708,7 +1878,14 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { public enum EventCreationType { None, Object, Warp, Script, Signpost, Fly } - public enum PrimaryInteractionStart { None, Click, DoubleClick, ControlClick, ShiftClick } + [Flags] + public enum PrimaryInteractionStart { + None = 0, + Click = 1, + DoubleClick = 2, + ControlClick = 4, + ShiftClick = 8, + } public enum PrimaryInteractionType { None, Draw, Event, RectangleDraw, Draw9Grid } public record BlocksetCache(ObservableCollection Primary, ObservableCollection Secondary) { diff --git a/src/HexManiac.Core/ViewModels/Map/MapRepointer.cs b/src/HexManiac.Core/ViewModels/Map/MapRepointer.cs index 9e84bdcf..3c6574e7 100644 --- a/src/HexManiac.Core/ViewModels/Map/MapRepointer.cs +++ b/src/HexManiac.Core/ViewModels/Map/MapRepointer.cs @@ -359,7 +359,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { DataMoved.Raise(this, new("Block", blockStart)); } - private (int currentCount, int maxCount) EstimateBlockCount(ModelArrayElement layout, bool primary) { + public (int currentCount, int maxCount) EstimateBlockCount(ModelArrayElement layout, bool primary) { var blocksetName = primary ? Format.PrimaryBlockset : Format.SecondaryBlockset; if (layout == null) return (0, 0); var blockset = layout.GetSubTable(blocksetName)[0]; diff --git a/src/HexManiac.WPF/Controls/MapTab.xaml.cs b/src/HexManiac.WPF/Controls/MapTab.xaml.cs index 24546bae..0b20e7b6 100644 --- a/src/HexManiac.WPF/Controls/MapTab.xaml.cs +++ b/src/HexManiac.WPF/Controls/MapTab.xaml.cs @@ -125,6 +125,7 @@ namespace HavenSoft.HexManiac.WPF.Controls { if (e.ClickCount == 2) interactionStart = PrimaryInteractionStart.DoubleClick; if (Keyboard.Modifiers == ModifierKeys.Shift) interactionStart = PrimaryInteractionStart.ShiftClick; if (Keyboard.Modifiers == ModifierKeys.Control) interactionStart = PrimaryInteractionStart.ControlClick; + if (e.ClickCount == 2 && Keyboard.Modifiers == ModifierKeys.Control) interactionStart = PrimaryInteractionStart.ControlClick | PrimaryInteractionStart.DoubleClick; vm.PrimaryDown(p.X, p.Y, interactionStart); } else if (e.MiddleButton == MouseButtonState.Pressed) { withinMapInteraction = MouseButton.Middle;