Rewrite sequence loading logic.

Multiple layers of Lazy<T>ness are replaced with
an explicit two-part loading scheme.

Sequences are parsed immediately, without the need
for the sprite assets, and tell the SpriteCache
which frames they need. Use-cases that want the
actual sprites can then tell the SpriteCache to
load the frames and the sequences to resolve the
sprites.
This commit is contained in:
Paul Chote
2023-03-09 20:39:17 +00:00
committed by Gustas
parent 1f3403717b
commit c35ab081ff
21 changed files with 780 additions and 632 deletions

View File

@@ -15,7 +15,6 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenRA.FileSystem; using OpenRA.FileSystem;
using OpenRA.GameRules; using OpenRA.GameRules;
using OpenRA.Graphics;
using OpenRA.Traits; using OpenRA.Traits;
namespace OpenRA namespace OpenRA
@@ -28,7 +27,6 @@ namespace OpenRA
public readonly IReadOnlyDictionary<string, SoundInfo> Notifications; public readonly IReadOnlyDictionary<string, SoundInfo> Notifications;
public readonly IReadOnlyDictionary<string, MusicInfo> Music; public readonly IReadOnlyDictionary<string, MusicInfo> Music;
public readonly ITerrainInfo TerrainInfo; public readonly ITerrainInfo TerrainInfo;
public readonly SequenceProvider Sequences;
public readonly IReadOnlyDictionary<string, MiniYamlNode> ModelSequences; public readonly IReadOnlyDictionary<string, MiniYamlNode> ModelSequences;
public Ruleset( public Ruleset(
@@ -38,7 +36,6 @@ namespace OpenRA
IReadOnlyDictionary<string, SoundInfo> notifications, IReadOnlyDictionary<string, SoundInfo> notifications,
IReadOnlyDictionary<string, MusicInfo> music, IReadOnlyDictionary<string, MusicInfo> music,
ITerrainInfo terrainInfo, ITerrainInfo terrainInfo,
SequenceProvider sequences,
IReadOnlyDictionary<string, MiniYamlNode> modelSequences) IReadOnlyDictionary<string, MiniYamlNode> modelSequences)
{ {
Actors = new ActorInfoDictionary(actors); Actors = new ActorInfoDictionary(actors);
@@ -47,7 +44,6 @@ namespace OpenRA
Notifications = notifications; Notifications = notifications;
Music = music; Music = music;
TerrainInfo = terrainInfo; TerrainInfo = terrainInfo;
Sequences = sequences;
ModelSequences = modelSequences; ModelSequences = modelSequences;
foreach (var a in Actors.Values) foreach (var a in Actors.Values)
@@ -145,8 +141,8 @@ namespace OpenRA
var modelSequences = MergeOrDefault("Manifest,ModelSequences", fs, m.ModelSequences, null, null, var modelSequences = MergeOrDefault("Manifest,ModelSequences", fs, m.ModelSequences, null, null,
k => k); k => k);
// The default ruleset does not include a preferred tileset or sequence set // The default ruleset does not include a preferred tileset
ruleset = new Ruleset(actors, weapons, voices, notifications, music, null, null, modelSequences); ruleset = new Ruleset(actors, weapons, voices, notifications, music, null, modelSequences);
} }
if (modData.IsOnMainThread) if (modData.IsOnMainThread)
@@ -170,14 +166,13 @@ namespace OpenRA
{ {
var dr = modData.DefaultRules; var dr = modData.DefaultRules;
var terrainInfo = modData.DefaultTerrainInfo[tileSet]; var terrainInfo = modData.DefaultTerrainInfo[tileSet];
var sequences = modData.DefaultSequences[tileSet];
return new Ruleset(dr.Actors, dr.Weapons, dr.Voices, dr.Notifications, dr.Music, terrainInfo, sequences, dr.ModelSequences); 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, public static Ruleset Load(ModData modData, IReadOnlyFileSystem fileSystem, string tileSet,
MiniYaml mapRules, MiniYaml mapWeapons, MiniYaml mapVoices, MiniYaml mapNotifications, MiniYaml mapRules, MiniYaml mapWeapons, MiniYaml mapVoices, MiniYaml mapNotifications,
MiniYaml mapMusic, MiniYaml mapSequences, MiniYaml mapModelSequences) MiniYaml mapMusic, MiniYaml mapModelSequences)
{ {
var m = modData.Manifest; var m = modData.Manifest;
var dr = modData.DefaultRules; var dr = modData.DefaultRules;
@@ -204,16 +199,12 @@ namespace OpenRA
// TODO: Add support for merging custom terrain modifications // TODO: Add support for merging custom terrain modifications
var terrainInfo = modData.DefaultTerrainInfo[tileSet]; var terrainInfo = modData.DefaultTerrainInfo[tileSet];
// TODO: Top-level dictionary should be moved into the Ruleset instead of in its own object
var sequences = mapSequences == null ? modData.DefaultSequences[tileSet] :
new SequenceProvider(fileSystem, modData, tileSet, mapSequences);
var modelSequences = dr.ModelSequences; var modelSequences = dr.ModelSequences;
if (mapModelSequences != null) if (mapModelSequences != null)
modelSequences = MergeOrDefault("ModelSequences", fileSystem, m.ModelSequences, mapModelSequences, dr.ModelSequences, modelSequences = MergeOrDefault("ModelSequences", fileSystem, m.ModelSequences, mapModelSequences, dr.ModelSequences,
k => k); k => k);
ruleset = new Ruleset(actors, weapons, voices, notifications, music, terrainInfo, sequences, modelSequences); ruleset = new Ruleset(actors, weapons, voices, notifications, music, terrainInfo, modelSequences);
} }
if (modData.IsOnMainThread) if (modData.IsOnMainThread)

View File

@@ -22,7 +22,7 @@ namespace OpenRA.Graphics
public string Name { get; private set; } public string Name { get; private set; }
public bool IsDecoration { get; set; } public bool IsDecoration { get; set; }
readonly SequenceProvider sequences; readonly SequenceSet sequences;
readonly Func<WAngle> facingFunc; readonly Func<WAngle> facingFunc;
readonly Func<bool> paused; readonly Func<bool> paused;

View File

@@ -1,147 +0,0 @@
#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 OpenRA.FileSystem;
using OpenRA.Primitives;
namespace OpenRA.Graphics
{
using Sequences = IReadOnlyDictionary<string, Lazy<IReadOnlyDictionary<string, ISpriteSequence>>>;
using UnitSequences = Lazy<IReadOnlyDictionary<string, ISpriteSequence>>;
public interface ISpriteSequence
{
string Name { get; }
int Length { get; }
int Facings { get; }
int Tick { get; }
int ZOffset { get; }
int ShadowZOffset { get; }
Rectangle Bounds { get; }
bool IgnoreWorldTint { get; }
float Scale { get; }
Sprite GetSprite(int frame);
Sprite GetSprite(int frame, WAngle facing);
(Sprite, WAngle) GetSpriteWithRotation(int frame, WAngle facing);
Sprite GetShadow(int frame, WAngle facing);
float GetAlpha(int frame);
}
public interface ISpriteSequenceLoader
{
IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node);
}
public class SequenceProvider : IDisposable
{
readonly ModData modData;
readonly string tileSet;
readonly Lazy<Sequences> sequences;
readonly Lazy<SpriteCache> spriteCache;
public SpriteCache SpriteCache => spriteCache.Value;
readonly Dictionary<string, UnitSequences> sequenceCache = new Dictionary<string, UnitSequences>();
public SequenceProvider(IReadOnlyFileSystem fileSystem, ModData modData, string tileSet, MiniYaml additionalSequences)
{
this.modData = modData;
this.tileSet = tileSet;
sequences = Exts.Lazy(() =>
{
using (new Support.PerfTimer("LoadSequences"))
return Load(fileSystem, additionalSequences);
});
spriteCache = Exts.Lazy(() => new SpriteCache(fileSystem, modData.SpriteLoaders));
}
public ISpriteSequence GetSequence(string unitName, string sequenceName)
{
if (!sequences.Value.TryGetValue(unitName, out var unitSeq))
throw new InvalidOperationException($"Unit `{unitName}` does not have any sequences defined.");
if (!unitSeq.Value.TryGetValue(sequenceName, out var seq))
throw new InvalidOperationException($"Unit `{unitName}` does not have a sequence named `{sequenceName}`");
return seq;
}
public IEnumerable<string> Images => sequences.Value.Keys;
public bool HasSequence(string unitName)
{
return sequences.Value.ContainsKey(unitName);
}
public bool HasSequence(string unitName, string sequenceName)
{
if (!sequences.Value.TryGetValue(unitName, out var unitSeq))
throw new InvalidOperationException($"Unit `{unitName}` does not have any sequences defined.");
return unitSeq.Value.ContainsKey(sequenceName);
}
public IEnumerable<string> Sequences(string unitName)
{
if (!sequences.Value.TryGetValue(unitName, out var unitSeq))
throw new InvalidOperationException($"Unit `{unitName}` does not have any sequences defined.");
return unitSeq.Value.Keys;
}
Sequences Load(IReadOnlyFileSystem fileSystem, MiniYaml additionalSequences)
{
var nodes = MiniYaml.Load(fileSystem, modData.Manifest.Sequences, additionalSequences);
var items = new Dictionary<string, UnitSequences>();
foreach (var node in nodes)
{
// Nodes starting with ^ are inheritable but never loaded directly
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal))
continue;
var key = node.Value.ToLines(node.Key).JoinWith("|");
if (sequenceCache.TryGetValue(key, out var t))
items.Add(node.Key, t);
else
{
t = Exts.Lazy(() => modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node));
sequenceCache.Add(key, t);
items.Add(node.Key, t);
}
}
return items;
}
public void Preload()
{
foreach (var sb in SpriteCache.SheetBuilders.Values)
sb.Current.CreateBuffer();
foreach (var unitSeq in sequences.Value.Values)
foreach (var seq in unitSeq.Value.Values) { }
foreach (var sb in SpriteCache.SheetBuilders.Values)
sb.Current.ReleaseBuffer();
}
public void Dispose()
{
if (spriteCache.IsValueCreated)
foreach (var sb in SpriteCache.SheetBuilders.Values)
sb.Dispose();
}
}
}

View File

