Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
201
OpenRA.Game/GameRules/ActorInfo.cs
Normal file
201
OpenRA.Game/GameRules/ActorInfo.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
#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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
/// <summary>
|
||||
/// A unit/building inside the game. Every rules starts with one and adds trait to it.
|
||||
/// </summary>
|
||||
public class ActorInfo
|
||||
{
|
||||
public const char AbstractActorPrefix = '^';
|
||||
public const char TraitInstanceSeparator = '@';
|
||||
|
||||
/// <summary>
|
||||
/// The actor name can be anything, but the sprites used in the Render*: traits default to this one.
|
||||
/// If you add an ^ in front of the name, the engine will recognize this as a collection of traits
|
||||
/// that can be inherited by others (using Inherits:) and not a real unit.
|
||||
/// You can remove inherited traits by adding a - in front of them as in -TraitName: to inherit everything, but this trait.
|
||||
/// </summary>
|
||||
public readonly string Name;
|
||||
readonly TypeDictionary traits = [];
|
||||
TraitInfo[] constructOrderCache = null;
|
||||
|
||||
public ActorInfo(ObjectCreator creator, string name, MiniYaml node)
|
||||
{
|
||||
try
|
||||
{
|
||||
Name = name;
|
||||
|
||||
foreach (var t in node.Nodes)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 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, t.Value);
|
||||
if (trait != null)
|
||||
traits.Add(trait);
|
||||
}
|
||||
catch (FieldLoader.MissingFieldsException e)
|
||||
{
|
||||
throw new YamlException(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
traits.TrimExcess();
|
||||
}
|
||||
catch (YamlException e)
|
||||
{
|
||||
throw new YamlException($"Actor type {name}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public ActorInfo(string name, params TraitInfo[] traitInfos)
|
||||
{
|
||||
Name = name;
|
||||
foreach (var t in traitInfos)
|
||||
traits.Add(t);
|
||||
traits.TrimExcess();
|
||||
}
|
||||
|
||||
static TraitInfo LoadTraitInfo(ObjectCreator creator, string traitName, MiniYaml my)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(my.Value))
|
||||
throw new YamlException($"Junk value `{my.Value}` on trait node {traitName}");
|
||||
|
||||
// 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 traitInstance = traitName.Split(TraitInstanceSeparator);
|
||||
var info = creator.CreateObject<TraitInfo>(traitInstance[0] + "Info");
|
||||
if (info == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
if (traitInstance.Length > 1)
|
||||
info.GetType().GetField(nameof(info.InstanceName)).SetValue(info, traitInstance[1]);
|
||||
|
||||
FieldLoader.Load(info, my);
|
||||
}
|
||||
catch (FieldLoader.MissingFieldsException e)
|
||||
{
|
||||
var header = "Trait name " + traitName + ": " + (e.Missing.Length > 1 ? "Required properties missing" : "Required property missing");
|
||||
throw new FieldLoader.MissingFieldsException(e.Missing, header);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public IEnumerable<TraitInfo> TraitsInConstructOrder()
|
||||
{
|
||||
if (constructOrderCache != null)
|
||||
return constructOrderCache;
|
||||
|
||||
var source = traits.WithInterface<TraitInfo>().Select(i => new
|
||||
{
|
||||
Trait = i,
|
||||
Type = i.GetType(),
|
||||
Dependencies = PrerequisitesOf(i).ToList(),
|
||||
OptionalDependencies = OptionalPrerequisitesOf(i).ToList()
|
||||
}).ToList();
|
||||
|
||||
var resolved = source.Where(s => s.Dependencies.Count == 0 && s.OptionalDependencies.Count == 0).ToList();
|
||||
var unresolved = source.ToHashSet();
|
||||
unresolved.ExceptWith(resolved);
|
||||
|
||||
static bool AreResolvable(Type a, Type b) => a.IsAssignableFrom(b);
|
||||
|
||||
// This query detects which unresolved traits can be immediately resolved as all their direct dependencies are met.
|
||||
var more = unresolved.Where(u =>
|
||||
u.Dependencies.All(d => // To be resolvable, all dependencies must be satisfied according to the following conditions:
|
||||
resolved.Exists(r => AreResolvable(d, r.Type)) && // There must exist a resolved trait that meets the dependency.
|
||||
!unresolved.Any(u1 => AreResolvable(d, u1.Type))) && // All matching traits that meet this dependency must be resolved first.
|
||||
u.OptionalDependencies.All(d => // To be resolvable, all optional dependencies must be satisfied according to the following condition:
|
||||
!unresolved.Any(u1 => AreResolvable(d, u1.Type)))); // All matching traits that meet this optional dependencies must be resolved first.
|
||||
|
||||
// Continue resolving traits as long as possible.
|
||||
// Each time we resolve some traits, this means dependencies for other traits may then be possible to satisfy in the next pass.
|
||||
#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection
|
||||
var readyToResolve = more.ToList();
|
||||
while (readyToResolve.Count != 0)
|
||||
{
|
||||
resolved.AddRange(readyToResolve);
|
||||
unresolved.ExceptWith(readyToResolve);
|
||||
readyToResolve.Clear();
|
||||
readyToResolve.AddRange(more);
|
||||
}
|
||||
#pragma warning restore CA1851
|
||||
|
||||
if (unresolved.Count != 0)
|
||||
{
|
||||
var exceptionString = "ActorInfo(\"" + Name + "\") failed to initialize because of the following:\n";
|
||||
var missing = unresolved.SelectMany(u => u.Dependencies.Where(d => !source.Any(s => AreResolvable(d, s.Type)))).Distinct();
|
||||
|
||||
exceptionString += "Missing:\n";
|
||||
foreach (var m in missing)
|
||||
exceptionString += m + " \n";
|
||||
|
||||
exceptionString += "Unresolved:\n";
|
||||
foreach (var u in unresolved)
|
||||
{
|
||||
var deps = u.Dependencies.Where(d => !resolved.Exists(r => r.Type == d));
|
||||
var optDeps = u.OptionalDependencies.Where(d => !resolved.Exists(r => r.Type == d));
|
||||
var allDeps = string.Join(", ", deps.Select(o => o.ToString()).Concat(optDeps.Select(o => $"[{o}]")));
|
||||
exceptionString += $"{u.Type}: {{ {allDeps} }}\n";
|
||||
}
|
||||
|
||||
throw new YamlException(exceptionString);
|
||||
}
|
||||
|
||||
constructOrderCache = resolved.Select(r => r.Trait).ToArray();
|
||||
return constructOrderCache;
|
||||
}
|
||||
|
||||
public static IEnumerable<Type> PrerequisitesOf(TraitInfo info)
|
||||
{
|
||||
return info
|
||||
.GetType()
|
||||
.GetInterfaces()
|
||||
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Requires<>))
|
||||
.Select(t => t.GetGenericArguments()[0]);
|
||||
}
|
||||
|
||||
public static IEnumerable<Type> OptionalPrerequisitesOf(TraitInfo info)
|
||||
{
|
||||
return info
|
||||
.GetType()
|
||||
.GetInterfaces()
|
||||
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(NotBefore<>))
|
||||
.Select(t => t.GetGenericArguments()[0]);
|
||||
}
|
||||
|
||||
public bool HasTraitInfo<T>() where T : ITraitInfoInterface { return traits.Contains<T>(); }
|
||||
public T TraitInfo<T>() where T : ITraitInfoInterface { return traits.Get<T>(); }
|
||||
public T TraitInfoOrDefault<T>() where T : ITraitInfoInterface { return traits.GetOrDefault<T>(); }
|
||||
public IReadOnlyCollection<T> TraitInfos<T>() where T : ITraitInfoInterface { return traits.WithInterface<T>(); }
|
||||
|
||||
public BitSet<TargetableType> GetAllTargetTypes()
|
||||
{
|
||||
// PERF: Avoid LINQ.
|
||||
var targetTypes = default(BitSet<TargetableType>);
|
||||
foreach (var targetable in TraitInfos<ITargetableInfo>())
|
||||
targetTypes = targetTypes.Union(targetable.GetTargetTypes());
|
||||
return targetTypes;
|
||||
}
|
||||
}
|
||||
}
|
||||
65
OpenRA.Game/GameRules/MusicInfo.cs
Normal file
65
OpenRA.Game/GameRules/MusicInfo.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
#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.FileSystem;
|
||||
|
||||
namespace OpenRA.GameRules
|
||||
{
|
||||
public class MusicInfo
|
||||
{
|
||||
public readonly string Filename;
|
||||
public readonly string Title;
|
||||
public readonly bool Hidden;
|
||||
public readonly float VolumeModifier = 1f;
|
||||
|
||||
public int Length { get; private set; } // seconds
|
||||
public bool Exists { get; private set; }
|
||||
|
||||
public MusicInfo(string key, MiniYaml value)
|
||||
{
|
||||
Title = value.Value;
|
||||
|
||||
var nd = value.ToDictionary();
|
||||
if (nd.TryGetValue("Hidden", out var yaml))
|
||||
bool.TryParse(yaml.Value, out Hidden);
|
||||
|
||||
if (nd.TryGetValue("VolumeModifier", out yaml))
|
||||
VolumeModifier = FieldLoader.GetValue<float>("VolumeModifier", yaml.Value);
|
||||
|
||||
var ext = nd.TryGetValue("Extension", out yaml) ? yaml.Value : "aud";
|
||||
Filename = (nd.TryGetValue("Filename", out yaml) ? yaml.Value : key) + "." + ext;
|
||||
}
|
||||
|
||||
public void Load(IReadOnlyFileSystem fileSystem)
|
||||
{
|
||||
if (!fileSystem.TryOpen(Filename, out var stream))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Exists = true;
|
||||
foreach (var loader in Game.ModData.SoundLoaders)
|
||||
{
|
||||
if (loader.TryParseSound(stream, out var soundFormat))
|
||||
{
|
||||
Length = (int)soundFormat.LengthInSeconds;
|
||||
soundFormat.Dispose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
stream.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
281
OpenRA.Game/GameRules/Ruleset.cs
Normal file
281
OpenRA.Game/GameRules/Ruleset.cs
Normal file
@@ -0,0 +1,281 @@
|
||||
#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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.GameRules;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public class Ruleset
|
||||
{
|
||||
public readonly ActorInfoDictionary Actors;
|
||||
public readonly IReadOnlyDictionary<string, WeaponInfo> Weapons;
|
||||
public readonly IReadOnlyDictionary<string, SoundInfo> Voices;
|
||||
public readonly IReadOnlyDictionary<string, SoundInfo> Notifications;
|
||||
public readonly IReadOnlyDictionary<string, MusicInfo> Music;
|
||||
public readonly ITerrainInfo TerrainInfo;
|
||||
public readonly IReadOnlyDictionary<string, MiniYamlNode> ModelSequences;
|
||||
|
||||
public Ruleset(
|
||||
IReadOnlyDictionary<string, ActorInfo> actors,
|
||||
IReadOnlyDictionary<string, WeaponInfo> weapons,
|
||||
IReadOnlyDictionary<string, SoundInfo> voices,
|
||||
IReadOnlyDictionary<string, SoundInfo> notifications,
|
||||
IReadOnlyDictionary<string, MusicInfo> music,
|
||||
ITerrainInfo terrainInfo,
|
||||
IReadOnlyDictionary<string, MiniYamlNode> modelSequences)
|
||||
{
|
||||
Actors = new ActorInfoDictionary(actors);
|
||||
Weapons = weapons;
|
||||
Voices = voices;
|
||||
Notifications = notifications;
|
||||
Music = music;
|
||||
TerrainInfo = terrainInfo;
|
||||
ModelSequences = modelSequences;
|
||||
|
||||
foreach (var a in Actors.Values)
|
||||
{
|
||||
foreach (var t in a.TraitInfos<IRulesetLoaded>())
|
||||
{
|
||||
try
|
||||
{
|
||||
t.RulesetLoaded(this, a);
|
||||
}
|
||||
catch (YamlException e)
|
||||
{
|
||||
throw new YamlException($"Actor type {a.Name}: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var weapon in Weapons)
|
||||
{
|
||||
if (weapon.Value.Projectile is IRulesetLoaded<WeaponInfo> projectileLoaded)
|
||||
{
|
||||
try
|
||||
{
|
||||
projectileLoaded.RulesetLoaded(this, weapon.Value);
|
||||
}
|
||||
catch (YamlException e)
|
||||
{
|
||||
throw new YamlException($"Projectile type {weapon.Key}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var warhead in weapon.Value.Warheads)
|
||||
{
|
||||
if (warhead is IRulesetLoaded<WeaponInfo> cacher)
|
||||
{
|
||||
try
|
||||
{
|
||||
cacher.RulesetLoaded(this, weapon.Value);
|
||||
}
|
||||
catch (YamlException e)
|
||||
{
|
||||
throw new YamlException($"Weapon type {weapon.Key}: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<KeyValuePair<string, MusicInfo>> InstalledMusic { get { return Music.Where(m => m.Value.Exists); } }
|
||||
|
||||
static IReadOnlyDictionary<string, T> MergeOrDefault<T>(string name,
|
||||
IReadOnlyFileSystem fileSystem,
|
||||
IEnumerable<string> files,
|
||||
MiniYaml additional,
|
||||
IReadOnlyDictionary<string, T> defaults,
|
||||
Func<MiniYamlNode, T> makeObject,
|
||||
Func<MiniYamlNode, bool> filterNode = null)
|
||||
{
|
||||
if (additional == null && defaults != null)
|
||||
return defaults;
|
||||
|
||||
IEnumerable<MiniYamlNode> yamlNodes = MiniYaml.Load(fileSystem, files, additional);
|
||||
|
||||
// Optionally, the caller can filter out elements from the loaded set of nodes. Default behavior is unfiltered.
|
||||
if (filterNode != null)
|
||||
yamlNodes = yamlNodes.Where(k => !filterNode(k));
|
||||
|
||||
return yamlNodes.ToDictionaryWithConflictLog(k => k.Key.ToLowerInvariant(), makeObject, "LoadFromManifest<" + name + ">");
|
||||
}
|
||||
|
||||
public static Ruleset LoadDefaults(ModData modData)
|
||||
{
|
||||
var m = modData.Manifest;
|
||||
var fs = modData.DefaultFileSystem;
|
||||
|
||||
Ruleset ruleset = null;
|
||||
void LoadRuleset()
|
||||
{
|
||||
var actors = MergeOrDefault("Manifest,Rules", fs, m.Rules, null, null,
|
||||
k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value),
|
||||
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix));
|
||||
|
||||
var weapons = MergeOrDefault("Manifest,Weapons", fs, m.Weapons, null, null,
|
||||
k => new WeaponInfo(k.Value));
|
||||
|
||||
var voices = MergeOrDefault("Manifest,Voices", fs, m.Voices, null, null,
|
||||
k => new SoundInfo(k.Value));
|
||||
|
||||
var notifications = MergeOrDefault("Manifest,Notifications", fs, m.Notifications, null, null,
|
||||
k => new SoundInfo(k.Value));
|
||||
|
||||
var music = MergeOrDefault("Manifest,Music", fs, m.Music, null, null,
|
||||
k => new MusicInfo(k.Key, k.Value));
|
||||
|
||||
var modelSequences = MergeOrDefault("Manifest,ModelSequences", fs, m.ModelSequences, null, null,
|
||||
k => k);
|
||||
|
||||
// The default ruleset does not include a preferred tileset
|
||||
ruleset = new Ruleset(actors, weapons, voices, notifications, music, null, modelSequences);
|
||||
}
|
||||
|
||||
if (modData.IsOnMainThread)
|
||||
{
|
||||
modData.HandleLoadingProgress();
|
||||
|
||||
var loader = new Task(LoadRuleset);
|
||||
loader.Start();
|
||||
|
||||
// Animate the loadscreen while we wait
|
||||
while (!loader.Wait(40))
|
||||
modData.HandleLoadingProgress();
|
||||
}
|
||||
else
|
||||
LoadRuleset();
|
||||
|
||||
return ruleset;
|
||||
}
|
||||
|
||||
public static Ruleset LoadDefaultsForTileSet(ModData modData, string tileSet)
|
||||
{
|
||||
var dr = modData.DefaultRules;
|
||||
var terrainInfo = modData.DefaultTerrainInfo[tileSet];
|
||||
|
||||
return new Ruleset(dr.Actors, dr.Weapons, dr.Voices, dr.Notifications, dr.Music, terrainInfo, dr.ModelSequences);
|
||||
}
|
||||
|
||||
public static Ruleset Load(ModData modData, IReadOnlyFileSystem fileSystem, string tileSet,
|
||||
MiniYaml mapRules, MiniYaml mapWeapons, MiniYaml mapVoices, MiniYaml mapNotifications,
|
||||
MiniYaml mapMusic, MiniYaml mapModelSequences)
|
||||
{
|
||||
var m = modData.Manifest;
|
||||
var dr = modData.DefaultRules;
|
||||
|
||||
Ruleset ruleset = null;
|
||||
void LoadRuleset()
|
||||
{
|
||||
var actors = MergeOrDefault("Rules", fileSystem, m.Rules, mapRules, dr.Actors,
|
||||
k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value),
|
||||
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix));
|
||||
|
||||
var weapons = MergeOrDefault("Weapons", fileSystem, m.Weapons, mapWeapons, dr.Weapons,
|
||||
k => new WeaponInfo(k.Value));
|
||||
|
||||
var voices = MergeOrDefault("Voices", fileSystem, m.Voices, mapVoices, dr.Voices,
|
||||
k => new SoundInfo(k.Value));
|
||||
|
||||
var notifications = MergeOrDefault("Notifications", fileSystem, m.Notifications, mapNotifications, dr.Notifications,
|
||||
k => new SoundInfo(k.Value));
|
||||
|
||||
var music = MergeOrDefault("Music", fileSystem, m.Music, mapMusic, dr.Music,
|
||||
k => new MusicInfo(k.Key, k.Value));
|
||||
|
||||
// TODO: Add support for merging custom terrain modifications
|
||||
var terrainInfo = modData.DefaultTerrainInfo[tileSet];
|
||||
|
||||
var modelSequences = dr.ModelSequences;
|
||||
if (mapModelSequences != null)
|
||||
modelSequences = MergeOrDefault("ModelSequences", fileSystem, m.ModelSequences, mapModelSequences, dr.ModelSequences,
|
||||
k => k);
|
||||
|
||||
ruleset = new Ruleset(actors, weapons, voices, notifications, music, terrainInfo, modelSequences);
|
||||
}
|
||||
|
||||
if (modData.IsOnMainThread)
|
||||
{
|
||||
modData.HandleLoadingProgress();
|
||||
|
||||
var loader = new Task(LoadRuleset);
|
||||
loader.Start();
|
||||
|
||||
// Animate the loadscreen while we wait
|
||||
while (!loader.Wait(40))
|
||||
modData.HandleLoadingProgress();
|
||||
}
|
||||
else
|
||||
LoadRuleset();
|
||||
|
||||
return ruleset;
|
||||
}
|
||||
|
||||
static bool AnyCustomYaml(MiniYaml yaml)
|
||||
{
|
||||
return yaml != null && (yaml.Value != null || yaml.Nodes.Length > 0);
|
||||
}
|
||||
|
||||
static bool AnyFlaggedTraits(ModData modData, IEnumerable<MiniYamlNode> actors)
|
||||
{
|
||||
foreach (var actorNode in actors)
|
||||
{
|
||||
foreach (var traitNode in actorNode.Value.Nodes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var traitName = traitNode.Key.Split('@')[0];
|
||||
var traitType = modData.ObjectCreator.FindType(traitName + "Info");
|
||||
if (traitType != null && traitType.GetInterface(nameof(ILobbyCustomRulesIgnore)) == null)
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Write("debug", "Error in AnyFlaggedTraits\n" + ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool DefinesUnsafeCustomRules(ModData modData, IReadOnlyFileSystem fileSystem,
|
||||
MiniYaml mapRules, MiniYaml mapWeapons, MiniYaml mapVoices, MiniYaml mapNotifications, MiniYaml mapSequences)
|
||||
{
|
||||
// Maps that define any weapon, voice, notification, or sequence overrides are always flagged
|
||||
if (AnyCustomYaml(mapWeapons) || AnyCustomYaml(mapVoices) || AnyCustomYaml(mapNotifications) || AnyCustomYaml(mapSequences))
|
||||
return true;
|
||||
|
||||
// Any trait overrides that aren't explicitly whitelisted are flagged
|
||||
if (mapRules == null)
|
||||
return false;
|
||||
|
||||
if (AnyFlaggedTraits(modData, mapRules.Nodes))
|
||||
return true;
|
||||
|
||||
if (mapRules.Value != null)
|
||||
{
|
||||
var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", mapRules.Value);
|
||||
foreach (var f in mapFiles)
|
||||
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
97
OpenRA.Game/GameRules/SoundInfo.cs
Normal file
97
OpenRA.Game/GameRules/SoundInfo.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
#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 System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace OpenRA.GameRules
|
||||
{
|
||||
public class SoundInfo
|
||||
{
|
||||
public readonly FrozenDictionary<string, ImmutableArray<string>> Variants = FrozenDictionary<string, ImmutableArray<string>>.Empty;
|
||||
public readonly FrozenDictionary<string, ImmutableArray<string>> Prefixes = FrozenDictionary<string, ImmutableArray<string>>.Empty;
|
||||
public readonly FrozenDictionary<string, ImmutableArray<string>> Voices = FrozenDictionary<string, ImmutableArray<string>>.Empty;
|
||||
public readonly FrozenDictionary<string, ImmutableArray<string>> Notifications = FrozenDictionary<string, ImmutableArray<string>>.Empty;
|
||||
public readonly string DefaultVariant = ".aud";
|
||||
public readonly string DefaultPrefix = "";
|
||||
public readonly FrozenSet<string> DisableVariants = FrozenSet<string>.Empty;
|
||||
public readonly FrozenSet<string> DisablePrefixes = FrozenSet<string>.Empty;
|
||||
|
||||
public readonly Lazy<FrozenDictionary<string, SoundPool>> VoicePools;
|
||||
public readonly Lazy<FrozenDictionary<string, SoundPool>> NotificationsPools;
|
||||
|
||||
public SoundInfo(MiniYaml y)
|
||||
{
|
||||
FieldLoader.Load(this, y);
|
||||
|
||||
VoicePools = Exts.Lazy(() => Voices.ToFrozenDictionary(a => a.Key, a => new SoundPool(1f, SoundPool.DefaultInterruptType, a.Value)));
|
||||
NotificationsPools = Exts.Lazy(() => ParseSoundPool(y, "Notifications"));
|
||||
}
|
||||
|
||||
static FrozenDictionary<string, SoundPool> ParseSoundPool(MiniYaml y, string key)
|
||||
{
|
||||
var classifiction = y.NodeWithKey(key);
|
||||
var ret = new Dictionary<string, SoundPool>(classifiction.Value.Nodes.Length);
|
||||
foreach (var t in classifiction.Value.Nodes)
|
||||
{
|
||||
var volumeModifier = 1f;
|
||||
var volumeModifierNode = t.Value.NodeWithKeyOrDefault(nameof(SoundPool.VolumeModifier));
|
||||
if (volumeModifierNode != null)
|
||||
volumeModifier = FieldLoader.GetValue<float>(volumeModifierNode.Key, volumeModifierNode.Value.Value);
|
||||
|
||||
var interruptType = SoundPool.DefaultInterruptType;
|
||||
var interruptTypeNode = t.Value.NodeWithKeyOrDefault(nameof(SoundPool.InterruptType));
|
||||
if (interruptTypeNode != null)
|
||||
interruptType = FieldLoader.GetValue<SoundPool.InterruptType>(interruptTypeNode.Key, interruptTypeNode.Value.Value);
|
||||
|
||||
var names = FieldLoader.GetValue<ImmutableArray<string>>(t.Key, t.Value.Value);
|
||||
var sp = new SoundPool(volumeModifier, interruptType, names);
|
||||
ret.Add(t.Key, sp);
|
||||
}
|
||||
|
||||
return ret.ToFrozenDictionary();
|
||||
}
|
||||
}
|
||||
|
||||
public class SoundPool
|
||||
{
|
||||
public enum InterruptType { DoNotPlay, Interrupt, Overlap }
|
||||
public const InterruptType DefaultInterruptType = InterruptType.DoNotPlay;
|
||||
public readonly float VolumeModifier;
|
||||
public readonly InterruptType Type;
|
||||
readonly ImmutableArray<string> clips;
|
||||
readonly List<string> liveclips = [];
|
||||
|
||||
public SoundPool(float volumeModifier, InterruptType interruptType, ImmutableArray<string> clips)
|
||||
{
|
||||
VolumeModifier = volumeModifier;
|
||||
Type = interruptType;
|
||||
this.clips = clips;
|
||||
}
|
||||
|
||||
public string GetNext()
|
||||
{
|
||||
if (liveclips.Count == 0)
|
||||
liveclips.AddRange(clips);
|
||||
|
||||
// Avoid crashing if there's no clips at all
|
||||
if (liveclips.Count == 0)
|
||||
return null;
|
||||
|
||||
var i = Game.CosmeticRandom.Next(liveclips.Count);
|
||||
var s = liveclips[i];
|
||||
liveclips.RemoveAt(i);
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
268
OpenRA.Game/GameRules/WeaponInfo.cs
Normal file
268
OpenRA.Game/GameRules/WeaponInfo.cs
Normal file
@@ -0,0 +1,268 @@
|
||||
#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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using OpenRA.Effects;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.GameRules
|
||||
{
|
||||
public class ProjectileArgs
|
||||
{
|
||||
public WeaponInfo Weapon;
|
||||
public int[] DamageModifiers;
|
||||
public int[] InaccuracyModifiers;
|
||||
public int[] RangeModifiers;
|
||||
public WAngle Facing;
|
||||
public Func<WAngle> CurrentMuzzleFacing;
|
||||
public WPos Source;
|
||||
public Func<WPos> CurrentSource;
|
||||
public Actor SourceActor;
|
||||
public WPos PassiveTarget;
|
||||
public Target GuidedTarget;
|
||||
}
|
||||
|
||||
public class WarheadArgs
|
||||
{
|
||||
public WeaponInfo Weapon;
|
||||
public int[] DamageModifiers = [];
|
||||
public WPos? Source;
|
||||
public WRot ImpactOrientation;
|
||||
public WPos ImpactPosition;
|
||||
public Actor SourceActor;
|
||||
public Target WeaponTarget;
|
||||
|
||||
public WarheadArgs(ProjectileArgs args)
|
||||
{
|
||||
Weapon = args.Weapon;
|
||||
DamageModifiers = args.DamageModifiers;
|
||||
ImpactPosition = args.PassiveTarget;
|
||||
Source = args.Source;
|
||||
SourceActor = args.SourceActor;
|
||||
WeaponTarget = args.GuidedTarget;
|
||||
}
|
||||
|
||||
// For places that only want to update some of the fields (usually DamageModifiers)
|
||||
public WarheadArgs(WarheadArgs args)
|
||||
{
|
||||
Weapon = args.Weapon;
|
||||
DamageModifiers = args.DamageModifiers;
|
||||
Source = args.Source;
|
||||
SourceActor = args.SourceActor;
|
||||
WeaponTarget = args.WeaponTarget;
|
||||
}
|
||||
|
||||
// Default empty constructor for callers that want to initialize fields themselves
|
||||
public WarheadArgs() { }
|
||||
}
|
||||
|
||||
public interface IProjectile : IEffect { }
|
||||
public interface IProjectileInfo { IProjectile Create(ProjectileArgs args); }
|
||||
|
||||
public sealed class WeaponInfo
|
||||
{
|
||||
[Desc("The maximum range the weapon can fire.")]
|
||||
public readonly WDist Range = WDist.Zero;
|
||||
|
||||
[Desc("First burst is aimed at this offset relative to target position.")]
|
||||
public readonly WVec FirstBurstTargetOffset = WVec.Zero;
|
||||
|
||||
[Desc("Each burst after the first lands by this offset away from the previous burst.")]
|
||||
public readonly WVec FollowingBurstTargetOffset = WVec.Zero;
|
||||
|
||||
[Desc("The sound played each time the weapon is fired.")]
|
||||
public readonly ImmutableArray<string> Report = default;
|
||||
|
||||
[Desc("Sound played only on first burst in a salvo.")]
|
||||
public readonly ImmutableArray<string> StartBurstReport = default;
|
||||
|
||||
[Desc("The sound played when the weapon is reloaded.")]
|
||||
public readonly ImmutableArray<string> AfterFireSound = default;
|
||||
|
||||
[Desc("Delay in ticks to play reloading sound.")]
|
||||
public readonly int AfterFireSoundDelay = 0;
|
||||
|
||||
[Desc("Delay in ticks between reloading ammo magazines.")]
|
||||
public readonly int ReloadDelay = 1;
|
||||
|
||||
[Desc("Number of shots in a single ammo magazine.")]
|
||||
public readonly int Burst = 1;
|
||||
|
||||
[Desc("Can this weapon target the attacker itself?")]
|
||||
public readonly bool CanTargetSelf = false;
|
||||
|
||||
[Desc("What types of targets are affected.")]
|
||||
public readonly BitSet<TargetableType> ValidTargets = new("Ground", "Water");
|
||||
|
||||
[Desc("What types of targets are unaffected.", "Overrules ValidTargets.")]
|
||||
public readonly BitSet<TargetableType> InvalidTargets;
|
||||
|
||||
static readonly BitSet<TargetableType> TargetTypeAir = new("Air");
|
||||
|
||||
[Desc("If weapon is not directly targeting an actor and targeted position is above this altitude,",
|
||||
"the weapon will ignore terrain target types and only check TargetTypeAir for validity.")]
|
||||
public readonly WDist AirThreshold = new(128);
|
||||
|
||||
[Desc("Delay in ticks between firing shots from the same ammo magazine. If one entry, it will be used for all bursts.",
|
||||
"If multiple entries, their number needs to match Burst - 1.")]
|
||||
public readonly ImmutableArray<int> BurstDelays = [5];
|
||||
|
||||
[Desc("The minimum range the weapon can fire.")]
|
||||
public readonly WDist MinRange = WDist.Zero;
|
||||
|
||||
[Desc("Does this weapon aim at the target's center regardless of other targetable offsets?")]
|
||||
public readonly bool TargetActorCenter = false;
|
||||
|
||||
[FieldLoader.LoadUsing(nameof(LoadProjectile))]
|
||||
public readonly IProjectileInfo Projectile;
|
||||
|
||||
[FieldLoader.LoadUsing(nameof(LoadWarheads))]
|
||||
public readonly ImmutableArray<IWarhead> Warheads = [];
|
||||
|
||||
/// <summary>
|
||||
/// This constructor is used solely for documentation generation.
|
||||
/// </summary>
|
||||
public WeaponInfo() { }
|
||||
|
||||
public WeaponInfo(MiniYaml content)
|
||||
{
|
||||
// Resolve any weapon-level yaml inheritance or removals
|
||||
// HACK: The "Defaults" sequence syntax prevents us from doing this generally during yaml parsing
|
||||
content = content.WithNodes(MiniYaml.Merge([content.Nodes]));
|
||||
FieldLoader.Load(this, content);
|
||||
}
|
||||
|
||||
static object LoadProjectile(MiniYaml yaml)
|
||||
{
|
||||
var proj = yaml.NodeWithKeyOrDefault("Projectile")?.Value;
|
||||
if (proj == null)
|
||||
return null;
|
||||
|
||||
var ret = Game.CreateObject<IProjectileInfo>(proj.Value + "Info");
|
||||
if (ret == null)
|
||||
return null;
|
||||
|
||||
FieldLoader.Load(ret, proj);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static object LoadWarheads(MiniYaml yaml)
|
||||
{
|
||||
var retList = new List<IWarhead>();
|
||||
foreach (var node in yaml.Nodes.Where(n => n.Key.StartsWith("Warhead", StringComparison.Ordinal)))
|
||||
{
|
||||
var ret = Game.CreateObject<IWarhead>(node.Value.Value + "Warhead");
|
||||
if (ret == null)
|
||||
continue;
|
||||
|
||||
FieldLoader.Load(ret, node.Value);
|
||||
retList.Add(ret);
|
||||
}
|
||||
|
||||
return retList.ToImmutableArray();
|
||||
}
|
||||
|
||||
public bool IsValidTarget(BitSet<TargetableType> targetTypes)
|
||||
{
|
||||
return ValidTargets.Overlaps(targetTypes) && !InvalidTargets.Overlaps(targetTypes);
|
||||
}
|
||||
|
||||
/// <summary>Checks if the weapon is valid against (can target) the target.</summary>
|
||||
public bool IsValidAgainst(in Target target, World world, Actor firedBy)
|
||||
{
|
||||
if (target.Type == TargetType.Actor)
|
||||
return IsValidAgainst(target.Actor, firedBy);
|
||||
|
||||
if (target.Type == TargetType.FrozenActor)
|
||||
return IsValidAgainst(target.FrozenActor, firedBy);
|
||||
|
||||
if (target.Type == TargetType.Terrain)
|
||||
{
|
||||
var dat = world.Map.DistanceAboveTerrain(target.CenterPosition);
|
||||
if (dat > AirThreshold)
|
||||
return IsValidTarget(TargetTypeAir);
|
||||
|
||||
var cell = world.Map.CellContaining(target.CenterPosition);
|
||||
if (!world.Map.Contains(cell))
|
||||
return false;
|
||||
|
||||
var cellInfo = world.Map.GetTerrainInfo(cell);
|
||||
if (!IsValidTarget(cellInfo.TargetTypes))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Checks if the weapon is valid against (can target) the actor.</summary>
|
||||
public bool IsValidAgainst(Actor victim, Actor firedBy)
|
||||
{
|
||||
if (!CanTargetSelf && victim == firedBy)
|
||||
return false;
|
||||
|
||||
var targetTypes = victim.GetEnabledTargetTypes();
|
||||
|
||||
return IsValidTarget(targetTypes);
|
||||
}
|
||||
|
||||
/// <summary>Checks if the weapon is valid against (can target) the frozen actor.</summary>
|
||||
public bool IsValidAgainst(FrozenActor victim, Actor firedBy)
|
||||
{
|
||||
if (!victim.IsValid)
|
||||
return false;
|
||||
|
||||
if (!CanTargetSelf && victim.Actor == firedBy)
|
||||
return false;
|
||||
|
||||
return IsValidTarget(victim.TargetTypes);
|
||||
}
|
||||
|
||||
/// <summary>Applies all the weapon's warheads to the target.</summary>
|
||||
public void Impact(in Target target, WarheadArgs args)
|
||||
{
|
||||
var world = args.SourceActor.World;
|
||||
foreach (var warhead in Warheads)
|
||||
{
|
||||
if (warhead.Delay > 0)
|
||||
{
|
||||
// Lambdas can't use 'in' variables, so capture a copy for later
|
||||
var delayedTarget = target;
|
||||
world.AddFrameEndTask(w => w.Add(new DelayedImpact(warhead.Delay, warhead, delayedTarget, args)));
|
||||
}
|
||||
else
|
||||
warhead.DoImpact(target, args);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Applies all the weapon's warheads to the target. Only use for projectile-less, special-case impacts.</summary>
|
||||
public void Impact(in Target target, Actor firedBy)
|
||||
{
|
||||
// The impact will happen immediately at target.CenterPosition.
|
||||
var args = new WarheadArgs
|
||||
{
|
||||
Weapon = this,
|
||||
SourceActor = firedBy,
|
||||
WeaponTarget = target
|
||||
};
|
||||
|
||||
if (firedBy.OccupiesSpace != null)
|
||||
args.Source = firedBy.CenterPosition;
|
||||
|
||||
Impact(target, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user