From 0eb0041f903296a789bc9577bd217e5dbe9029d5 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Tue, 2 Jun 2020 19:39:32 +0100 Subject: [PATCH] Allow ActorInits to target a specific trait by matching the @ suffix. --- OpenRA.Game/GameRules/ActorInfo.cs | 9 +++++-- OpenRA.Game/Map/ActorInitializer.cs | 25 +++++++++++++++++-- OpenRA.Game/Map/ActorReference.cs | 13 +++++++--- OpenRA.Game/Traits/TraitsInterfaces.cs | 3 +++ OpenRA.Mods.Common/Graphics/ActorPreview.cs | 12 ++++++++- .../Scripting/Global/ActorGlobal.cs | 10 +++++--- 6 files changed, 61 insertions(+), 11 deletions(-) diff --git a/OpenRA.Game/GameRules/ActorInfo.cs b/OpenRA.Game/GameRules/ActorInfo.cs index 636ab93ea2..34747f1325 100644 --- a/OpenRA.Game/GameRules/ActorInfo.cs +++ b/OpenRA.Game/GameRules/ActorInfo.cs @@ -23,6 +23,7 @@ namespace OpenRA public class ActorInfo { public const string AbstractActorPrefix = "^"; + public const char TraitInstanceSeparator = '@'; /// /// The actor name can be anything, but the sprites used in the Render*: traits default to this one. @@ -46,7 +47,7 @@ namespace OpenRA { // HACK: The linter does not want to crash when a trait doesn't exist but only print an error instead // LoadTraitInfo will only return null to signal us to abort here if the linter is running - var trait = LoadTraitInfo(creator, t.Key.Split('@')[0], t.Value); + var trait = LoadTraitInfo(creator, t.Key, t.Value); if (trait != null) traits.Add(trait); } @@ -80,12 +81,16 @@ namespace OpenRA // HACK: The linter does not want to crash when a trait doesn't exist but only print an error instead // ObjectCreator will only return null to signal us to abort here if the linter is running - var info = creator.CreateObject(traitName + "Info"); + var traitInstance = traitName.Split(TraitInstanceSeparator); + var info = creator.CreateObject(traitInstance[0] + "Info"); if (info == null) return null; try { + if (traitInstance.Length > 1) + info.GetType().GetField("InstanceName").SetValue(info, traitInstance[1]); + FieldLoader.Load(info, my); } catch (FieldLoader.MissingFieldsException e) diff --git a/OpenRA.Game/Map/ActorInitializer.cs b/OpenRA.Game/Map/ActorInitializer.cs index ef26739e6a..5012907617 100644 --- a/OpenRA.Game/Map/ActorInitializer.cs +++ b/OpenRA.Game/Map/ActorInitializer.cs @@ -43,7 +43,17 @@ namespace OpenRA public T GetOrDefault(TraitInfo info) where T : ActorInit { - return Dict.GetOrDefault(); + var inits = Dict.WithInterface(); + + // Traits tagged with an instance name prefer inits with the same name. + // If a more specific init is not available, fall back to an unnamed init. + // If duplicate inits are defined, take the last to match standard yaml override expectations + if (info != null && !string.IsNullOrEmpty(info.InstanceName)) + return inits.LastOrDefault(i => i.InstanceName == info.InstanceName) ?? + inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName)); + + // Untagged traits will only use untagged inits + return inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName)); } public T Get(TraitInfo info) where T : ActorInit @@ -81,6 +91,16 @@ namespace OpenRA */ public abstract class ActorInit { + [FieldLoader.Ignore] + public readonly string InstanceName; + + protected ActorInit(string instanceName) + { + InstanceName = instanceName; + } + + protected ActorInit() { } + public abstract MiniYaml Save(); } @@ -88,7 +108,8 @@ namespace OpenRA { protected readonly T value; - protected ValueActorInit(TraitInfo info, T value) { this.value = value; } + protected ValueActorInit(TraitInfo info, T value) + : base(info.InstanceName) { this.value = value; } protected ValueActorInit(T value) { this.value = value; } diff --git a/OpenRA.Game/Map/ActorReference.cs b/OpenRA.Game/Map/ActorReference.cs index 02ad97775b..3aee55a924 100644 --- a/OpenRA.Game/Map/ActorReference.cs +++ b/OpenRA.Game/Map/ActorReference.cs @@ -47,14 +47,18 @@ namespace OpenRA static ActorInit LoadInit(string initName, MiniYaml initYaml) { - var type = Game.ModData.ObjectCreator.FindType(initName + "Init"); + var initInstance = initName.Split(ActorInfo.TraitInstanceSeparator); + var type = Game.ModData.ObjectCreator.FindType(initInstance[0] + "Init"); if (type == null) - throw new InvalidDataException("Unknown initializer type '{0}Init'".F(initName)); + throw new InvalidDataException("Unknown initializer type '{0}Init'".F(initInstance[0])); var init = (ActorInit)FormatterServices.GetUninitializedObject(type); + if (initInstance.Length > 1) + type.GetField("InstanceName").SetValue(init, initInstance[1]); + var loader = type.GetMethod("Initialize", new[] { typeof(MiniYaml) }); if (loader == null) - throw new InvalidDataException("{0}Init does not define a yaml-assignable type.".F(initName)); + throw new InvalidDataException("{0}Init does not define a yaml-assignable type.".F(initInstance[0])); loader.Invoke(init, new[] { initYaml }); return init; @@ -74,6 +78,9 @@ namespace OpenRA var initTypeName = init.GetType().Name; var initName = initTypeName.Substring(0, initTypeName.Length - 4); + if (!string.IsNullOrEmpty(init.InstanceName)) + initName += ActorInfo.TraitInstanceSeparator + init.InstanceName; + ret.Nodes.Add(new MiniYamlNode(initName, init.Save())); } diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs index 2d8432518c..30e6b1d2e4 100644 --- a/OpenRA.Game/Traits/TraitsInterfaces.cs +++ b/OpenRA.Game/Traits/TraitsInterfaces.cs @@ -327,6 +327,9 @@ namespace OpenRA.Traits public abstract class TraitInfo : ITraitInfoInterface { + // Value is set using reflection during TraitInfo creation + public readonly string InstanceName = null; + public abstract object Create(ActorInitializer init); } diff --git a/OpenRA.Mods.Common/Graphics/ActorPreview.cs b/OpenRA.Mods.Common/Graphics/ActorPreview.cs index 6a29a2cd0f..ddf7c7f5fb 100644 --- a/OpenRA.Mods.Common/Graphics/ActorPreview.cs +++ b/OpenRA.Mods.Common/Graphics/ActorPreview.cs @@ -44,7 +44,17 @@ namespace OpenRA.Mods.Common.Graphics public T GetOrDefault(TraitInfo info) where T : ActorInit { - return dict.GetOrDefault(); + var inits = dict.WithInterface(); + + // Traits tagged with an instance name prefer inits with the same name. + // If a more specific init is not available, fall back to an unnamed init. + // If duplicate inits are defined, take the last to match standard yaml override expectations + if (info != null && !string.IsNullOrEmpty(info.InstanceName)) + return inits.LastOrDefault(i => i.InstanceName == info.InstanceName) ?? + inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName)); + + // Untagged traits will only use untagged inits + return inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName)); } public T Get(TraitInfo info) where T : ActorInit diff --git a/OpenRA.Mods.Common/Scripting/Global/ActorGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/ActorGlobal.cs index 8662d50761..28e91362fb 100644 --- a/OpenRA.Mods.Common/Scripting/Global/ActorGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/ActorGlobal.cs @@ -30,12 +30,16 @@ namespace OpenRA.Mods.Common.Scripting ActorInit CreateInit(string initName, LuaValue value) { // Find the requested type - var initType = Game.ModData.ObjectCreator.FindType(initName + "Init"); + var initInstance = initName.Split(ActorInfo.TraitInstanceSeparator); + var initType = Game.ModData.ObjectCreator.FindType(initInstance[0] + "Init"); if (initType == null) - throw new LuaException("Unknown initializer type '{0}'".F(initName)); + throw new LuaException("Unknown initializer type '{0}'".F(initInstance[0])); // Construct the ActorInit. var init = (ActorInit)FormatterServices.GetUninitializedObject(initType); + if (initInstance.Length > 1) + initType.GetField("InstanceName").SetValue(init, initInstance[1]); + var initializers = initType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Where(m => m.Name == "Initialize" && m.GetParameters().Length == 1); @@ -55,7 +59,7 @@ namespace OpenRA.Mods.Common.Scripting } var types = initializers.Select(y => y.GetParameters()[0].ParameterType.Name).JoinWith(", "); - throw new LuaException("Invalid data type for '{0}' (expected one of {1})".F(initName, types)); + throw new LuaException("Invalid data type for '{0}' (expected one of {1})".F(initInstance[0], types)); } [Desc("Create a new actor. initTable specifies a list of key-value pairs that defines the initial parameters for the actor's traits.")]