From 2cf2d7eed787eb145d8fddbbcdab50fe2ec38297 Mon Sep 17 00:00:00 2001 From: Ashley Newson Date: Sat, 3 Jan 2026 01:52:03 +0000 Subject: [PATCH] Place map generator spawns more intuitively The map generators previously placed spawns in an order that was somewhat arbitrary and unintuitive to users. This change provides and makes use of a Terraformer utility to reorder spawns, based on their location, so that they typically make more sense to users selecting spawns from a lobby. Added to ExperimentalMapGenerator (RA, CnC), and D2kMapGenerator. Note that as this changes the output of the map generator for a given selection of settings. (Replays using generated maps from older versions will lose compatibility and no longer load.) --- .../MapGenerator/Terraformer.cs | 58 +++++++++++++++++++ .../Traits/World/ExperimentalMapGenerator.cs | 1 + .../Traits/World/D2kMapGenerator.cs | 1 + 3 files changed, 60 insertions(+) diff --git a/OpenRA.Mods.Common/MapGenerator/Terraformer.cs b/OpenRA.Mods.Common/MapGenerator/Terraformer.cs index 9dab8c6eea..8b6d1279e3 100644 --- a/OpenRA.Mods.Common/MapGenerator/Terraformer.cs +++ b/OpenRA.Mods.Common/MapGenerator/Terraformer.cs @@ -2243,5 +2243,63 @@ namespace OpenRA.Mods.Common.MapGenerator return decorable; } + + /// Reorder mpspawn positions to make them more intuitive within a lobby. + public void ReorderPlayerSpawns() + { + // Find and take the actors out of the ActorPlans list. + var mpspawns = ActorPlans.Where(a => a.Reference.Type == "mpspawn").ToList(); + if (mpspawns.Count <= 1) + return; + + ActorPlans.RemoveAll(a => a.Reference.Type == "mpspawn"); + + // Sort the spawns clockwise around the center + var wCenter = CellLayerUtils.Center(Map); + (int Angle, long RadiusSq) PolarPosition(ActorPlan plan) + { + var locationFromCenter = plan.WPosCenterLocation - wCenter; + var wangle = WAngle.ArcTan(locationFromCenter.Y, locationFromCenter.X); + var radiusSq = locationFromCenter.LengthSquared; + return (wangle.Angle, radiusSq); + } + + mpspawns = mpspawns.OrderBy(PolarPosition).ToList(); + + // Find a reasonable ("A") spawn. It should: + // - Have the largest possible preceeding angular gap from the preceeding spawn. + // - (Tie breaker) should be close to the left. + // - (Tie breaker 2) should be close to the top. + // + // Note that this offers a strong suggestion of spawn groupings for teams. This works + // well for the vast majority of cases, but is ultimately subjective. In some rare + // cases, it may suggest teams that seem unusual compared to alternatives, for example: + // - long but orderly straight lines of spawns (rather than tight clusters); + // - clustering by euclidean distance rather than polar angle; + // - division according to land masses or terrain barriers. + var previousPolar = PolarPosition(mpspawns[^1]); + var choices = new List<(int Gap, int X, int Y, int Index)>(); + for (var i = 0; i < mpspawns.Count; i++) + { + var thisPolar = PolarPosition(mpspawns[i]); + var gap = (thisPolar.Angle - previousPolar.Angle + 1024) % 1024; + var location = mpspawns[i].WPosLocation; + choices.Add((gap, location.X, location.Y, i)); + previousPolar = thisPolar; + } + + const int ComparisonThreshold = 2; + var bestGap = choices.Max(c => c.Gap); + var bestIndex = choices + .Where(c => c.Gap >= bestGap - ComparisonThreshold) + .Min(c => (c.X, c.Y, c.Index)) + .Index; + + // Rotate the spawns so that our "A" spawn is first. + mpspawns = [.. mpspawns[bestIndex..], .. mpspawns[..bestIndex]]; + + // Put them back into the ActorPlans list. + ActorPlans.AddRange(mpspawns); + } } } diff --git a/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs b/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs index b19ed12a41..8b2d320ac5 100644 --- a/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs +++ b/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs @@ -947,6 +947,7 @@ namespace OpenRA.Mods.Common.Traits // Cosmetically repaint tiles terraformer.RepaintTiles(repaintRandom, param.RepaintTiles); + terraformer.ReorderPlayerSpawns(); terraformer.BakeMap(); return map; diff --git a/OpenRA.Mods.D2k/Traits/World/D2kMapGenerator.cs b/OpenRA.Mods.D2k/Traits/World/D2kMapGenerator.cs index db8bcf869a..4630946b1d 100644 --- a/OpenRA.Mods.D2k/Traits/World/D2kMapGenerator.cs +++ b/OpenRA.Mods.D2k/Traits/World/D2kMapGenerator.cs @@ -610,6 +610,7 @@ namespace OpenRA.Mods.D2k.Traits } } + terraformer.ReorderPlayerSpawns(); terraformer.BakeMap(); return map;