Split IMapGeneratorInfo into core backend and editor frontend.

This commit is contained in:
Paul Chote
2025-04-19 14:06:47 +01:00
committed by Gustas Kažukauskas
parent ce41ec3fc3
commit 9c78c45faf
10 changed files with 141 additions and 72 deletions

View File

@@ -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;
}
}
}

View File

@@ -650,4 +650,13 @@ namespace OpenRA.Traits
bool CrushableBy(Actor self, Actor crusher, BitSet<CrushClass> crushClasses);
LongBitSet<PlayerBitMask> CrushableBy(Actor self, BitSet<CrushClass> crushClasses);
}
public interface IMapGeneratorInfo : ITraitInfoInterface
{
string Type { get; }
string Name { get; }
string MapTitle { get; }
Map Generate(ModData modData, MapGenerationArgs args);
}
}

View File

@@ -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))
};
}
}
}

View File

@@ -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<ClearMapGenerator>, IMapGeneratorInfo, IEditorToolInfo
public sealed class ClearMapGeneratorInfo : TraitInfo<ClearMapGenerator>, 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<string> 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;

View File

@@ -23,7 +23,7 @@ using static OpenRA.Mods.Common.Traits.ResourceLayerInfo;
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.EditorWorld)]
public sealed class ExperimentalMapGeneratorInfo : TraitInfo<ExperimentalMapGenerator>, IMapGeneratorInfo, IEditorToolInfo
public sealed class ExperimentalMapGeneratorInfo : TraitInfo<ExperimentalMapGenerator>, 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<string> 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<ActorPlan>();
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<TerrainTile, MultiBrush.Replaceability>();
var playabilityMap = new Dictionary<TerrainTile, PlayableSpace.Playability>();
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<bool>(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<bool>(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<MultiBrush.Replaceability> IdentifyReplaceableTiles(

View File

@@ -977,33 +977,17 @@ namespace OpenRA.Mods.Common.Traits
public interface IMapGeneratorSettings
{
/// <summary>Returns the options that allow users to customise the result.</summary>
List<MapGeneratorOption> Options { get; }
int PlayerCount { get; }
void Randomize(MersenneTwister random);
/// <summary>Merge all choices into a complete settings MiniYaml.</summary>
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();
/// <summary>
/// Generate or manipulate a supplied map in-place.
/// </summary>
/// <exception cref="YamlException">
/// May be thrown if the map settings are invalid. Map should be discarded.
/// </exception>
/// <exception cref="MapGenerationException">
/// Thrown if the map could not be generated with the requested configuration. Map should be discarded.
/// </exception>
void Generate(Map map, MiniYaml settings);
}
}

View File

@@ -207,7 +207,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
.ToImmutableArray();
var generator = modData.DefaultRules.Actors[SystemActors.EditorWorld]
.TraitInfos<IMapGeneratorInfo>()
.TraitInfos<IEditorMapGeneratorInfo>()
.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)
{

View File

@@ -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<EditorActionManager>();
@@ -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<EditorActorLayer>();
@@ -283,13 +280,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic
kv => kv.Key);
var tiles = new Dictionary<CPos, BlitTile>();
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<string, EditorActorPreview>();
@@ -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,

View File

@@ -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

View File

@@ -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