diff --git a/OpenRA.Game/Map/MapGenerationArgs.cs b/OpenRA.Game/Map/MapGenerationArgs.cs new file mode 100644 index 0000000000..93562694fe --- /dev/null +++ b/OpenRA.Game/Map/MapGenerationArgs.cs @@ -0,0 +1,43 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Primitives; + +namespace OpenRA +{ + public class MapGenerationArgs + { + [FieldLoader.Require] + public string Uid = null; + + [FieldLoader.Require] + public string Generator = null; + + [FieldLoader.Require] + public string Tileset = null; + + [FieldLoader.Require] + public Size Size = default; + + [FieldLoader.Require] + public string Title = null; + + [FieldLoader.Require] + public string Author = null; + + [FieldLoader.LoadUsing(nameof(LoadSettings))] + public MiniYaml Settings = null; + static MiniYaml LoadSettings(MiniYaml yaml) + { + return yaml.NodeWithKey("Settings").Value; + } + } +} diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs index 91f062abf0..0e7338d4e5 100644 --- a/OpenRA.Game/Traits/TraitsInterfaces.cs +++ b/OpenRA.Game/Traits/TraitsInterfaces.cs @@ -650,4 +650,13 @@ namespace OpenRA.Traits bool CrushableBy(Actor self, Actor crusher, BitSet crushClasses); LongBitSet CrushableBy(Actor self, BitSet crushClasses); } + + public interface IMapGeneratorInfo : ITraitInfoInterface + { + string Type { get; } + string Name { get; } + string MapTitle { get; } + + Map Generate(ModData modData, MapGenerationArgs args); + } } diff --git a/OpenRA.Mods.Common/MapGenerator/MapGeneratorSettings.cs b/OpenRA.Mods.Common/MapGenerator/MapGeneratorSettings.cs index 0fb9db434f..5b9fa0beb2 100644 --- a/OpenRA.Mods.Common/MapGenerator/MapGeneratorSettings.cs +++ b/OpenRA.Mods.Common/MapGenerator/MapGeneratorSettings.cs @@ -14,7 +14,9 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; using OpenRA.Support; +using OpenRA.Traits; namespace OpenRA.Mods.Common.MapGenerator { @@ -206,8 +208,11 @@ namespace OpenRA.Mods.Common.MapGenerator public sealed class MapGeneratorSettings : IMapGeneratorSettings { - public MapGeneratorSettings(MiniYaml yaml) + readonly IMapGeneratorInfo generatorInfo; + + public MapGeneratorSettings(IMapGeneratorInfo generatorInfo, MiniYaml yaml) { + this.generatorInfo = generatorInfo; foreach (var node in yaml.Nodes) { var split = node.Key.Split('@'); @@ -248,7 +253,7 @@ namespace OpenRA.Mods.Common.MapGenerator } } - public MiniYaml Compile(ITerrainInfo terrainInfo) + public MapGenerationArgs Compile(ITerrainInfo terrainInfo, Size size) { // Apply the choices in their canonical order. var playerCount = PlayerCount; @@ -256,7 +261,15 @@ namespace OpenRA.Mods.Common.MapGenerator .OrderBy(option => option.Priority) .Select(o => o.GetSettings(terrainInfo, playerCount)); - return new MiniYaml(null, MiniYaml.Merge(layers)); + return new MapGenerationArgs() + { + Generator = generatorInfo.Type, + Tileset = terrainInfo.Id, + Size = size, + Title = FluentProvider.GetMessage(generatorInfo.MapTitle), + Author = FluentProvider.GetMessage(generatorInfo.Name), + Settings = new MiniYaml(null, MiniYaml.Merge(layers)) + }; } } } diff --git a/OpenRA.Mods.Common/Traits/World/ClearMapGenerator.cs b/OpenRA.Mods.Common/Traits/World/ClearMapGenerator.cs index c31b7cc8c9..4b8cf4bc95 100644 --- a/OpenRA.Mods.Common/Traits/World/ClearMapGenerator.cs +++ b/OpenRA.Mods.Common/Traits/World/ClearMapGenerator.cs @@ -21,7 +21,7 @@ namespace OpenRA.Mods.Common.Traits { [TraitLocation(SystemActors.EditorWorld)] [Desc("A map generator that clears a map.")] - public sealed class ClearMapGeneratorInfo : TraitInfo, IMapGeneratorInfo, IEditorToolInfo + public sealed class ClearMapGeneratorInfo : TraitInfo, IEditorMapGeneratorInfo, IEditorToolInfo { [FieldLoader.Require] [Desc("Human-readable name this generator uses.")] @@ -32,6 +32,10 @@ namespace OpenRA.Mods.Common.Traits [Desc("Internal id for this map generator.")] public readonly string Type = null; + [FluentReference] + [Desc("The title to use for generated maps.")] + public readonly string MapTitle = "label-random-map"; + [Desc("The widget tree to open when the tool is selected.")] public readonly string PanelWidget = "MAP_GENERATOR_TOOL_PANEL"; @@ -44,8 +48,8 @@ namespace OpenRA.Mods.Common.Traits public readonly MiniYaml Settings; string IMapGeneratorInfo.Type => Type; - string IMapGeneratorInfo.Name => Name; + string IMapGeneratorInfo.MapTitle => MapTitle; static MiniYaml SettingsLoader(MiniYaml my) { @@ -54,26 +58,31 @@ namespace OpenRA.Mods.Common.Traits static List FluentReferencesLoader(MiniYaml my) { - return new MapGeneratorSettings(my.NodeWithKey("Settings").Value) + return new MapGeneratorSettings(null, my.NodeWithKey("Settings").Value) .Options.SelectMany(o => o.GetFluentReferences()).ToList(); } public IMapGeneratorSettings GetSettings() { - return new MapGeneratorSettings(Settings); + return new MapGeneratorSettings(this, Settings); } - public void Generate(Map map, MiniYaml settings) + public Map Generate(ModData modData, MapGenerationArgs args) { var random = new MersenneTwister(); + var terrainInfo = modData.DefaultTerrainInfo[args.Tileset]; - var tileset = map.Rules.TerrainInfo; + var map = new Map(modData, terrainInfo, args.Size); + var maxTerrainHeight = map.Grid.MaximumTerrainHeight; + var tl = new PPos(1, 1 + maxTerrainHeight); + var br = new PPos(args.Size.Width - 1, args.Size.Height + maxTerrainHeight - 1); + map.SetBounds(tl, br); - if (!Exts.TryParseUshortInvariant(settings.NodeWithKey("Tile").Value.Value, out var tileType)) + if (!Exts.TryParseUshortInvariant(args.Settings.NodeWithKey("Tile").Value.Value, out var tileType)) throw new YamlException("Illegal tile type"); var tile = new TerrainTile(tileType, 0); - if (!tileset.TryGetTerrainInfo(tile, out var _)) + if (!terrainInfo.TryGetTerrainInfo(tile, out var _)) throw new MapGenerationException("Illegal tile type"); // If the default terrain tile is part of a PickAny template, pick @@ -100,6 +109,8 @@ namespace OpenRA.Mods.Common.Traits map.PlayerDefinitions = new MapPlayers(map.Rules, 0).ToMiniYaml(); map.ActorDefinitions = []; + + return map; } string IEditorToolInfo.Label => Name; diff --git a/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs b/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs index 4d0a6b93f1..f6b461ed0a 100644 --- a/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs +++ b/OpenRA.Mods.Common/Traits/World/ExperimentalMapGenerator.cs @@ -23,7 +23,7 @@ using static OpenRA.Mods.Common.Traits.ResourceLayerInfo; namespace OpenRA.Mods.Common.Traits { [TraitLocation(SystemActors.EditorWorld)] - public sealed class ExperimentalMapGeneratorInfo : TraitInfo, IMapGeneratorInfo, IEditorToolInfo + public sealed class ExperimentalMapGeneratorInfo : TraitInfo, IEditorMapGeneratorInfo, IEditorToolInfo { [FieldLoader.Require] public readonly string Type = null; @@ -32,6 +32,10 @@ namespace OpenRA.Mods.Common.Traits [FluentReference] public readonly string Name = null; + [FluentReference] + [Desc("The title to use for generated maps.")] + public readonly string MapTitle = "label-random-map"; + [Desc("The widget tree to open when the tool is selected.")] public readonly string PanelWidget = "MAP_GENERATOR_TOOL_PANEL"; @@ -45,6 +49,7 @@ namespace OpenRA.Mods.Common.Traits string IMapGeneratorInfo.Type => Type; string IMapGeneratorInfo.Name => Name; + string IMapGeneratorInfo.MapTitle => MapTitle; static MiniYaml SettingsLoader(MiniYaml my) { @@ -53,7 +58,7 @@ namespace OpenRA.Mods.Common.Traits static List FluentReferencesLoader(MiniYaml my) { - return new MapGeneratorSettings(my.NodeWithKey("Settings").Value) + return new MapGeneratorSettings(null, my.NodeWithKey("Settings").Value) .Options.SelectMany(o => o.GetFluentReferences()).ToList(); } @@ -481,16 +486,27 @@ namespace OpenRA.Mods.Common.Traits public IMapGeneratorSettings GetSettings() { - return new MapGeneratorSettings(Settings); + return new MapGeneratorSettings(this, Settings); } - public void Generate(Map map, MiniYaml settings) + public Map Generate(ModData modData, MapGenerationArgs args) { const int ExternalBias = 4096; - var size = map.MapSize; + var terrainInfo = modData.DefaultTerrainInfo[args.Tileset]; + var size = args.Size; + + var map = new Map(modData, terrainInfo, size); + var maxTerrainHeight = map.Grid.MaximumTerrainHeight; + var tl = new PPos(1, 1 + maxTerrainHeight); + var br = new PPos(size.Width - 1, size.Height + maxTerrainHeight - 1); + map.SetBounds(tl, br); + map.Title = args.Title; + map.Author = args.Author; + map.RequiresMod = modData.Manifest.Id; + var minSpan = Math.Min(size.Width, size.Height); - var mapCenter1024ths = size.ToInt2() * 512; + var mapCenter1024ths = new int2(size.Width * 512, size.Height * 512); var wMapCenter = CellLayerUtils.Center(map.Tiles); var matrixMapCenter1024ths = CellLayerUtils.CellBounds(map).Size.ToInt2() * 512; var cellBounds = CellLayerUtils.CellBounds(map); @@ -499,19 +515,18 @@ namespace OpenRA.Mods.Common.Traits var actorPlans = new List(); - var param = new Parameters(map, settings); + var param = new Parameters(map, args.Settings); var externalCircleRadius = minCSpan / 2 - (param.MinimumLandSeaThickness + param.MinimumMountainThickness); if (externalCircleRadius <= 0) throw new MapGenerationException("map is too small for circular shaping"); - var tileset = (ITemplatedTerrainInfo)map.Rules.TerrainInfo; - var beachPermittedTemplates = - TilingPath.PermittedSegments.FromType(param.SegmentedBrushes, param.BeachSegmentTypes); - + var beachPermittedTemplates = TilingPath.PermittedSegments.FromType(param.SegmentedBrushes, param.BeachSegmentTypes); var replaceabilityMap = new Dictionary(); var playabilityMap = new Dictionary(); - foreach (var kv in tileset.Templates) + + var templatedTerrainInfo = (ITemplatedTerrainInfo)terrainInfo; + foreach (var kv in templatedTerrainInfo.Templates) { var id = kv.Key; var template = kv.Value; @@ -520,7 +535,7 @@ namespace OpenRA.Mods.Common.Traits if (template[ti] == null) continue; var tile = new TerrainTile(id, (byte)ti); - var type = tileset.GetTerrainIndex(tile); + var type = terrainInfo.GetTerrainIndex(tile); if (param.PlayableTerrain.Contains(type)) playabilityMap[tile] = PlayableSpace.Playability.Playable; @@ -565,7 +580,6 @@ namespace OpenRA.Mods.Common.Traits // random.Next(). All generators should be created unconditionally. var random = new MersenneTwister(param.Seed); - var pickAnyRandom = new MersenneTwister(random.Next()); var waterRandom = new MersenneTwister(random.Next()); var beachTilingRandom = new MersenneTwister(random.Next()); var cliffTilingRandom = new MersenneTwister(random.Next()); @@ -585,7 +599,7 @@ namespace OpenRA.Mods.Common.Traits TerrainTile PickTile(ushort tileType) { - if (tileset.Templates.TryGetValue(tileType, out var template) && template.PickAny) + if (templatedTerrainInfo.Templates.TryGetValue(tileType, out var template) && template.PickAny) return new TerrainTile(tileType, (byte)random.Next(0, template.TilesCount)); else return new TerrainTile(tileType, 0); @@ -939,10 +953,10 @@ namespace OpenRA.Mods.Common.Traits param.Mirror, (CPos[] sources, CPos destination) => { - var main = tileset.GetTerrainIndex(map.Tiles[destination]); + var main = templatedTerrainInfo.GetTerrainIndex(map.Tiles[destination]); var compatible = sources .Where(replace.Contains) - .Select(source => tileset.GetTerrainIndex(map.Tiles[source])) + .Select(source => templatedTerrainInfo.GetTerrainIndex(map.Tiles[source])) .All(source => CheckCompatibility(main, source)); replace[destination] = compatible ? MultiBrush.Replaceability.None : MultiBrush.Replaceability.Actor; }); @@ -1108,7 +1122,7 @@ namespace OpenRA.Mods.Common.Traits - CellLayerUtils.WPosToCPos(CellLayerUtils.Center(map.Tiles), gridType); foreach (var cpos in map.AllCells) - space[cpos + enlargedOffset] = param.ClearTerrain.Contains(tileset.GetTerrainIndex(map.Tiles[cpos])); + space[cpos + enlargedOffset] = param.ClearTerrain.Contains(templatedTerrainInfo.GetTerrainIndex(map.Tiles[cpos])); foreach (var actorPlan in actorPlans) foreach (var (cpos, _) in actorPlan.Footprint()) @@ -1291,7 +1305,7 @@ namespace OpenRA.Mods.Common.Traits var zoneable = new CellLayer(map); foreach (var mpos in map.AllCells.MapCoords) - zoneable[mpos] = playableArea[mpos] && param.ZoneableTerrain.Contains(tileset.GetTerrainIndex(map.Tiles[mpos])); + zoneable[mpos] = playableArea[mpos] && param.ZoneableTerrain.Contains(templatedTerrainInfo.GetTerrainIndex(map.Tiles[mpos])); foreach (var actorPlan in actorPlans) foreach (var cpos in actorPlan.Footprint().Keys) @@ -1738,7 +1752,7 @@ namespace OpenRA.Mods.Common.Traits { var space = new CellLayer(map); foreach (var mpos in map.AllCells.MapCoords) - space[mpos] = param.PlayableTerrain.Contains(tileset.GetTerrainIndex(map.Tiles[mpos])); + space[mpos] = param.PlayableTerrain.Contains(templatedTerrainInfo.GetTerrainIndex(map.Tiles[mpos])); foreach (var actorPlan in actorPlans) foreach (var (cpos, _) in actorPlan.Footprint()) @@ -1858,6 +1872,8 @@ namespace OpenRA.Mods.Common.Traits map.ActorDefinitions = actorPlans .Select((plan, i) => new MiniYamlNode($"Actor{i}", plan.Reference.Save())) .ToImmutableArray(); + + return map; } static CellLayer IdentifyReplaceableTiles( diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index 1d638f37d8..da25b3203a 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -977,33 +977,17 @@ namespace OpenRA.Mods.Common.Traits public interface IMapGeneratorSettings { - /// Returns the options that allow users to customise the result. List Options { get; } int PlayerCount { get; } void Randomize(MersenneTwister random); - /// Merge all choices into a complete settings MiniYaml. - MiniYaml Compile(ITerrainInfo terrainInfo); + MapGenerationArgs Compile(ITerrainInfo terrainInfo, Size size); } - public interface IMapGeneratorInfo : ITraitInfoInterface + public interface IEditorMapGeneratorInfo : IMapGeneratorInfo { - string Type { get; } - string Name { get; } - IMapGeneratorSettings GetSettings(); - - /// - /// Generate or manipulate a supplied map in-place. - /// - /// - /// May be thrown if the map settings are invalid. Map should be discarded. - /// - /// - /// Thrown if the map could not be generated with the requested configuration. Map should be discarded. - /// - void Generate(Map map, MiniYaml settings); } } diff --git a/OpenRA.Mods.Common/UtilityCommands/FuzzMapGeneratorCommand.cs b/OpenRA.Mods.Common/UtilityCommands/FuzzMapGeneratorCommand.cs index b046b7255a..1748ea9768 100644 --- a/OpenRA.Mods.Common/UtilityCommands/FuzzMapGeneratorCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/FuzzMapGeneratorCommand.cs @@ -207,7 +207,7 @@ namespace OpenRA.Mods.Common.UtilityCommands .ToImmutableArray(); var generator = modData.DefaultRules.Actors[SystemActors.EditorWorld] - .TraitInfos() + .TraitInfos() .FirstOrDefault(info => info.Type == config.MapGeneratorType); if (generator == null) throw new ArgumentException($"No map generator with type `{config.MapGeneratorType}`"); @@ -265,12 +265,6 @@ namespace OpenRA.Mods.Common.UtilityCommands if (size.Length != 2) throw new ArgumentException($"bad map size `{iterationChoices[Configuration.SizeVariable]}`"); - var map = new Map(modData, terrainInfo, new Size(size[0], size[1])); - var maxTerrainHeight = map.Grid.MaximumTerrainHeight; - var tl = new PPos(1, 1 + maxTerrainHeight); - var br = new PPos(size[0], size[1] + maxTerrainHeight); - map.SetBounds(tl, br); - var settings = generator.GetSettings(); foreach (var o in settings.Options) { @@ -299,7 +293,7 @@ namespace OpenRA.Mods.Common.UtilityCommands tests++; try { - generator.Generate(map, settings.Compile(terrainInfo)); + generator.Generate(modData, settings.Compile(terrainInfo, new Size(size[0], size[1]))); } catch (Exception e) when (e is MapGenerationException || e is YamlException) { diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/MapGeneratorToolLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/MapGeneratorToolLogic.cs index 8d863035e6..7d5e30358e 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Editor/MapGeneratorToolLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/MapGeneratorToolLogic.cs @@ -35,7 +35,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic readonly World world; readonly WorldRenderer worldRenderer; readonly ModData modData; - readonly IMapGeneratorInfo generator; + readonly IEditorMapGeneratorInfo generator; readonly IMapGeneratorSettings settings; readonly ScrollPanelWidget settingsPanel; @@ -45,7 +45,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic [ObjectCreator.UseCtor] public MapGeneratorToolLogic(Widget widget, World world, WorldRenderer worldRenderer, ModData modData, - IMapGeneratorInfo tool) + IEditorMapGeneratorInfo tool) { editorActionManager = world.WorldActor.Trait(); @@ -261,17 +261,14 @@ namespace OpenRA.Mods.Common.Widgets.Logic void GenerateMapMayThrow() { var map = world.Map; - var terrainInfo = map.Rules.TerrainInfo; - var generatedMap = new Map(modData, terrainInfo, map.MapSize); - var bounds = map.Bounds; - generatedMap.SetBounds(new PPos(bounds.Left, bounds.Top), new PPos(bounds.Right - 1, bounds.Bottom - 1)); - - var settings = this.settings.Compile(terrainInfo); + var terrainInfo = modData.DefaultTerrainInfo[map.Tileset]; + var size = new Size(map.Bounds.Width + 2, map.Bounds.Height + 2); + var args = settings.Compile(terrainInfo, size); // Run main generator logic. May throw. var generateStopwatch = Stopwatch.StartNew(); - Log.Write("debug", $"Running '{generator.Type}' map generator with settings:\n{MiniYamlExts.WriteToString(settings.Nodes)}\n\n"); - generator.Generate(generatedMap, settings); + Log.Write("debug", $"Running '{generator.Type}' map generator with settings:\n{MiniYamlExts.WriteToString(args.Settings.Nodes)}\n\n"); + var generatedMap = generator.Generate(modData, args); Log.Write("debug", $"Generator finished, taking {generateStopwatch.ElapsedMilliseconds}ms"); var editorActorLayer = world.WorldActor.Trait(); @@ -283,13 +280,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic kv => kv.Key); var tiles = new Dictionary(); - foreach (var cell in generatedMap.AllCells) + foreach (var uv in generatedMap.AllCells.MapCoords) { - var mpos = cell.ToMPos(map); - var resourceTile = generatedMap.Resources[mpos]; + var resourceTile = generatedMap.Resources[uv]; resourceTypesByIndex.TryGetValue(resourceTile.Type, out var resourceType); var resourceLayerContents = new ResourceLayerContents(resourceType, resourceTile.Index); - tiles.Add(cell, new BlitTile(generatedMap.Tiles[mpos], resourceTile, resourceLayerContents, generatedMap.Height[mpos])); + tiles.Add(uv.ToCPos(generatedMap), new BlitTile(generatedMap.Tiles[uv], resourceTile, resourceLayerContents, generatedMap.Height[uv])); } var previews = new Dictionary(); @@ -306,11 +302,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic previews.Add(kv.Key, preview); } + var offset = map.CellContaining(map.ProjectedTopLeft) - generatedMap.CellContaining(generatedMap.ProjectedTopLeft); var blitSource = new EditorBlitSource(generatedMap.AllCells, previews, tiles); var editorBlit = new EditorBlit( MapBlitFilters.All, resourceLayer, - new CPos(0, 0), + new CPos(offset.X, offset.Y), map, blitSource, editorActorLayer, diff --git a/mods/cnc/fluent/rules.ftl b/mods/cnc/fluent/rules.ftl index 3938b13076..f2fa9978b0 100644 --- a/mods/cnc/fluent/rules.ftl +++ b/mods/cnc/fluent/rules.ftl @@ -781,6 +781,7 @@ bot-hal9001 = .name = HAL 9001 ## map-generators.yaml +label-random-map = Random Map label-clear-map-generator-option-tile = Tile label-clear-map-generator-choice-tile-clear = .label = Clear diff --git a/mods/ra/fluent/rules.ftl b/mods/ra/fluent/rules.ftl index 6ea5f95804..aa3243ecb0 100644 --- a/mods/ra/fluent/rules.ftl +++ b/mods/ra/fluent/rules.ftl @@ -946,6 +946,7 @@ bot-naval-ai = .name = Naval AI ## map-generators.yaml +label-random-map = Random Map label-clear-map-generator-option-tile = Tile label-clear-map-generator-choice-tile-clear = .label = Clear