#region Copyright & License Information /* * Copyright 2007-2020 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 System.Reflection; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA { public interface IActorInitializer { World World { get; } T GetOrDefault(TraitInfo info) where T : ActorInit; T Get(TraitInfo info) where T : ActorInit; U GetValue(TraitInfo info) where T : ValueActorInit; U GetValue(TraitInfo info, U fallback) where T : ValueActorInit; bool Contains(TraitInfo info) where T : ActorInit; T GetOrDefault() where T : ActorInit, ISingleInstanceInit; T Get() where T : ActorInit, ISingleInstanceInit; U GetValue() where T : ValueActorInit, ISingleInstanceInit; U GetValue(U fallback) where T : ValueActorInit, ISingleInstanceInit; bool Contains() where T : ActorInit, ISingleInstanceInit; } public class ActorInitializer : IActorInitializer { public readonly Actor Self; public World World { get { return Self.World; } } internal TypeDictionary Dict; public ActorInitializer(Actor actor, TypeDictionary dict) { Self = actor; Dict = dict; } public T GetOrDefault(TraitInfo info) where T : ActorInit { 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 { var init = GetOrDefault(info); if (init == null) throw new InvalidOperationException("TypeDictionary does not contain instance of type `{0}`".F(typeof(T))); return init; } public U GetValue(TraitInfo info) where T : ValueActorInit { return Get(info).Value; } public U GetValue(TraitInfo info, U fallback) where T : ValueActorInit { var init = GetOrDefault(info); return init != null ? init.Value : fallback; } public bool Contains(TraitInfo info) where T : ActorInit { return GetOrDefault(info) != null; } public T GetOrDefault() where T : ActorInit, ISingleInstanceInit { return Dict.GetOrDefault(); } public T Get() where T : ActorInit, ISingleInstanceInit { return Dict.Get(); } public U GetValue() where T : ValueActorInit, ISingleInstanceInit { return Get().Value; } public U GetValue(U fallback) where T : ValueActorInit, ISingleInstanceInit { var init = GetOrDefault(); return init != null ? init.Value : fallback; } public bool Contains() where T : ActorInit, ISingleInstanceInit { return GetOrDefault() != null; } } /* * Things to be aware of when writing ActorInits: * * - ActorReference and ActorGlobal can dynamically create objects without calling a constructor. * The object will be allocated directly then the best matching Initialize() method will be called to set valid state. * - ActorReference will always attempt to call Initialize(MiniYaml). ActorGlobal will use whichever one it first * finds with an argument type that matches the given LuaValue. * - Most ActorInits will want to inherit either ValueActorInit or CompositeActorInit which hide the low-level plumbing. * - Inits that reference actors should use ActorInitActorReference which allows actors to be referenced by name in map.yaml * - Inits that should only have a single instance defined on an actor should implement ISingleInstanceInit to allow * direct queries and runtime enforcement. * - Inits that aren't ISingleInstanceInit should expose a ctor that accepts a TraitInfo to allow per-trait targeting. */ public abstract class ActorInit { [FieldLoader.Ignore] public readonly string InstanceName; protected ActorInit(string instanceName) { InstanceName = instanceName; } protected ActorInit() { } public abstract MiniYaml Save(); } public interface ISingleInstanceInit { } public abstract class ValueActorInit : ActorInit { protected readonly T value; protected ValueActorInit(TraitInfo info, T value) : base(info.InstanceName) { this.value = value; } protected ValueActorInit(string instanceName, T value) : base(instanceName) { this.value = value; } protected ValueActorInit(T value) { this.value = value; } public virtual T Value { get { return value; } } public virtual void Initialize(MiniYaml yaml) { Initialize((T)FieldLoader.GetValue(nameof(value), typeof(T), yaml.Value)); } public virtual void Initialize(T value) { var field = GetType().GetField(nameof(value), BindingFlags.NonPublic | BindingFlags.Instance); if (field != null) field.SetValue(this, value); } public override MiniYaml Save() { return new MiniYaml(FieldSaver.FormatValue(value)); } } public abstract class CompositeActorInit : ActorInit { protected CompositeActorInit(TraitInfo info) : base(info.InstanceName) { } protected CompositeActorInit() : base() { } public virtual void Initialize(MiniYaml yaml) { FieldLoader.Load(this, yaml); } public virtual void Initialize(Dictionary values) { foreach (var field in GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { var sa = field.GetCustomAttributes(false).DefaultIfEmpty(FieldLoader.SerializeAttribute.Default).First(); if (!sa.Serialize) continue; if (values.TryGetValue(field.Name, out var value)) field.SetValue(this, value); } } public virtual Dictionary InitializeArgs() { var dict = new Dictionary(); foreach (var field in GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { var sa = field.GetCustomAttributes(false).DefaultIfEmpty(FieldLoader.SerializeAttribute.Default).First(); if (!sa.Serialize) continue; dict[field.Name] = field.FieldType; } return dict; } public override MiniYaml Save() { return FieldSaver.Save(this); } } public class LocationInit : ValueActorInit, ISingleInstanceInit { public LocationInit(CPos value) : base(value) { } } public class OwnerInit : ActorInit, ISingleInstanceInit { public readonly string InternalName; protected readonly Player value; public OwnerInit(Player value) { this.value = value; InternalName = value.InternalName; } public OwnerInit(string value) { InternalName = value; } public Player Value(World world) { return value ?? world.Players.First(x => x.InternalName == InternalName); } public void Initialize(MiniYaml yaml) { var field = GetType().GetField(nameof(InternalName), BindingFlags.Public | BindingFlags.Instance); if (field != null) field.SetValue(this, yaml.Value); } public void Initialize(Player player) { var field = GetType().GetField(nameof(value), BindingFlags.NonPublic | BindingFlags.Instance); if (field != null) field.SetValue(this, player); } public override MiniYaml Save() { return new MiniYaml(InternalName); } } public abstract class RuntimeFlagInit : ActorInit, ISuppressInitExport { public override MiniYaml Save() { throw new NotImplementedException("RuntimeFlagInit cannot be saved"); } } }