@@ -0,0 +1,119 @@
#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 OpenRA.FileSystem;
using OpenRA.Primitives;
namespace OpenRA.Graphics
{
public interface ISpriteSequence
{
string Name { get; }
int Length { get; }
int Facings { get; }
int Tick { get; }
int ZOffset { get; }
int ShadowZOffset { get; }
Rectangle Bounds { get; }
bool IgnoreWorldTint { get; }
float Scale { get; }
void ResolveSprites(SpriteCache cache);
Sprite GetSprite(int frame);
Sprite GetSprite(int frame, WAngle facing);
(Sprite, WAngle) GetSpriteWithRotation(int frame, WAngle facing);
Sprite GetShadow(int frame, WAngle facing);
float GetAlpha(int frame);
}
public interface ISpriteSequenceLoader
{
int BgraSheetSize { get; }
int IndexedSheetSize { get; }
IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node);
}
public sealed class SequenceSet : IDisposable
{
readonly ModData modData;
readonly string tileSet;
readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, ISpriteSequence>> images;
public SpriteCache SpriteCache { get; }
public SequenceSet(IReadOnlyFileSystem fileSystem, ModData modData, string tileSet, MiniYaml additionalSequences)
{
this.modData = modData;
this.tileSet = tileSet;
SpriteCache = new SpriteCache(fileSystem, modData.SpriteLoaders, modData.SpriteSequenceLoader.BgraSheetSize, modData.SpriteSequenceLoader.IndexedSheetSize);
using (new Support.PerfTimer("LoadSequences"))
images = Load(fileSystem, additionalSequences);
}
public ISpriteSequence GetSequence(string image, string sequence)
{
if (!images.TryGetValue(image, out var sequences))
throw new InvalidOperationException($"Image `{image}` does not have any sequences defined.");
if (!sequences.TryGetValue(sequence, out var seq))
throw new InvalidOperationException($"Image `{image}` does not have a sequence named `{sequence}`.");
return seq;
}
public IEnumerable<string> Images => images.Keys;
public bool HasSequence(string image, string sequence)
{
if (!images.TryGetValue(image, out var sequences))
throw new InvalidOperationException($"Image `{image}` does not have any sequences defined.");
return sequences.ContainsKey(sequence);
}
public IEnumerable<string> Sequences(string image)
{
if (!images.TryGetValue(image, out var sequences))
throw new InvalidOperationException($"Image `{image}` does not have any sequences defined.");
return sequences.Keys;
}
IReadOnlyDictionary<string, IReadOnlyDictionary<string, ISpriteSequence>> Load(IReadOnlyFileSystem fileSystem, MiniYaml additionalSequences)
{
var nodes = MiniYaml.Load(fileSystem, modData.Manifest.Sequences, additionalSequences);
var images = new Dictionary<string, IReadOnlyDictionary<string, ISpriteSequence>>();
foreach (var node in nodes)
{
// Nodes starting with ^ are inheritable but never loaded directly
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal))
continue;
images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node);
}
return images;
}
public void LoadSprites()
{
SpriteCache.LoadReservations(modData);
foreach (var sequences in images.Values)
foreach (var sequence in sequences)
sequence.Value.ResolveSprites(SpriteCache);
}
public void Dispose()
{
SpriteCache.Dispose();
}
}
}

View File

@@ -0,0 +1,174 @@
#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.IO;
using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Primitives;
namespace OpenRA.Graphics
{
public class SpriteCache : IDisposable
{
public readonly Dictionary<SheetType, SheetBuilder> SheetBuilders;
readonly ISpriteLoader[] loaders;
readonly IReadOnlyFileSystem fileSystem;
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> spriteReservations = new Dictionary<int, (int[], MiniYamlNode.SourceLocation)>();
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> frameReservations = new Dictionary<int, (int[], MiniYamlNode.SourceLocation)>();
readonly Dictionary<string, List<int>> reservationsByFilename = new Dictionary<string, List<int>>();
readonly Dictionary<int, ISpriteFrame[]> resolvedFrames = new Dictionary<int, ISpriteFrame[]>();
readonly Dictionary<int, Sprite[]> resolvedSprites = new Dictionary<int, Sprite[]>();
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = new Dictionary<int, (string, MiniYamlNode.SourceLocation)>();
int nextReservationToken = 1;
public SpriteCache(IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int bgraSheetSize, int indexedSheetSize, int bgraSheetMargin = 1, int indexedSheetMargin = 1)
{
SheetBuilders = new Dictionary<SheetType, SheetBuilder>
{
{ SheetType.Indexed, new SheetBuilder(SheetType.Indexed, indexedSheetSize, indexedSheetMargin) },
{ SheetType.BGRA, new SheetBuilder(SheetType.BGRA, bgraSheetSize, bgraSheetMargin) }
};
this.fileSystem = fileSystem;
this.loaders = loaders;
}
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
{
var token = nextReservationToken++;
spriteReservations[token] = (frames?.ToArray(), location);
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
return token;
}
public int ReserveFrames(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
{
var token = nextReservationToken++;
frameReservations[token] = (frames?.ToArray(), location);
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
return token;
}
static ISpriteFrame[] GetFrames(IReadOnlyFileSystem fileSystem, string filename, ISpriteLoader[] loaders, out TypeDictionary metadata)
{
metadata = null;
if (!fileSystem.TryOpen(filename, out var stream))
return null;
using (stream)
{
foreach (var loader in loaders)
if (loader.TryParseSprite(stream, filename, out var frames, out metadata))
return frames;
return null;
}
}
public void LoadReservations(ModData modData)
{
foreach (var sb in SheetBuilders.Values)
sb.Current.CreateBuffer();
var spriteCache = new Dictionary<int, Sprite>();
foreach (var (filename, tokens) in reservationsByFilename)
{
modData.LoadScreen?.Display();
var loadedFrames = GetFrames(fileSystem, filename, loaders, out _);
foreach (var token in tokens)
{
if (frameReservations.TryGetValue(token, out var r))
{
if (loadedFrames != null)
{
if (r.Frames != null)
{
var resolved = new ISpriteFrame[loadedFrames.Length];
foreach (var i in r.Frames)
resolved[i] = loadedFrames[i];
resolvedFrames[token] = resolved;
}
else
resolvedFrames[token] = loadedFrames;
}
else
{
resolvedFrames[token] = null;
missingFiles[token] = (filename, r.Location);
}
}
if (spriteReservations.TryGetValue(token, out r))
{
if (loadedFrames != null)
{
var resolved = new Sprite[loadedFrames.Length];
var frames = r.Frames ?? Enumerable.Range(0, loadedFrames.Length);
foreach (var i in frames)
resolved[i] = spriteCache.GetOrAdd(i,
f => SheetBuilders[SheetBuilder.FrameTypeToSheetType(loadedFrames[f].Type)].Add(loadedFrames[f]));
resolvedSprites[token] = resolved;
}
else
{
resolvedSprites[token] = null;
missingFiles[token] = (filename, r.Location);
}
}
}
spriteCache.Clear();
}
spriteReservations.Clear();
frameReservations.Clear();
reservationsByFilename.Clear();
foreach (var sb in SheetBuilders.Values)
sb.Current.ReleaseBuffer();
}
public Sprite[] ResolveSprites(int token)
{
var resolved = resolvedSprites[token];
resolvedSprites.Remove(token);
if (missingFiles.TryGetValue(token, out var r))
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);
return resolved;
}
public ISpriteFrame[] ResolveFrames(int token)
{
var resolved = resolvedFrames[token];
resolvedFrames.Remove(token);
if (missingFiles.TryGetValue(token, out var r))
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);
return resolved;
}
public IEnumerable<(string Filename, MiniYamlNode.SourceLocation Location)> MissingFiles => missingFiles.Values.ToHashSet();
public void Dispose()
{
foreach (var sb in SheetBuilders.Values)
sb.Dispose();
}
}
}

View File

