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 OpenRA.FileSystem;
using OpenRA.GameRules;
using OpenRA.Graphics;
using OpenRA.Traits;
namespace OpenRA
@@ -28,7 +27,6 @@ namespace OpenRA
public readonly IReadOnlyDictionary<string, SoundInfo> Notifications;
public readonly IReadOnlyDictionary<string, MusicInfo> Music;
public readonly ITerrainInfo TerrainInfo;
public readonly SequenceProvider Sequences;
public readonly IReadOnlyDictionary<string, MiniYamlNode> ModelSequences;
public Ruleset(
@@ -38,7 +36,6 @@ namespace OpenRA
IReadOnlyDictionary<string, SoundInfo> notifications,
IReadOnlyDictionary<string, MusicInfo> music,
ITerrainInfo terrainInfo,
SequenceProvider sequences,
IReadOnlyDictionary<string, MiniYamlNode> modelSequences)
{
Actors = new ActorInfoDictionary(actors);
@@ -47,7 +44,6 @@ namespace OpenRA
Notifications = notifications;
Music = music;
TerrainInfo = terrainInfo;
Sequences = sequences;
ModelSequences = modelSequences;
foreach (var a in Actors.Values)
@@ -145,8 +141,8 @@ namespace OpenRA
var modelSequences = MergeOrDefault("Manifest,ModelSequences", fs, m.ModelSequences, null, null,
k => k);
// The default ruleset does not include a preferred tileset or sequence set
ruleset = new Ruleset(actors, weapons, voices, notifications, music, null, null, modelSequences);
// The default ruleset does not include a preferred tileset
ruleset = new Ruleset(actors, weapons, voices, notifications, music, null, modelSequences);
}
if (modData.IsOnMainThread)
@@ -170,14 +166,13 @@ namespace OpenRA
{
var dr = modData.DefaultRules;
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,
MiniYaml mapRules, MiniYaml mapWeapons, MiniYaml mapVoices, MiniYaml mapNotifications,
MiniYaml mapMusic, MiniYaml mapSequences, MiniYaml mapModelSequences)
MiniYaml mapMusic, MiniYaml mapModelSequences)
{
var m = modData.Manifest;
var dr = modData.DefaultRules;
@@ -204,16 +199,12 @@ namespace OpenRA
// TODO: Add support for merging custom terrain modifications
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;
if (mapModelSequences != null)
modelSequences = MergeOrDefault("ModelSequences", fileSystem, m.ModelSequences, mapModelSequences, dr.ModelSequences,
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)

View File

@@ -22,7 +22,7 @@ namespace OpenRA.Graphics
public string Name { get; private set; }
public bool IsDecoration { get; set; }
readonly SequenceProvider sequences;
readonly SequenceSet sequences;
readonly Func<WAngle> facingFunc;
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
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Primitives;
@@ -67,94 +64,6 @@ namespace OpenRA.Graphics
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
{
readonly Cache<string, ISpriteFrame[]> frames;

View File

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

View File

@@ -48,9 +48,6 @@ namespace OpenRA
readonly Lazy<IReadOnlyDictionary<string, ITerrainInfo>> defaultTerrainInfo;
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)
{
Languages = Array.Empty<string>();
@@ -121,12 +118,6 @@ namespace OpenRA
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;
}
@@ -167,6 +158,7 @@ namespace OpenRA
// Reinitialize all our assets
InitializeLoaders(map);
map.Sequences.LoadSprites();
// Load music with map assets mounted
using (new Support.PerfTimer("Map.Music"))