diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs index 24f5c9387e..191dd00f46 100644 --- a/OpenRA.Game/Map/MapPreview.cs +++ b/OpenRA.Game/Map/MapPreview.cs @@ -101,6 +101,15 @@ namespace OpenRA return node; } + static bool IsLoadableRuleDefinition(MiniYamlNode n) + { + if (n.Key[0] == '^') + return true; + + var key = n.Key.ToLowerInvariant(); + return key == "world" || key == "player"; + } + public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary yaml) { RuleDefinitions = LoadRuleSection(yaml, "Rules"); @@ -113,19 +122,34 @@ namespace OpenRA try { - var rules = Ruleset.Load(modData, fileSystem, TileSet, RuleDefinitions, - WeaponDefinitions, VoiceDefinitions, NotificationDefinitions, - MusicDefinitions, SequenceDefinitions, ModelSequenceDefinitions); + // PERF: Implement a minimal custom loader for custom world and player actors to minimize loading time + // This assumes/enforces that these actor types can only inherit abstract definitions (starting with ^) + if (RuleDefinitions != null) + { + var files = modData.Manifest.Rules.AsEnumerable(); + if (RuleDefinitions.Value != null) + { + var mapFiles = FieldLoader.GetValue("value", RuleDefinitions.Value); + files = files.Append(mapFiles); + } - WorldActorInfo = rules.Actors[SystemActors.World]; - PlayerActorInfo = rules.Actors[SystemActors.Player]; + var sources = files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s).Where(IsLoadableRuleDefinition).ToList()); + if (RuleDefinitions.Nodes.Any()) + sources = sources.Append(RuleDefinitions.Nodes.Where(IsLoadableRuleDefinition).ToList()); + + var yamlNodes = MiniYaml.Merge(sources); + WorldActorInfo = new ActorInfo(modData.ObjectCreator, "world", yamlNodes.First(n => n.Key.ToLowerInvariant() == "world").Value); + PlayerActorInfo = new ActorInfo(modData.ObjectCreator, "player", yamlNodes.First(n => n.Key.ToLowerInvariant() == "player").Value); + return; + } } catch (Exception e) { - Log.Write("debug", "Failed to load rules for `{0}` with error :{1}", Title, e.Message); - WorldActorInfo = modData.DefaultRules.Actors[SystemActors.World]; - PlayerActorInfo = modData.DefaultRules.Actors[SystemActors.Player]; + Log.Write("debug", "Failed to load rules for `{0}` with error: {1}", Title, e.Message); } + + WorldActorInfo = modData.DefaultRules.Actors[SystemActors.World]; + PlayerActorInfo = modData.DefaultRules.Actors[SystemActors.Player]; } public InnerData Clone() @@ -158,6 +182,8 @@ namespace OpenRA public MapClassification Class => innerData.Class; public MapVisibility Visibility => innerData.Visibility; + public MiniYaml RuleDefinitions => innerData.RuleDefinitions; + public ActorInfo WorldActorInfo => innerData.WorldActorInfo; public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo; diff --git a/OpenRA.Mods.Common/Lint/CheckWorldAndPlayerInherits.cs b/OpenRA.Mods.Common/Lint/CheckWorldAndPlayerInherits.cs new file mode 100644 index 0000000000..2d968a4fe6 --- /dev/null +++ b/OpenRA.Mods.Common/Lint/CheckWorldAndPlayerInherits.cs @@ -0,0 +1,96 @@ +#region Copyright & License Information +/* + * Copyright 2007-2021 The OpenRA Developers (see AUTHORS) + * 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 System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.FileSystem; +using OpenRA.Mods.Common.UpdateRules; +using OpenRA.Server; + +namespace OpenRA.Mods.Common.Lint +{ + public class CheckWorldAndPlayerInherits : ILintPass, ILintMapPass, ILintServerMapPass + { + void ILintPass.Run(Action emitError, Action emitWarning, ModData modData) + { + var nodes = new List(); + foreach (var f in modData.Manifest.Rules) + nodes.AddRange(MiniYaml.FromStream(modData.DefaultFileSystem.Open(f), f)); + + Run(emitError, nodes); + } + + void ILintMapPass.Run(Action emitError, Action emitWarning, ModData modData, Map map) + { + CheckMapYaml(emitError, modData, map, map.RuleDefinitions); + } + + void ILintServerMapPass.Run(Action emitError, Action emitWarning, ModData modData, MapPreview map, Ruleset mapRules) + { + CheckMapYaml(emitError, modData, map, map.RuleDefinitions); + } + + void CheckMapYaml(Action emitError, ModData modData, IReadOnlyFileSystem fileSystem, MiniYaml ruleDefinitions) + { + if (ruleDefinitions == null) + return; + + var files = modData.Manifest.Rules.AsEnumerable(); + if (ruleDefinitions.Value != null) + { + var mapFiles = FieldLoader.GetValue("value", ruleDefinitions.Value); + files = files.Append(mapFiles); + } + + var nodes = new List(); + foreach (var f in files) + nodes.AddRange(MiniYaml.FromStream(fileSystem.Open(f), f)); + + nodes.AddRange(ruleDefinitions.Nodes); + Run(emitError, nodes); + } + + void Run(Action emitError, List nodes) + { + // Build a list of all inheritance relationships + var inheritsMap = new Dictionary>(); + foreach (var actorNode in nodes) + { + var inherits = inheritsMap.GetOrAdd(actorNode.Key, _ => new List()); + foreach (var inheritsNode in actorNode.ChildrenMatching("Inherits")) + inherits.Add(inheritsNode.Value.Value); + } + + CheckInheritance(emitError, "World", inheritsMap); + CheckInheritance(emitError, "Player", inheritsMap); + } + + void CheckInheritance(Action emitError, string actor, Dictionary> inheritsMap) + { + var toResolve = new Queue(inheritsMap.Keys.Where(k => k.ToLowerInvariant() == actor.ToLowerInvariant())); + while (toResolve.TryDequeue(out var key)) + { + // Missing keys are a fatal merge error, so will have already been reported by other lint checks + if (!inheritsMap.TryGetValue(key, out var inherits)) + continue; + + foreach (var inherit in inherits) + { + if (inherit[0] != '^') + emitError("{0} definition inherits from {1}, which is not an abstract template.".F(actor, inherit)); + + toResolve.Enqueue(inherit); + } + } + } + } +}