@@ -9,10 +9,7 @@
*/ */
#endregion #endregion
using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using OpenRA.FileSystem; using OpenRA.FileSystem;
using OpenRA.Primitives; using OpenRA.Primitives;
@@ -67,94 +64,6 @@ namespace OpenRA.Graphics
bool DisableExportPadding { get; } bool DisableExportPadding { get; }
} }
public class SpriteCache
{
public readonly Cache<SheetType, SheetBuilder> SheetBuilders;
readonly ISpriteLoader[] loaders;
readonly IReadOnlyFileSystem fileSystem;
readonly Dictionary<string, List<Sprite[]>> sprites = new Dictionary<string, List<Sprite[]>>();
readonly Dictionary<string, ISpriteFrame[]> unloadedFrames = new Dictionary<string, ISpriteFrame[]>();
readonly Dictionary<string, TypeDictionary> metadata = new Dictionary<string, TypeDictionary>();
public SpriteCache(IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders)
{
SheetBuilders = new Cache<SheetType, SheetBuilder>(t => new SheetBuilder(t));
this.fileSystem = fileSystem;
this.loaders = loaders;
}
/// <summary>
/// Returns the first set of sprites with the given filename.
/// If getUsedFrames is defined then the indices returned by the function call
/// are guaranteed to be loaded. The value of other indices in the returned
/// array are undefined and should never be accessed.
/// </summary>
public Sprite[] this[string filename, Func<int, IEnumerable<int>> getUsedFrames = null]
{
get
{
var allSprites = sprites.GetOrAdd(filename);
var sprite = allSprites.FirstOrDefault();
if (!unloadedFrames.TryGetValue(filename, out var unloaded))
unloaded = null;
// This is the first time that the file has been requested
// Load all of the frames into the unused buffer and initialize
// the loaded cache (initially empty)
if (sprite == null)
{
unloaded = FrameLoader.GetFrames(fileSystem, filename, loaders, out var fileMetadata);
unloadedFrames[filename] = unloaded;
metadata[filename] = fileMetadata;
sprite = new Sprite[unloaded.Length];
allSprites.Add(sprite);
}
// HACK: The sequence code relies on side-effects from getUsedFrames
var indices = getUsedFrames != null ? getUsedFrames(sprite.Length) :
Enumerable.Range(0, sprite.Length);
// Load any unused frames into the SheetBuilder
if (unloaded != null)
{
foreach (var i in indices)
{
if (unloaded[i] != null)
{
sprite[i] = SheetBuilders[SheetBuilder.FrameTypeToSheetType(unloaded[i].Type)].Add(unloaded[i]);
unloaded[i] = null;
}
}
// All frames have been loaded
if (unloaded.All(f => f == null))
unloadedFrames.Remove(filename);
}
return sprite;
}
}
/// <summary>
/// Returns a TypeDictionary containing any metadata defined by the frame
/// or null if the frame does not define metadata.
/// </summary>
public TypeDictionary FrameMetadata(string filename)
{
if (!metadata.TryGetValue(filename, out var fileMetadata))
{
FrameLoader.GetFrames(fileSystem, filename, loaders, out fileMetadata);
metadata[filename] = fileMetadata;
}
return fileMetadata;
}
}
public class FrameCache public class FrameCache
{ {
readonly Cache<string, ISpriteFrame[]> frames; readonly Cache<string, ISpriteFrame[]> frames;

View File

@@ -218,7 +218,7 @@ namespace OpenRA
public string Uid { get; private set; } public string Uid { get; private set; }
public Ruleset Rules { get; private set; } public Ruleset Rules { get; private set; }
public SequenceProvider Sequences => Rules.Sequences; public SequenceSet Sequences { get; private set; }
public bool InvalidCustomRules { get; private set; } public bool InvalidCustomRules { get; private set; }
public Exception InvalidCustomRulesException { get; private set; } public Exception InvalidCustomRulesException { get; private set; }
@@ -439,7 +439,7 @@ namespace OpenRA
try try
{ {
Rules = Ruleset.Load(modData, this, Tileset, RuleDefinitions, WeaponDefinitions, Rules = Ruleset.Load(modData, this, Tileset, RuleDefinitions, WeaponDefinitions,
VoiceDefinitions, NotificationDefinitions, MusicDefinitions, SequenceDefinitions, ModelSequenceDefinitions); VoiceDefinitions, NotificationDefinitions, MusicDefinitions, ModelSequenceDefinitions);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -449,8 +449,7 @@ namespace OpenRA
Rules = Ruleset.LoadDefaultsForTileSet(modData, Tileset); Rules = Ruleset.LoadDefaultsForTileSet(modData, Tileset);
} }
Rules.Sequences.Preload(); Sequences = new SequenceSet(this, modData, Tileset, SequenceDefinitions);
Translation = new Translation(Game.Settings.Player.Language, Translations, this); Translation = new Translation(Game.Settings.Player.Language, Translations, this);
var tl = new MPos(0, 0).ToCPos(this); var tl = new MPos(0, 0).ToCPos(this);

View File

@@ -226,7 +226,7 @@ namespace OpenRA
{ {
return Ruleset.Load(modData, this, TileSet, innerData.RuleDefinitions, return Ruleset.Load(modData, this, TileSet, innerData.RuleDefinitions,
innerData.WeaponDefinitions, innerData.VoiceDefinitions, innerData.NotificationDefinitions, innerData.WeaponDefinitions, innerData.VoiceDefinitions, innerData.NotificationDefinitions,
innerData.MusicDefinitions, innerData.SequenceDefinitions, innerData.ModelSequenceDefinitions); innerData.MusicDefinitions, innerData.ModelSequenceDefinitions);
} }
public MapPreview(ModData modData, string uid, MapGridType gridType, MapCache cache) public MapPreview(ModData modData, string uid, MapGridType gridType, MapCache cache)

View File

@@ -48,9 +48,6 @@ namespace OpenRA
readonly Lazy<IReadOnlyDictionary<string, ITerrainInfo>> defaultTerrainInfo; readonly Lazy<IReadOnlyDictionary<string, ITerrainInfo>> defaultTerrainInfo;
public IReadOnlyDictionary<string, ITerrainInfo> DefaultTerrainInfo => defaultTerrainInfo.Value; public IReadOnlyDictionary<string, ITerrainInfo> DefaultTerrainInfo => defaultTerrainInfo.Value;
readonly Lazy<IReadOnlyDictionary<string, SequenceProvider>> defaultSequences;
public IReadOnlyDictionary<string, SequenceProvider> DefaultSequences => defaultSequences.Value;
public ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false) public ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false)
{ {
Languages = Array.Empty<string>(); Languages = Array.Empty<string>();
@@ -121,12 +118,6 @@ namespace OpenRA
return (IReadOnlyDictionary<string, ITerrainInfo>)new ReadOnlyDictionary<string, ITerrainInfo>(items); return (IReadOnlyDictionary<string, ITerrainInfo>)new ReadOnlyDictionary<string, ITerrainInfo>(items);
}); });
defaultSequences = Exts.Lazy(() =>
{
var items = DefaultTerrainInfo.ToDictionary(t => t.Key, t => new SequenceProvider(DefaultFileSystem, this, t.Key, null));
return (IReadOnlyDictionary<string, SequenceProvider>)new ReadOnlyDictionary<string, SequenceProvider>(items);
});
initialThreadId = Environment.CurrentManagedThreadId; initialThreadId = Environment.CurrentManagedThreadId;
} }
@@ -167,6 +158,7 @@ namespace OpenRA
// Reinitialize all our assets // Reinitialize all our assets
InitializeLoaders(map); InitializeLoaders(map);
map.Sequences.LoadSprites();
// Load music with map assets mounted // Load music with map assets mounted
using (new Support.PerfTimer("Map.Music")) using (new Support.PerfTimer("Map.Music"))

View File

@@ -22,7 +22,7 @@ namespace OpenRA.Mods.Cnc.Graphics
public override ISpriteSequence CreateSequence(ModData modData, string tileset, SpriteCache cache, string image, string sequence, MiniYaml data, MiniYaml defaults) public override ISpriteSequence CreateSequence(ModData modData, string tileset, SpriteCache cache, string image, string sequence, MiniYaml data, MiniYaml defaults)
{ {
return new ClassicSpriteSequence(modData, tileset, cache, this, image, sequence, data, defaults); return new ClassicSpriteSequence(cache, this, image, sequence, data, defaults);
} }
} }
@@ -33,8 +33,8 @@ namespace OpenRA.Mods.Cnc.Graphics
static readonly SpriteSequenceField<bool> UseClassicFacings = new SpriteSequenceField<bool>(nameof(UseClassicFacings), false); static readonly SpriteSequenceField<bool> UseClassicFacings = new SpriteSequenceField<bool>(nameof(UseClassicFacings), false);
readonly bool useClassicFacings; readonly bool useClassicFacings;
public ClassicSpriteSequence(ModData modData, string tileSet, SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) public ClassicSpriteSequence(SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults)
: base(modData, tileSet, cache, loader, image, sequence, data, defaults) : base(cache, loader, image, sequence, data, defaults)
{ {
useClassicFacings = LoadField(UseClassicFacings, data, defaults); useClassicFacings = LoadField(UseClassicFacings, data, defaults);

View File

@@ -23,7 +23,7 @@ namespace OpenRA.Mods.Cnc.Graphics
public override ISpriteSequence CreateSequence(ModData modData, string tileset, SpriteCache cache, string image, string sequence, MiniYaml data, MiniYaml defaults) public override ISpriteSequence CreateSequence(ModData modData, string tileset, SpriteCache cache, string image, string sequence, MiniYaml data, MiniYaml defaults)
{ {
return new ClassicTilesetSpecificSpriteSequence(modData, tileset, cache, this, image, sequence, data, defaults); return new ClassicTilesetSpecificSpriteSequence(cache, this, image, sequence, data, defaults);
} }
} }
@@ -34,20 +34,56 @@ namespace OpenRA.Mods.Cnc.Graphics
[Desc("Dictionary of <tileset name>: filename to override the Filename key.")] [Desc("Dictionary of <tileset name>: filename to override the Filename key.")]
static readonly SpriteSequenceField<Dictionary<string, string>> TilesetFilenames = new SpriteSequenceField<Dictionary<string, string>>(nameof(TilesetFilenames), null); static readonly SpriteSequenceField<Dictionary<string, string>> TilesetFilenames = new SpriteSequenceField<Dictionary<string, string>>(nameof(TilesetFilenames), null);
public ClassicTilesetSpecificSpriteSequence(ModData modData, string tileset, SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) public ClassicTilesetSpecificSpriteSequence(SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults)
: base(modData, tileset, cache, loader, image, sequence, data, defaults) { } : base(cache, loader, image, sequence, data, defaults) { }
protected override string GetSpriteFilename(ModData modData, string tileset, string image, string sequence, MiniYaml data, MiniYaml defaults) protected override IEnumerable<ReservationInfo> ParseFilenames(ModData modData, string tileset, int[] frames, MiniYaml data, MiniYaml defaults)
{ {
var node = data.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key); var node = data.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key);
if (node != null) if (node != null)
{ {
var tilesetNode = node.Value.Nodes.FirstOrDefault(n => n.Key == tileset); var tilesetNode = node.Value.Nodes.FirstOrDefault(n => n.Key == tileset);
if (tilesetNode != null) if (tilesetNode != null)
return tilesetNode.Value.Value; {
// Only request the subset of frames that we actually need
int[] loadFrames = null;
if (length != null)
{
loadFrames = CalculateFrameIndices(start, length.Value, stride ?? length.Value, facings, frames, transpose, reverseFacings);
if (shadowStart >= 0)
loadFrames = loadFrames.Concat(loadFrames.Select(i => i + shadowStart - start)).ToArray();
}
return new[] { new ReservationInfo(tilesetNode.Value.Value, loadFrames, frames, tilesetNode.Location) };
}
} }
return base.GetSpriteFilename(modData, tileset, image, sequence, data, defaults); return base.ParseFilenames(modData, tileset, frames, data, defaults);
}
protected override IEnumerable<ReservationInfo> ParseCombineFilenames(ModData modData, string tileset, int[] frames, MiniYaml data)
{
var node = data.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key);
if (node != null)
{
var tilesetNode = node.Value.Nodes.FirstOrDefault(n => n.Key == tileset);
if (tilesetNode != null)
{
if (frames == null)
{
if (LoadField<string>("Length", null, data) != "*")
{
var subStart = LoadField("Start", 0, data);
var subLength = LoadField("Length", 1, data);
frames = Exts.MakeArray(subLength, i => subStart + i);
}
}
return new[] { new ReservationInfo(tilesetNode.Value.Value, frames, frames, tilesetNode.Location) };
}
}
return base.ParseCombineFilenames(modData, tileset, frames, data);
} }
} }
} }

View File

@@ -21,29 +21,35 @@ namespace OpenRA.Mods.Common.Graphics
{ {
public class DefaultSpriteSequenceLoader : ISpriteSequenceLoader public class DefaultSpriteSequenceLoader : ISpriteSequenceLoader
{ {
public readonly int BgraSheetSize = 2048;
public readonly int IndexedSheetSize = 2048;
static readonly MiniYaml NoData = new MiniYaml(null); static readonly MiniYaml NoData = new MiniYaml(null);
public DefaultSpriteSequenceLoader(ModData modData) { } public DefaultSpriteSequenceLoader(ModData modData)
{
var metadata = modData.Manifest.Get<SpriteSequenceFormat>().Metadata;
if (metadata.TryGetValue("BgraSheetSize", out var yaml))
BgraSheetSize = FieldLoader.GetValue<int>("BgraSheetSize", yaml.Value);
if (metadata.TryGetValue("IndexedSheetSize", out yaml))
IndexedSheetSize = FieldLoader.GetValue<int>("IndexedSheetSize", yaml.Value);
}
public virtual ISpriteSequence CreateSequence(ModData modData, string tileset, SpriteCache cache, string image, string sequence, MiniYaml data, MiniYaml defaults) public virtual ISpriteSequence CreateSequence(ModData modData, string tileset, SpriteCache cache, string image, string sequence, MiniYaml data, MiniYaml defaults)
{ {
return new DefaultSpriteSequence(modData, tileset, cache, this, image, sequence, data, defaults); return new DefaultSpriteSequence(cache, this, image, sequence, data, defaults);
} }
public IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode imageNode) int ISpriteSequenceLoader.BgraSheetSize => BgraSheetSize;
int ISpriteSequenceLoader.IndexedSheetSize => IndexedSheetSize;
IReadOnlyDictionary<string, ISpriteSequence> ISpriteSequenceLoader.ParseSequences(ModData modData, string tileset, SpriteCache cache, MiniYamlNode imageNode)
{ {
var sequences = new Dictionary<string, ISpriteSequence>(); var sequences = new Dictionary<string, ISpriteSequence>();
MiniYaml defaults; var node = imageNode.Value.Nodes.SingleOrDefault(n => n.Key == "Defaults");
try var defaults = node?.Value ?? NoData;
{ imageNode.Value.Nodes.Remove(node);
var node = imageNode.Value.Nodes.SingleOrDefault(n => n.Key == "Defaults");
defaults = node?.Value ?? NoData;
imageNode.Value.Nodes.Remove(node);
}
catch (Exception e)
{
throw new InvalidDataException($"Error occurred while parsing {imageNode.Key}", e);
}
foreach (var sequenceNode in imageNode.Value.Nodes) foreach (var sequenceNode in imageNode.Value.Nodes)
{ {
@@ -51,18 +57,13 @@ namespace OpenRA.Mods.Common.Graphics
{ {
try try
{ {
var sequence = CreateSequence(modData, tileSet, cache, imageNode.Key, sequenceNode.Key, sequenceNode.Value, defaults); var sequence = CreateSequence(modData, tileset, cache, imageNode.Key, sequenceNode.Key, sequenceNode.Value, defaults);
((DefaultSpriteSequence)sequence).ReserveSprites(modData, tileset, cache, sequenceNode.Value, defaults);
sequences.Add(sequenceNode.Key, sequence); sequences.Add(sequenceNode.Key, sequence);
} }
catch (FileNotFoundException ex) catch (Exception e)
{ {
// Defer exception until something tries to access the sequence throw new InvalidDataException($"Failed to parse sequences for {imageNode.Key}.{sequenceNode.Key} at {imageNode.Value.Nodes[0].Location}:\n{e}");
// This allows the asset installer and OpenRA.Utility to load the game without having the actor assets
sequences.Add(sequenceNode.Key, new FileNotFoundSequence(ex));
}
catch (FormatException f)
{
throw new FormatException($"Failed to parse sequences for {imageNode.Key}.{sequenceNode.Key} at {imageNode.Value.Nodes[0].Location}:\n{f}");
} }
} }
} }
@@ -71,33 +72,6 @@ namespace OpenRA.Mods.Common.Graphics
} }
} }
public class FileNotFoundSequence : ISpriteSequence
{
readonly FileNotFoundException exception;
public FileNotFoundSequence(FileNotFoundException exception)
{
this.exception = exception;
}
public string Filename => exception.FileName;
string ISpriteSequence.Name => throw exception;
int ISpriteSequence.Length => throw exception;
int ISpriteSequence.Facings => throw exception;
int ISpriteSequence.Tick => throw exception;
int ISpriteSequence.ZOffset => throw exception;
int ISpriteSequence.ShadowZOffset => throw exception;
Rectangle ISpriteSequence.Bounds => throw exception;
bool ISpriteSequence.IgnoreWorldTint => throw exception;
float ISpriteSequence.Scale => throw exception;
Sprite ISpriteSequence.GetSprite(int frame) { throw exception; }
Sprite ISpriteSequence.GetSprite(int frame, WAngle facing) { throw exception; }
(Sprite, WAngle) ISpriteSequence.GetSpriteWithRotation(int frame, WAngle facing) { throw exception; }
Sprite ISpriteSequence.GetShadow(int frame, WAngle facing) { throw exception; }
float ISpriteSequence.GetAlpha(int frame) { throw exception; }
}
public struct SpriteSequenceField<T> public struct SpriteSequenceField<T>
{ {
public string Key; public string Key;
@@ -113,139 +87,200 @@ namespace OpenRA.Mods.Common.Graphics
[Desc("Generic sprite sequence implementation, mostly unencumbered with game- or artwork-specific logic.")] [Desc("Generic sprite sequence implementation, mostly unencumbered with game- or artwork-specific logic.")]
public class DefaultSpriteSequence : ISpriteSequence public class DefaultSpriteSequence : ISpriteSequence
{ {
static readonly MiniYaml NoData = new MiniYaml(null); protected class SpriteReservation
{
public int Token;
public float3 Offset;
public bool FlipX;
public bool FlipY;
public float ZRamp;
public BlendMode BlendMode;
public int[] Frames;
}
readonly string image; protected readonly struct ReservationInfo
protected Sprite[] sprites; {
readonly bool reverseFacings, transpose; public readonly string Filename;
public readonly int[] LoadFrames;
public readonly int[] Frames;
public readonly MiniYamlNode.SourceLocation Location;
protected readonly ISpriteSequenceLoader Loader; public ReservationInfo(string filename, int[] loadFrames, int[] frames, MiniYamlNode.SourceLocation location)
{
public Rectangle Bounds { get; } Filename = filename;
LoadFrames = loadFrames;
public string Name { get; } Frames = frames;
Location = location;
}
}
[Desc("File name of the sprite to use for this sequence.")] [Desc("File name of the sprite to use for this sequence.")]
static readonly SpriteSequenceField<string> Filename = new SpriteSequenceField<string>(nameof(Filename), null); protected static readonly SpriteSequenceField<string> Filename = new SpriteSequenceField<string>(nameof(Filename), null);
[Desc("Frame index to start from.")] [Desc("Frame index to start from.")]
static readonly SpriteSequenceField<int> Start = new SpriteSequenceField<int>(nameof(Start), 0); protected static readonly SpriteSequenceField<int> Start = new SpriteSequenceField<int>(nameof(Start), 0);
int start;
[Desc("Number of frames to use. Does not have to be the total amount the sprite sheet has.")] [Desc("Number of frames to use. Does not have to be the total amount the sprite sheet has.")]
static readonly SpriteSequenceField<int> Length = new SpriteSequenceField<int>(nameof(Length), 1); protected static readonly SpriteSequenceField<int> Length = new SpriteSequenceField<int>(nameof(Length), 1);
int ISpriteSequence.Length => length;
int length;
[Desc("Overrides Length if a different number of frames is defined between facings.")] [Desc("Overrides Length if a different number of frames is defined between facings.")]
static readonly SpriteSequenceField<int> Stride = new SpriteSequenceField<int>(nameof(Stride), -1); protected static readonly SpriteSequenceField<int> Stride = new SpriteSequenceField<int>(nameof(Stride), -1);
int stride;
[Desc("The amount of directions the unit faces. Use negative values to rotate counter-clockwise.")] [Desc("The number of facings that are provided by sprite frames. Use negative values to rotate counter-clockwise.")]
static readonly SpriteSequenceField<int> Facings = new SpriteSequenceField<int>(nameof(Facings), 1); protected static readonly SpriteSequenceField<int> Facings = new SpriteSequenceField<int>(nameof(Facings), 1);
int ISpriteSequence.Facings => interpolatedFacings ?? facings;
protected int facings;
[Desc("The amount of directions the unit faces. Use negative values to rotate counter-clockwise.")] [Desc("The total number of facings for the sequence. If >Facings, the closest facing sprite will be rotated to match. Use negative values to rotate counter-clockwise.")]
static readonly SpriteSequenceField<int?> InterpolatedFacings = new SpriteSequenceField<int?>(nameof(InterpolatedFacings), null); protected static readonly SpriteSequenceField<int?> InterpolatedFacings = new SpriteSequenceField<int?>(nameof(InterpolatedFacings), null);
protected int? interpolatedFacings;
[Desc("Time (in milliseconds at default game speed) to wait until playing the next frame in the animation.")] [Desc("Time (in milliseconds at default game speed) to wait until playing the next frame in the animation.")]
static readonly SpriteSequenceField<int> Tick = new SpriteSequenceField<int>(nameof(Tick), 40); protected static readonly SpriteSequenceField<int> Tick = new SpriteSequenceField<int>(nameof(Tick), 40);
int ISpriteSequence.Tick => tick;
readonly int tick;
[Desc("Value controlling the Z-order. A higher values means rendering on top of other sprites at the same position. " + [Desc("Value controlling the Z-order. A higher values means rendering on top of other sprites at the same position. " +
"Use power of 2 values to avoid glitches.")] "Use power of 2 values to avoid glitches.")]
static readonly SpriteSequenceField<WDist> ZOffset = new SpriteSequenceField<WDist>(nameof(ZOffset), WDist.Zero); protected static readonly SpriteSequenceField<WDist> ZOffset = new SpriteSequenceField<WDist>(nameof(ZOffset), WDist.Zero);
int ISpriteSequence.ZOffset => zOffset;
readonly int zOffset;
[Desc("Additional sprite depth Z offset to apply as a function of sprite Y (0: vertical, 1: flat on terrain)")] [Desc("Additional sprite depth Z offset to apply as a function of sprite Y (0: vertical, 1: flat on terrain)")]
static readonly SpriteSequenceField<int> ZRamp = new SpriteSequenceField<int>(nameof(ZRamp), 0); protected static readonly SpriteSequenceField<int> ZRamp = new SpriteSequenceField<int>(nameof(ZRamp), 0);
[Desc("If the shadow is not part of the sprite, but baked into the same sprite sheet at a fixed offset, " + [Desc("If the shadow is not part of the sprite, but baked into the same sprite sheet at a fixed offset, " +
"set this to the frame index where it starts.")] "set this to the frame index where it starts.")]
static readonly SpriteSequenceField<int> ShadowStart = new SpriteSequenceField<int>(nameof(ShadowStart), -1); protected static readonly SpriteSequenceField<int> ShadowStart = new SpriteSequenceField<int>(nameof(ShadowStart), -1);
readonly int shadowStart;
[Desc("Set Z-Offset for the separate shadow. Used by the later Westwood 2.5D titles.")] [Desc("Set Z-Offset for the separate shadow. Used by the later Westwood 2.5D titles.")]
static readonly SpriteSequenceField<WDist> ShadowZOffset = new SpriteSequenceField<WDist>(nameof(ShadowZOffset), new WDist(-5)); protected static readonly SpriteSequenceField<WDist> ShadowZOffset = new SpriteSequenceField<WDist>(nameof(ShadowZOffset), new WDist(-5));
int ISpriteSequence.ShadowZOffset => shadowZOffset;
readonly int shadowZOffset;
[Desc("The individual frames to play instead of going through them sequentially from the `Start`.")] [Desc("The individual frames to play instead of going through them sequentially from the `Start`.")]
static readonly SpriteSequenceField<int[]> Frames = new SpriteSequenceField<int[]>(nameof(Frames), null); protected static readonly SpriteSequenceField<int[]> Frames = new SpriteSequenceField<int[]>(nameof(Frames), null);
int[] frames;
[Desc("Don't apply terrain lighting or colored overlays.")] [Desc("Don't apply terrain lighting or colored overlays.")]
static readonly SpriteSequenceField<bool> IgnoreWorldTint = new SpriteSequenceField<bool>(nameof(IgnoreWorldTint), false); protected static readonly SpriteSequenceField<bool> IgnoreWorldTint = new SpriteSequenceField<bool>(nameof(IgnoreWorldTint), false);
bool ISpriteSequence.IgnoreWorldTint => ignoreWorldTint;
readonly bool ignoreWorldTint;
[Desc("Adjusts the rendered size of the sprite")] [Desc("Adjusts the rendered size of the sprite")]
static readonly SpriteSequenceField<float> Scale = new SpriteSequenceField<float>(nameof(Scale), 1); protected static readonly SpriteSequenceField<float> Scale = new SpriteSequenceField<float>(nameof(Scale), 1);
float ISpriteSequence.Scale => scale;
readonly float scale;
[Desc("Play the sprite sequence back and forth.")] [Desc("Play the sprite sequence back and forth.")]
static readonly SpriteSequenceField<bool> Reverses = new SpriteSequenceField<bool>(nameof(Reverses), false); protected static readonly SpriteSequenceField<bool> Reverses = new SpriteSequenceField<bool>(nameof(Reverses), false);
[Desc("Support a frame order where each animation step is split per each direction.")] [Desc("Support a frame order where each animation step is split per each direction.")]
static readonly SpriteSequenceField<bool> Transpose = new SpriteSequenceField<bool>(nameof(Transpose), false); protected static readonly SpriteSequenceField<bool> Transpose = new SpriteSequenceField<bool>(nameof(Transpose), false);
[Desc("Mirror on the X axis.")] [Desc("Mirror on the X axis.")]
static readonly SpriteSequenceField<bool> FlipX = new SpriteSequenceField<bool>(nameof(FlipX), false); protected static readonly SpriteSequenceField<bool> FlipX = new SpriteSequenceField<bool>(nameof(FlipX), false);
[Desc("Mirror on the Y axis.")] [Desc("Mirror on the Y axis.")]
static readonly SpriteSequenceField<bool> FlipY = new SpriteSequenceField<bool>(nameof(FlipY), false); protected static readonly SpriteSequenceField<bool> FlipY = new SpriteSequenceField<bool>(nameof(FlipY), false);
[Desc("Change the position in-game on X, Y, Z.")] [Desc("Change the position in-game on X, Y, Z.")]
static readonly SpriteSequenceField<float3> Offset = new SpriteSequenceField<float3>(nameof(Offset), float3.Zero); protected static readonly SpriteSequenceField<float3> Offset = new SpriteSequenceField<float3>(nameof(Offset), float3.Zero);
[Desc("Apply an OpenGL/Photoshop inspired blend mode.")] [Desc("Apply an OpenGL/Photoshop inspired blend mode.")]
static readonly SpriteSequenceField<BlendMode> BlendMode = new SpriteSequenceField<BlendMode>(nameof(BlendMode), OpenRA.BlendMode.Alpha); protected static readonly SpriteSequenceField<BlendMode> BlendMode = new SpriteSequenceField<BlendMode>(nameof(BlendMode), OpenRA.BlendMode.Alpha);
[Desc("Allows to append multiple sequence definitions which are indented below this node " + [Desc("Create a virtual sprite file by concatenating one or more frames from multiple files, with optional transformations applied. " +
"like when offsets differ per frame or a sequence is spread across individual files.")] "All defined frames will be loaded into memory, even if unused, so use this property with care.")]
static readonly SpriteSequenceField<MiniYaml> Combine = new SpriteSequenceField<MiniYaml>(nameof(Combine), null); protected static readonly SpriteSequenceField<MiniYaml> Combine = new SpriteSequenceField<MiniYaml>(nameof(Combine), null);
[Desc("Sets transparency - use one value to set for all frames or provide a value for each frame.")] [Desc("Sets transparency - use one value to set for all frames or provide a value for each frame.")]
static readonly SpriteSequenceField<float[]> Alpha = new SpriteSequenceField<float[]>(nameof(Alpha), null); protected static readonly SpriteSequenceField<float[]> Alpha = new SpriteSequenceField<float[]>(nameof(Alpha), null);
readonly float[] alpha;
[Desc("Fade the animation from fully opaque on the first frame to fully transparent after the last frame.")] [Desc("Fade the animation from fully opaque on the first frame to fully transparent after the last frame.")]
static readonly SpriteSequenceField<bool> AlphaFade = new SpriteSequenceField<bool>(nameof(AlphaFade), false); protected static readonly SpriteSequenceField<bool> AlphaFade = new SpriteSequenceField<bool>(nameof(AlphaFade), false);
[Desc("Name of the file containing the depth data sprite.")] [Desc("Name of the file containing the depth data sprite.")]
static readonly SpriteSequenceField<string> DepthSprite = new SpriteSequenceField<string>(nameof(DepthSprite), null); protected static readonly SpriteSequenceField<string> DepthSprite = new SpriteSequenceField<string>(nameof(DepthSprite), null);
[Desc("Frame index containing the depth data.")] [Desc("Frame index containing the depth data.")]
static readonly SpriteSequenceField<int> DepthSpriteFrame = new SpriteSequenceField<int>(nameof(DepthSpriteFrame), 0); protected static readonly SpriteSequenceField<int> DepthSpriteFrame = new SpriteSequenceField<int>(nameof(DepthSpriteFrame), 0);
[Desc("X, Y offset to apply to the depth sprite.")] [Desc("X, Y offset to apply to the depth sprite.")]
static readonly SpriteSequenceField<float2> DepthSpriteOffset = new SpriteSequenceField<float2>(nameof(DepthSpriteOffset), float2.Zero); protected static readonly SpriteSequenceField<float2> DepthSpriteOffset = new SpriteSequenceField<float2>(nameof(DepthSpriteOffset), float2.Zero);
protected virtual string GetSpriteFilename(ModData modData, string tileset, string image, string sequence, MiniYaml data, MiniYaml defaults) protected static readonly MiniYaml NoData = new MiniYaml(null);
protected readonly ISpriteSequenceLoader Loader;
protected string image;
protected List<SpriteReservation> spritesToLoad = new List<SpriteReservation>();
protected Sprite[] sprites;
protected Sprite[] shadowSprites;
protected bool reverseFacings;
protected bool reverses;
protected int start;
protected int shadowStart;
protected int? length;
protected int? stride;
protected bool transpose;
protected int facings;
protected int? interpolatedFacings;
protected int tick;
protected int zOffset;
protected int shadowZOffset;
protected bool ignoreWorldTint;
protected float scale;
protected float[] alpha;
protected bool alphaFade;
protected Rectangle? bounds;
protected int? depthSpriteReservation;
protected float2 depthSpriteOffset;
protected void ThrowIfUnresolved()
{ {
return LoadField(Filename, data, defaults); if (bounds == null)
throw new InvalidOperationException($"Unable to query unresolved sequence {image}.{Name}.");
} }
protected static T LoadField<T>(string key, T fallback, MiniYaml data, MiniYaml defaults) int ISpriteSequence.Length
{ {
var node = data.Nodes.FirstOrDefault(n => n.Key == key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == key); get
{
ThrowIfUnresolved();
return length.Value;
}
}
int ISpriteSequence.Facings => interpolatedFacings ?? facings;
int ISpriteSequence.Tick => tick;
int ISpriteSequence.ZOffset => zOffset;
int ISpriteSequence.ShadowZOffset => shadowZOffset;
bool ISpriteSequence.IgnoreWorldTint => ignoreWorldTint;
float ISpriteSequence.Scale => GetScale();
Rectangle ISpriteSequence.Bounds
{
get
{
ThrowIfUnresolved();
return bounds.Value;
}
}
public string Name { get; }
protected static T LoadField<T>(string key, T fallback, MiniYaml data, MiniYaml defaults = null)
{
var node = data.Nodes.FirstOrDefault(n => n.Key == key) ?? defaults?.Nodes.FirstOrDefault(n => n.Key == key);
if (node == null) if (node == null)
return fallback; return fallback;
return FieldLoader.GetValue<T>(key, node.Value.Value); return FieldLoader.GetValue<T>(key, node.Value.Value);
} }
protected static T LoadField<T>(SpriteSequenceField<T> field, MiniYaml data, MiniYaml defaults) protected static T LoadField<T>(SpriteSequenceField<T> field, MiniYaml data, MiniYaml defaults = null)
{ {
var node = data.Nodes.FirstOrDefault(n => n.Key == field.Key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == field.Key); return LoadField(field, data, defaults, out _);
if (node == null) }
return field.DefaultValue;
protected static T LoadField<T>(SpriteSequenceField<T> field, MiniYaml data, MiniYaml defaults, out MiniYamlNode.SourceLocation location)
{
var node = data.Nodes.FirstOrDefault(n => n.Key == field.Key) ?? defaults?.Nodes.FirstOrDefault(n => n.Key == field.Key);
if (node == null)
{
location = default;
return field.DefaultValue;
}
location = node.Location;
return FieldLoader.GetValue<T>(field.Key, node.Value.Value); return FieldLoader.GetValue<T>(field.Key, node.Value.Value);
} }
@@ -259,30 +294,92 @@ namespace OpenRA.Mods.Common.Graphics
return Rectangle.FromLTRB(left, top, right, bottom); return Rectangle.FromLTRB(left, top, right, bottom);
} }
public DefaultSpriteSequence(ModData modData, string tileSet, SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) protected static int[] CalculateFrameIndices(int start, int length, int stride, int facings, int[] frames, bool transpose, bool reverseFacings)
{
var usedFrames = new List<int>();
for (var facing = 0; facing < facings; facing++)
{
var facingInner = reverseFacings ? (facings - facing) % facings : facing;
for (var frame = 0; frame < length; frame++)
{
var i = transpose ? frame % length * facings + facingInner :
facingInner * stride + frame % length;
usedFrames.Add(frames?[i] ?? start + i);
}
}
return usedFrames.ToArray();
}
protected virtual IEnumerable<ReservationInfo> ParseFilenames(ModData modData, string tileset, int[] frames, MiniYaml data, MiniYaml defaults)
{
var filename = LoadField(Filename, data, defaults, out var location);
// Only request the subset of frames that we actually need.
int[] loadFrames = null;
if (length != null)
{
loadFrames = CalculateFrameIndices(start, length.Value, stride ?? length.Value, facings, frames, transpose, reverseFacings);
if (shadowStart >= 0)
loadFrames = loadFrames.Concat(loadFrames.Select(i => i + shadowStart - start)).ToArray();
}
yield return new ReservationInfo(filename, loadFrames, frames, location);
}
protected virtual IEnumerable<ReservationInfo> ParseCombineFilenames(ModData modData, string tileset, int[] frames, MiniYaml data)
{
var filename = LoadField(Filename, data, null, out var location);
if (frames == null)
{
if (LoadField<string>(Length.Key, null, data) != "*")
{
var subStart = LoadField("Start", 0, data);
var subLength = LoadField("Length", 1, data);
frames = Exts.MakeArray(subLength, i => subStart + i);
}
}
yield return new ReservationInfo(filename, frames, frames, location);
}
public DefaultSpriteSequence(SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults)
{ {
this.image = image; this.image = image;
Name = sequence; Name = sequence;
Loader = loader; Loader = loader;
start = LoadField(Start, data, defaults); start = LoadField(Start, data, defaults);
length = null;
var lengthLocation = default(MiniYamlNode.SourceLocation);
if (LoadField<string>(Length.Key, null, data, defaults) != "*")
length = LoadField(Length, data, defaults, out lengthLocation);
stride = LoadField(Stride.Key, length, data, defaults);
facings = LoadField(Facings, data, defaults, out var facingsLocation);
interpolatedFacings = LoadField(InterpolatedFacings, data, defaults, out var interpolatedFacingsLocation);
tick = LoadField(Tick, data, defaults);
zOffset = LoadField(ZOffset, data, defaults).Length;
shadowStart = LoadField(ShadowStart, data, defaults); shadowStart = LoadField(ShadowStart, data, defaults);
shadowZOffset = LoadField(ShadowZOffset, data, defaults).Length; shadowZOffset = LoadField(ShadowZOffset, data, defaults).Length;
zOffset = LoadField(ZOffset, data, defaults).Length;
tick = LoadField(Tick, data, defaults);
transpose = LoadField(Transpose, data, defaults);
frames = LoadField(Frames, data, defaults);
ignoreWorldTint = LoadField(IgnoreWorldTint, data, defaults); ignoreWorldTint = LoadField(IgnoreWorldTint, data, defaults);
scale = LoadField(Scale, data, defaults); scale = LoadField(Scale, data, defaults);
var flipX = LoadField(FlipX, data, defaults); reverses = LoadField(Reverses, data, defaults);
var flipY = LoadField(FlipY, data, defaults); transpose = LoadField(Transpose, data, defaults);
var zRamp = LoadField(ZRamp, data, defaults); alpha = LoadField(Alpha, data, defaults);
alphaFade = LoadField(AlphaFade, data, defaults, out var alphaFadeLocation);
facings = LoadField(Facings, data, defaults); var depthSprite = LoadField(DepthSprite, data, defaults, out var depthSpriteLocation);
interpolatedFacings = LoadField(InterpolatedFacings, data, defaults); if (!string.IsNullOrEmpty(depthSprite))
if (interpolatedFacings != null && (interpolatedFacings <= 1 || interpolatedFacings <= Math.Abs(facings) || interpolatedFacings > 1024 || !Exts.IsPowerOf2(interpolatedFacings.Value))) depthSpriteReservation = cache.ReserveSprites(depthSprite, new[] { LoadField(DepthSpriteFrame, data, defaults) }, depthSpriteLocation);
throw new YamlException($"Sequence {image}.{sequence}: InterpolatedFacings must be greater than Facings, within the range of 2 to 1024, and a power of 2.");
depthSpriteOffset = LoadField(DepthSpriteOffset, data, defaults);
if (facings < 0) if (facings < 0)
{ {
@@ -290,216 +387,157 @@ namespace OpenRA.Mods.Common.Graphics
facings = -facings; facings = -facings;
} }
if (interpolatedFacings != null && (interpolatedFacings < 2 || interpolatedFacings <= facings || interpolatedFacings > 1024 || !Exts.IsPowerOf2(interpolatedFacings.Value)))
throw new YamlException($"{interpolatedFacingsLocation}: {InterpolatedFacings.Key} must be greater than {Facings.Key}, within the range of 2 to 1024, and a power of 2.");
if (length != null && length <= 0)
throw new YamlException($"{lengthLocation}: {Length.Key} must be positive.");
if (length == null && facings > 1)
throw new YamlException($"{facingsLocation}: {Facings.Key} cannot be used with {Length.Key}: *.");
if (alphaFade && alpha != null)
throw new YamlException($"{alphaFadeLocation}: {AlphaFade.Key} cannot be used with {Alpha.Key}.");
}
public virtual void ReserveSprites(ModData modData, string tileset, SpriteCache cache, MiniYaml data, MiniYaml defaults)
{
var frames = LoadField(Frames, data, defaults);
var flipX = LoadField(FlipX, data, defaults);
var flipY = LoadField(FlipY, data, defaults);
var zRamp = LoadField(ZRamp, data, defaults);
var offset = LoadField(Offset, data, defaults); var offset = LoadField(Offset, data, defaults);
var blendMode = LoadField(BlendMode, data, defaults); var blendMode = LoadField(BlendMode, data, defaults);
IEnumerable<int> GetUsedFrames(int frameCount)
{
if (LoadField(Length.Key, "", data, defaults) == "*")
length = frames?.Length ?? frameCount - start;
else
length = LoadField(Length, data, defaults);
// Plays the animation forwards, and then in reverse
if (LoadField(Reverses, data, defaults))
{
var frames = this.frames != null ? this.frames.Skip(start).Take(length).ToArray() : Exts.MakeArray(length, i => start + i);
this.frames = frames.Concat(frames.Skip(1).Take(length - 2).Reverse()).ToArray();
length = 2 * length - 2;
start = 0;
}
// Overrides Length with a custom stride
stride = LoadField(Stride.Key, length, data, defaults);
if (length > stride)
throw new YamlException($"Sequence {image}.{sequence}: Length must be <= stride");
if (frames != null && length > frames.Length)
throw new YamlException($"Sequence {image}.{sequence}: Length must be <= Frames.Length");
var end = start + (facings - 1) * stride + length - 1;
if (frames != null)
{
foreach (var f in frames)
if (f < 0 || f >= frameCount)
throw new YamlException($"Sequence {image}.{sequence} defines a Frames override that references frame {f}, but only [{start}..{end}] actually exist");
if (start < 0 || end >= frames.Length)
throw new YamlException($"Sequence {image}.{sequence} uses indices [{start}..{end}] of the Frames list, but only {frames.Length} frames are defined");
}
else if (start < 0 || end >= frameCount)
throw new YamlException($"Sequence {image}.{sequence} uses frames [{start}..{end}], but only [0..{frameCount - 1}] actually exist");
if (shadowStart >= 0 && shadowStart + (facings - 1) * stride + length > frameCount)
throw new YamlException($"Sequence {image}.{sequence}'s shadow frames use frames [{shadowStart}..{shadowStart + (facings - 1) * stride + length - 1}], but only [0..{frameCount - 1}] actually exist");
var usedFrames = new List<int>();
for (var facing = 0; facing < facings; facing++)
{
for (var frame = 0; frame < length; frame++)
{
var i = transpose ? frame % length * facings + facing :
facing * stride + frame % length;
usedFrames.Add(frames != null ? frames[i] : start + i);
}
}
if (shadowStart >= 0)
return usedFrames.Concat(usedFrames.Select(i => i + shadowStart - start));
return usedFrames;
}
var combineNode = data.Nodes.FirstOrDefault(n => n.Key == Combine.Key); var combineNode = data.Nodes.FirstOrDefault(n => n.Key == Combine.Key);
if (combineNode != null) if (combineNode != null)
{ {
var combined = Enumerable.Empty<Sprite>(); for (var i = 0; i < combineNode.Value.Nodes.Count; i++)
foreach (var combineSequenceNode in combineNode.Value.Nodes)
{ {
var combineData = combineSequenceNode.Value; var subData = combineNode.Value.Nodes[i].Value;
var subOffset = LoadField(Offset, subData, NoData);
var subFlipX = LoadField(FlipX, subData, NoData);
var subFlipY = LoadField(FlipY, subData, NoData);
var subFrames = LoadField(Frames, data);
// Allow per-sprite offset, flipping, start, and length foreach (var f in ParseCombineFilenames(modData, tileset, subFrames, subData))
// These shouldn't inherit Start/Offset/etc from the main definition
var subStart = LoadField(Start, combineData, NoData);
var subOffset = LoadField(Offset, combineData, NoData);
var subFlipX = LoadField(FlipX, combineData, NoData);
var subFlipY = LoadField(FlipY, combineData, NoData);
var subFrames = LoadField(Frames, combineData, NoData);
var subLength = 0;
IEnumerable<int> SubGetUsedFrames(int subFrameCount)
{ {
var combineLengthNode = combineData.Nodes.FirstOrDefault(n => n.Key == Length.Key); spritesToLoad.Add(new SpriteReservation
if (combineLengthNode?.Value.Value == "*") {
subLength = subFrames?.Length ?? subFrameCount - subStart; Token = cache.ReserveSprites(f.Filename, f.LoadFrames, f.Location),
else Offset = subOffset + offset,
subLength = LoadField(Length, combineData, NoData); FlipX = subFlipX ^ flipX,
FlipY = subFlipY ^ flipY,
return subFrames != null ? subFrames.Skip(subStart).Take(subLength) : Enumerable.Range(subStart, subLength); BlendMode = blendMode,
ZRamp = zRamp,
Frames = f.Frames
});
} }
var subFilename = GetSpriteFilename(modData, tileSet, image, sequence, combineData, NoData);
if (subFilename == null)
throw new YamlException($"Sequence {image}.{sequence}.{combineSequenceNode.Key} does not define a filename.");
var subSprites = cache[subFilename, SubGetUsedFrames].Select(s =>
{
if (s == null)
return null;
var bounds = FlipRectangle(s.Bounds, subFlipX, subFlipY);
var dx = subOffset.X + offset.X + (subFlipX ? -s.Offset.X : s.Offset.X);
var dy = subOffset.Y + offset.Y + (subFlipY ? -s.Offset.Y : s.Offset.Y);
var dz = subOffset.Z + offset.Z + s.Offset.Z + zRamp * dy;
return new Sprite(s.Sheet, bounds, zRamp, new float3(dx, dy, dz), s.Channel, blendMode);
}).ToList();
var frames = subFrames != null ? subFrames.Skip(subStart).Take(subLength).ToArray() : Exts.MakeArray(subLength, i => subStart + i);
combined = combined.Concat(frames.Select(i => subSprites[i]));
} }
sprites = combined.ToArray();
GetUsedFrames(sprites.Length);
} }
else else
{ {
// Apply offset to each sprite in the sequence foreach (var f in ParseFilenames(modData, tileset, frames, data, defaults))
// Different sequences may apply different offsets to the same frame {
var filename = GetSpriteFilename(modData, tileSet, image, sequence, data, defaults); spritesToLoad.Add(new SpriteReservation
if (filename == null) {
throw new YamlException($"Sequence {image}.{sequence} does not define a filename."); Token = cache.ReserveSprites(f.Filename, f.LoadFrames, f.Location),
Offset = offset,
FlipX = flipX,
FlipY = flipY,
BlendMode = blendMode,
ZRamp = zRamp,
Frames = f.Frames,
});
}
}
}
sprites = cache[filename, GetUsedFrames].Select(s => public virtual void ResolveSprites(SpriteCache cache)
{
if (bounds != null)
return;
Sprite depthSprite = null;
if (depthSpriteReservation != null)
depthSprite = cache.ResolveSprites(depthSpriteReservation.Value).First(s => s != null);
var allSprites = spritesToLoad.SelectMany(r =>
{
var resolved = cache.ResolveSprites(r.Token);
if (r.Frames != null)
resolved = r.Frames.Select(f => resolved[f]).ToArray();
return resolved.Select(s =>
{ {
if (s == null) if (s == null)
return null; return null;
var bounds = FlipRectangle(s.Bounds, flipX, flipY); var dx = r.Offset.X + (r.FlipX ? -s.Offset.X : s.Offset.X);
var dx = offset.X + (flipX ? -s.Offset.X : s.Offset.X); var dy = r.Offset.Y + (r.FlipY ? -s.Offset.Y : s.Offset.Y);
var dy = offset.Y + (flipY ? -s.Offset.Y : s.Offset.Y); var dz = r.Offset.Z + s.Offset.Z + r.ZRamp * dy;
var dz = offset.Z + s.Offset.Z + zRamp * dy; var sprite = new Sprite(s.Sheet, FlipRectangle(s.Bounds, r.FlipX, r.FlipY), r.ZRamp, new float3(dx, dy, dz), s.Channel, r.BlendMode);
if (depthSprite == null)
return sprite;
return new Sprite(s.Sheet, bounds, zRamp, new float3(dx, dy, dz), s.Channel, blendMode); var cw = (depthSprite.Bounds.Left + depthSprite.Bounds.Right) / 2 + (int)(s.Offset.X + depthSpriteOffset.X);
}).ToArray(); var ch = (depthSprite.Bounds.Top + depthSprite.Bounds.Bottom) / 2 + (int)(s.Offset.Y + depthSpriteOffset.Y);
}
alpha = LoadField(Alpha, data, defaults);
if (alpha != null)
{
if (alpha.Length == 1)
alpha = Exts.MakeArray(length, _ => alpha[0]);
else if (alpha.Length != length)
throw new YamlException($"Sequence {image}.{sequence} must define either 1 or {length} Alpha values.");
}
if (LoadField(AlphaFade, data, defaults))
{
if (alpha != null)
throw new YamlException($"Sequence {image}.{sequence} cannot define both AlphaFade and Alpha.");
alpha = Exts.MakeArray(length, i => float2.Lerp(1f, 0f, i / (length - 1f)));
}
var depthSprite = LoadField(DepthSprite, data, defaults);
if (!string.IsNullOrEmpty(depthSprite))
{
var depthSpriteFrame = LoadField(DepthSpriteFrame, data, defaults);
var depthOffset = LoadField(DepthSpriteOffset, data, defaults);
IEnumerable<int> GetDepthFrame(int _) => new[] { depthSpriteFrame };
var ds = cache[depthSprite, GetDepthFrame][depthSpriteFrame];
sprites = sprites.Select(s =>
{
if (s == null)
return null;
var cw = (ds.Bounds.Left + ds.Bounds.Right) / 2 + (int)(s.Offset.X + depthOffset.X);
var ch = (ds.Bounds.Top + ds.Bounds.Bottom) / 2 + (int)(s.Offset.Y + depthOffset.Y);
var w = s.Bounds.Width / 2; var w = s.Bounds.Width / 2;
var h = s.Bounds.Height / 2; var h = s.Bounds.Height / 2;
var r = Rectangle.FromLTRB(cw - w, ch - h, cw + w, ch + h); return new SpriteWithSecondaryData(sprite, depthSprite.Sheet, Rectangle.FromLTRB(cw - w, ch - h, cw + w, ch + h), depthSprite.Channel);
return new SpriteWithSecondaryData(s, ds.Sheet, r, ds.Channel); });
}).ToArray(); }).ToArray();
length = length ?? allSprites.Length - start;
if (alpha != null)
{
if (alpha.Length == 1)
alpha = Exts.MakeArray(length.Value, _ => alpha[0]);
else if (alpha.Length != length.Value)
throw new YamlException($"Sequence {image}.{Name} must define either 1 or {length.Value} Alpha values.");
}
else if (alphaFade)
alpha = Exts.MakeArray(length.Value, i => float2.Lerp(1f, 0f, i / (length.Value - 1f)));
// Reindex sprites to order facings anti-clockwise and remove unused frames
var index = CalculateFrameIndices(start, length.Value, stride ?? length.Value, facings, null, transpose, reverseFacings).ToList();
if (reverses)
{
index.AddRange(index.Skip(1).Take(length.Value - 2).Reverse());
length = 2 * length - 2;
} }
var boundSprites = SpriteBounds(sprites, frames, start, facings, length, stride, transpose); if (!index.Any())
if (shadowStart > 0) throw new YamlException($"Sequence {image}.{Name} does not define any frames.");
boundSprites = boundSprites.Concat(SpriteBounds(sprites, frames, shadowStart, facings, length, stride, transpose));
Bounds = boundSprites.Union(); var minIndex = index.Min();
var maxIndex = index.Max();
if (minIndex < 0 || maxIndex >= allSprites.Length)
throw new YamlException($"Sequence {image}.{Name} uses frames between {minIndex}..{maxIndex}, but only 0..{allSprites.Length - 1} exist.");
sprites = index.Select(f => allSprites[f]).ToArray();
if (shadowStart >= 0)
shadowSprites = index.Select(f => allSprites[f - start + shadowStart]).ToArray();
bounds = sprites.Concat(shadowSprites ?? Enumerable.Empty<Sprite>()).Select(OffsetSpriteBounds).Union();
} }
/// <summary>Returns the bounds of all of the sprites that can appear in this animation</summary> protected static Rectangle OffsetSpriteBounds(Sprite sprite)
static IEnumerable<Rectangle> SpriteBounds(Sprite[] sprites, int[] frames, int start, int facings, int length, int stride, bool transpose)
{ {
for (var facing = 0; facing < facings; facing++) if (sprite == null || sprite.Bounds.IsEmpty)
{ return Rectangle.Empty;
for (var frame = 0; frame < length; frame++)
{ return new Rectangle(
var i = transpose ? frame % length * facings + facing : (int)(sprite.Offset.X - sprite.Size.X / 2),
facing * stride + frame % length; (int)(sprite.Offset.Y - sprite.Size.Y / 2),
var s = frames != null ? sprites[frames[i]] : sprites[start + i]; sprite.Bounds.Width, sprite.Bounds.Height);
if (!s.Bounds.IsEmpty)
yield return new Rectangle(
(int)(s.Offset.X - s.Size.X / 2),
(int)(s.Offset.Y - s.Size.Y / 2),
s.Bounds.Width, s.Bounds.Height);
}
}
} }
public Sprite GetSprite(int frame) public Sprite GetSprite(int frame)
{ {
return GetSprite(start, frame, WAngle.Zero); return GetSprite(frame, WAngle.Zero);
}
public Sprite GetSprite(int frame, WAngle facing)
{
return GetSprite(start, frame, facing);
} }
public (Sprite, WAngle) GetSpriteWithRotation(int frame, WAngle facing) public (Sprite, WAngle) GetSpriteWithRotation(int frame, WAngle facing)
@@ -508,28 +546,31 @@ namespace OpenRA.Mods.Common.Graphics
if (interpolatedFacings != null) if (interpolatedFacings != null)
rotation = Util.GetInterpolatedFacingRotation(facing, Math.Abs(facings), interpolatedFacings.Value); rotation = Util.GetInterpolatedFacingRotation(facing, Math.Abs(facings), interpolatedFacings.Value);
return (GetSprite(start, frame, facing), rotation); return (GetSprite(frame, facing), rotation);
} }
public Sprite GetShadow(int frame, WAngle facing) public Sprite GetShadow(int frame, WAngle facing)
{ {
return shadowStart >= 0 ? GetSprite(shadowStart, frame, facing) : null; if (shadowSprites == null)
return null;
var index = GetFacingFrameOffset(facing) * length.Value + frame % length.Value;
var sprite = shadowSprites[index];
if (sprite == null)
throw new InvalidOperationException($"Attempted to query unloaded shadow sprite from {image}.{Name} frame={frame} facing={facing}.");
return sprite;
} }
protected virtual Sprite GetSprite(int start, int frame, WAngle facing) public virtual Sprite GetSprite(int frame, WAngle facing)
{ {
var f = GetFacingFrameOffset(facing); ThrowIfUnresolved();
if (reverseFacings) var index = GetFacingFrameOffset(facing) * length.Value + frame % length.Value;
f = (facings - f) % facings; var sprite = sprites[index];
if (sprite == null)
throw new InvalidOperationException($"Attempted to query unloaded sprite from {image}.{Name} frame={frame} facing={facing}.");
var i = transpose ? frame % length * facings + f : return sprite;
f * stride + frame % length;
var j = frames != null ? frames[i] : start + i;
if (sprites[j] == null)
throw new InvalidOperationException($"Attempted to query unloaded sprite from {image}.{Name} start={start} frame={frame} facing={facing}");
return sprites[j];
} }
protected virtual int GetFacingFrameOffset(WAngle facing) protected virtual int GetFacingFrameOffset(WAngle facing)
@@ -541,5 +582,10 @@ namespace OpenRA.Mods.Common.Graphics
{ {
return alpha?[frame] ?? 1f; return alpha?[frame] ?? 1f;
} }
protected virtual float GetScale()
{
return scale;
}
} }
} }

View File

@@ -22,7 +22,7 @@ namespace OpenRA.Mods.Common.Graphics
public override ISpriteSequence CreateSequence(ModData modData, string tileSet, SpriteCache cache, string image, string sequence, MiniYaml data, MiniYaml defaults) public override ISpriteSequence CreateSequence(ModData modData, string tileSet, SpriteCache cache, string image, string sequence, MiniYaml data, MiniYaml defaults)
{ {
return new TilesetSpecificSpriteSequence(modData, tileSet, cache, this, image, sequence, data, defaults); return new TilesetSpecificSpriteSequence(cache, this, image, sequence, data, defaults);
} }
} }
@@ -32,20 +32,56 @@ namespace OpenRA.Mods.Common.Graphics
[Desc("Dictionary of <tileset name>: filename to override the Filename key.")] [Desc("Dictionary of <tileset name>: filename to override the Filename key.")]
static readonly SpriteSequenceField<Dictionary<string, string>> TilesetFilenames = new SpriteSequenceField<Dictionary<string, string>>(nameof(TilesetFilenames), null); static readonly SpriteSequenceField<Dictionary<string, string>> TilesetFilenames = new SpriteSequenceField<Dictionary<string, string>>(nameof(TilesetFilenames), null);
public TilesetSpecificSpriteSequence(ModData modData, string tileset, SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) public TilesetSpecificSpriteSequence(SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults)
: base(modData, tileset, cache, loader, image, sequence, data, defaults) { } : base(cache, loader, image, sequence, data, defaults) { }
protected override string GetSpriteFilename(ModData modData, string tileset, string image, string sequence, MiniYaml data, MiniYaml defaults) protected override IEnumerable<ReservationInfo> ParseFilenames(ModData modData, string tileset, int[] frames, MiniYaml data, MiniYaml defaults)
{ {
var node = data.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key); var node = data.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key) ?? defaults.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key);
if (node != null) if (node != null)
{ {
var tilesetNode = node.Value.Nodes.FirstOrDefault(n => n.Key == tileset); var tilesetNode = node.Value.Nodes.FirstOrDefault(n => n.Key == tileset);
if (tilesetNode != null) if (tilesetNode != null)
return tilesetNode.Value.Value; {
// Only request the subset of frames that we actually need
int[] loadFrames = null;
if (length != null)
{
loadFrames = CalculateFrameIndices(start, length.Value, stride ?? length.Value, facings, frames, transpose, reverseFacings);
if (shadowStart >= 0)
loadFrames = loadFrames.Concat(loadFrames.Select(i => i + shadowStart - start)).ToArray();
}
return new[] { new ReservationInfo(tilesetNode.Value.Value, loadFrames, frames, tilesetNode.Location) };
}
} }
return base.GetSpriteFilename(modData, tileset, image, sequence, data, defaults); return base.ParseFilenames(modData, tileset, frames, data, defaults);
}
protected override IEnumerable<ReservationInfo> ParseCombineFilenames(ModData modData, string tileset, int[] frames, MiniYaml data)
{
var node = data.Nodes.FirstOrDefault(n => n.Key == TilesetFilenames.Key);
if (node != null)
{
var tilesetNode = node.Value.Nodes.FirstOrDefault(n => n.Key == tileset);
if (tilesetNode != null)
{
if (frames == null)
{
if (LoadField<string>("Length", null, data) != "*")
{
var subStart = LoadField("Start", 0, data);
var subLength = LoadField("Length", 1, data);
frames = Exts.MakeArray(subLength, i => subStart + i);
}
}
return new[] { new ReservationInfo(tilesetNode.Value.Value, frames, frames, tilesetNode.Location) };
}
}
return base.ParseCombineFilenames(modData, tileset, frames, data);
} }
} }
} }

View File

@@ -20,7 +20,7 @@ namespace OpenRA.Mods.Common.Lint
{ {
class CheckSequences : ILintSequencesPass class CheckSequences : ILintSequencesPass
{ {
void ILintSequencesPass.Run(Action<string> emitError, Action<string> emitWarning, ModData modData, Ruleset rules, SequenceProvider sequences) void ILintSequencesPass.Run(Action<string> emitError, Action<string> emitWarning, ModData modData, Ruleset rules, SequenceSet sequences)
{ {
var factions = rules.Actors[SystemActors.World].TraitInfos<FactionInfo>().Select(f => f.InternalName).ToArray(); var factions = rules.Actors[SystemActors.World].TraitInfos<FactionInfo>().Select(f => f.InternalName).ToArray();
foreach (var actorInfo in rules.Actors) foreach (var actorInfo in rules.Actors)

View File

@@ -23,7 +23,7 @@ namespace OpenRA.Mods.Common.Traits
[Desc("Defines sequence to derive facings from.")] [Desc("Defines sequence to derive facings from.")]
public readonly string Sequence = "idle"; public readonly string Sequence = "idle";
public int QuantizedBodyFacings(ActorInfo ai, SequenceProvider sequences, string faction) public int QuantizedBodyFacings(ActorInfo ai, SequenceSet sequences, string faction)
{ {
if (string.IsNullOrEmpty(Sequence)) if (string.IsNullOrEmpty(Sequence))
throw new InvalidOperationException($"Actor {ai.Name} is missing sequence to quantize facings from."); throw new InvalidOperationException($"Actor {ai.Name} is missing sequence to quantize facings from.");

View File

@@ -35,7 +35,7 @@ namespace OpenRA.Mods.Common.Traits
public interface IQuantizeBodyOrientationInfo : ITraitInfoInterface public interface IQuantizeBodyOrientationInfo : ITraitInfoInterface
{ {
int QuantizedBodyFacings(ActorInfo ai, SequenceProvider sequenceProvider, string race); int QuantizedBodyFacings(ActorInfo ai, SequenceSet sequences, string faction);
} }
public interface IPlaceBuildingDecorationInfo : ITraitInfoInterface public interface IPlaceBuildingDecorationInfo : ITraitInfoInterface

View File

@@ -10,7 +10,7 @@
#endregion #endregion
using System; using System;
using OpenRA.Mods.Common.Graphics; using OpenRA.Graphics;
using OpenRA.Mods.Common.Terrain; using OpenRA.Mods.Common.Terrain;
using OpenRA.Mods.Common.Traits; using OpenRA.Mods.Common.Traits;
@@ -38,29 +38,21 @@ namespace OpenRA.Mods.Common.UtilityCommands
// any tilesets from being checked further. // any tilesets from being checked further.
try try
{ {
// DefaultSequences is a dictionary of tileset: SequenceProvider foreach (var (tileset, terrainInfo) in modData.DefaultTerrainInfo)
// so we can also use this to key our tileset checks
foreach (var kv in modData.DefaultSequences)
{ {
try try
{ {
Console.WriteLine("Tileset: " + kv.Key); Console.WriteLine("Tileset: " + tileset);
var terrainInfo = modData.DefaultTerrainInfo[kv.Key];
if (terrainInfo is ITemplatedTerrainInfo templatedTerrainInfo) if (terrainInfo is ITemplatedTerrainInfo templatedTerrainInfo)
foreach (var r in modData.DefaultRules.Actors[SystemActors.World].TraitInfos<ITiledTerrainRendererInfo>()) foreach (var ttr in modData.DefaultRules.Actors[SystemActors.World].TraitInfos<ITiledTerrainRendererInfo>())
failed |= r.ValidateTileSprites(templatedTerrainInfo, Console.WriteLine); failed |= ttr.ValidateTileSprites(templatedTerrainInfo, Console.WriteLine);
foreach (var image in kv.Value.Images) var sequences = new SequenceSet(modData.DefaultFileSystem, modData, tileset, null);
sequences.SpriteCache.LoadReservations(modData);
foreach (var (filename, location) in sequences.SpriteCache.MissingFiles)
{ {
foreach (var sequence in kv.Value.Sequences(image)) Console.WriteLine($"\t{location}: {filename} not found");
{ failed = true;
if (!(kv.Value.GetSequence(image, sequence) is FileNotFoundSequence s))
continue;
Console.WriteLine("\tSequence `{0}.{1}` references sprite `{2}` that does not exist.", image, sequence, s.Filename);
failed = true;
}
} }
} }
catch (YamlException e) catch (YamlException e)

View File

@@ -67,7 +67,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
{ {
Console.WriteLine($"Testing default sequences for {tileset}"); Console.WriteLine($"Testing default sequences for {tileset}");
var sequences = new SequenceProvider(modData.DefaultFileSystem, modData, tileset, null); var sequences = new SequenceSet(modData.DefaultFileSystem, modData, tileset, null);
CheckSequences(modData, modData.DefaultRules, sequences); CheckSequences(modData, modData.DefaultRules, sequences);
} }
@@ -165,7 +165,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
} }
} }
void CheckSequences(ModData modData, Ruleset rules, SequenceProvider sequences) void CheckSequences(ModData modData, Ruleset rules, SequenceSet sequences)
{ {
foreach (var customSequencesPassType in modData.ObjectCreator.GetTypesImplementing<ILintSequencesPass>()) foreach (var customSequencesPassType in modData.ObjectCreator.GetTypesImplementing<ILintSequencesPass>())
{ {

View File

@@ -34,8 +34,10 @@ namespace OpenRA.Mods.Common.UtilityCommands
var palette = new ImmutablePalette(args[1], new[] { 0 }, Array.Empty<int>()); var palette = new ImmutablePalette(args[1], new[] { 0 }, Array.Empty<int>());
SequenceProvider sequences; SequenceSet sequences;
if (!modData.DefaultSequences.TryGetValue(args[2], out sequences)) if (modData.DefaultTerrainInfo.ContainsKey(args[2]))
sequences = new SequenceSet(modData.ModFiles, modData, args[2], null);
else
{ {
var mapPackage = new Folder(Platform.EngineDir).OpenPackage(args[2], modData.ModFiles); var mapPackage = new Folder(Platform.EngineDir).OpenPackage(args[2], modData.ModFiles);
if (mapPackage == null) if (mapPackage == null)
@@ -44,7 +46,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
sequences = new Map(modData, mapPackage).Sequences; sequences = new Map(modData, mapPackage).Sequences;
} }
sequences.Preload(); sequences.LoadSprites();
var count = 0; var count = 0;

View File

@@ -51,8 +51,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
var relatedEnumTypes = new HashSet<Type>(); var relatedEnumTypes = new HashSet<Type>();
var sequenceTypesInfo = sequenceTypes var sequenceTypesInfo = sequenceTypes
.Where(x => !x.ContainsGenericParameters && !x.IsAbstract .Where(x => !x.ContainsGenericParameters && !x.IsAbstract)
&& x.Name != nameof(FileNotFoundSequence)) // NOTE: This is the simplest way to exclude FileNotFoundSequence, which shouldn't be added.
.Select(type => new .Select(type => new
{ {
type.Namespace, type.Namespace,

View File

@@ -17,5 +17,5 @@ namespace OpenRA.Mods.Common.Lint
public interface ILintPass { void Run(Action<string> emitError, Action<string> emitWarning, ModData modData); } public interface ILintPass { void Run(Action<string> emitError, Action<string> emitWarning, ModData modData); }
public interface ILintMapPass { void Run(Action<string> emitError, Action<string> emitWarning, ModData modData, Map map); } public interface ILintMapPass { void Run(Action<string> emitError, Action<string> emitWarning, ModData modData, Map map); }
public interface ILintRulesPass { void Run(Action<string> emitError, Action<string> emitWarning, ModData modData, Ruleset rules); } public interface ILintRulesPass { void Run(Action<string> emitError, Action<string> emitWarning, ModData modData, Ruleset rules); }
public interface ILintSequencesPass { void Run(Action<string> emitError, Action<string> emitWarning, ModData modData, Ruleset rules, SequenceProvider sequences); } public interface ILintSequencesPass { void Run(Action<string> emitError, Action<string> emitWarning, ModData modData, Ruleset rules, SequenceSet sequences); }
} }