diff --git a/OpenRA.Game/Exts.cs b/OpenRA.Game/Exts.cs index 11a428e089..53de85d9c7 100644 --- a/OpenRA.Game/Exts.cs +++ b/OpenRA.Game/Exts.cs @@ -361,7 +361,7 @@ namespace OpenRA public static Dictionary ToDictionaryWithConflictLog( this IEnumerable source, Func keySelector, Func elementSelector, - string debugName, Func logKey, Func logValue) + string debugName, Func logKey = null, Func logValue = null) { // Fall back on ToString() if null functions are provided: logKey = logKey ?? (s => s.ToString()); diff --git a/OpenRA.Game/GameRules/Ruleset.cs b/OpenRA.Game/GameRules/Ruleset.cs index 588abf8a08..0f7e299076 100644 --- a/OpenRA.Game/GameRules/Ruleset.cs +++ b/OpenRA.Game/GameRules/Ruleset.cs @@ -9,8 +9,11 @@ */ #endregion +using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using OpenRA.FileSystem; using OpenRA.GameRules; using OpenRA.Graphics; using OpenRA.Traits; @@ -28,19 +31,19 @@ namespace OpenRA public readonly SequenceProvider Sequences; public Ruleset( - IDictionary actors, - IDictionary weapons, - IDictionary voices, - IDictionary notifications, - IDictionary music, + IReadOnlyDictionary actors, + IReadOnlyDictionary weapons, + IReadOnlyDictionary voices, + IReadOnlyDictionary notifications, + IReadOnlyDictionary music, TileSet tileSet, SequenceProvider sequences) { - Actors = new ReadOnlyDictionary(actors); - Weapons = new ReadOnlyDictionary(weapons); - Voices = new ReadOnlyDictionary(voices); - Notifications = new ReadOnlyDictionary(notifications); - Music = new ReadOnlyDictionary(music); + Actors = actors; + Weapons = weapons; + Voices = voices; + Notifications = notifications; + Music = music; TileSet = tileSet; Sequences = sequences; @@ -80,5 +83,120 @@ namespace OpenRA } public IEnumerable> InstalledMusic { get { return Music.Where(m => m.Value.Exists); } } + + static IReadOnlyDictionary MergeOrDefault(string name, IReadOnlyFileSystem fileSystem, IEnumerable files, MiniYaml additional, + IReadOnlyDictionary defaults, Func makeObject) + { + if (additional == null && defaults != null) + return defaults; + + var result = MiniYaml.Load(fileSystem, files, additional) + .ToDictionaryWithConflictLog(k => k.Key.ToLowerInvariant(), makeObject, "LoadFromManifest<" + name + ">"); + + return new ReadOnlyDictionary(result); + } + + public static Ruleset LoadDefaults(ModData modData) + { + var m = modData.Manifest; + var fs = modData.DefaultFileSystem; + + Ruleset ruleset = null; + Action f = () => + { + var actors = MergeOrDefault("Manifest,Rules", fs, m.Rules, null, null, + k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value)); + + var weapons = MergeOrDefault("Manifest,Weapons", fs, m.Weapons, null, null, + k => new WeaponInfo(k.Key.ToLowerInvariant(), k.Value)); + + var voices = MergeOrDefault("Manifest,Voices", fs, m.Voices, null, null, + k => new SoundInfo(k.Value)); + + var notifications = MergeOrDefault("Manifest,Notifications", fs, m.Notifications, null, null, + k => new SoundInfo(k.Value)); + + var music = MergeOrDefault("Manifest,Music", fs, m.Music, null, null, + k => new MusicInfo(k.Key, k.Value)); + + // The default ruleset does not include a preferred tileset or sequence set + ruleset = new Ruleset(actors, weapons, voices, notifications, music, null, null); + }; + + if (modData.IsOnMainThread) + { + modData.HandleLoadingProgress(); + + var loader = new Task(f); + loader.Start(); + + // Animate the loadscreen while we wait + while (!loader.Wait(40)) + modData.HandleLoadingProgress(); + } + else + f(); + + return ruleset; + } + + public static Ruleset LoadDefaultsForTileSet(ModData modData, string tileSet) + { + var dr = modData.DefaultRules; + var ts = modData.DefaultTileSets[tileSet]; + var sequences = modData.DefaultSequences[tileSet]; + return new Ruleset(dr.Actors, dr.Weapons, dr.Voices, dr.Notifications, dr.Music, ts, sequences); + } + + public static Ruleset LoadFromMap(ModData modData, Map map) + { + var m = modData.Manifest; + var dr = modData.DefaultRules; + + Ruleset ruleset = null; + Action f = () => + { + var actors = MergeOrDefault("Manifest,Rules", map, m.Rules, map.RuleDefinitions, dr.Actors, + k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value)); + + var weapons = MergeOrDefault("Manifest,Weapons", map, m.Weapons, map.WeaponDefinitions, dr.Weapons, + k => new WeaponInfo(k.Key.ToLowerInvariant(), k.Value)); + + var voices = MergeOrDefault("Manifest,Voices", map, m.Voices, map.VoiceDefinitions, dr.Voices, + k => new SoundInfo(k.Value)); + + var notifications = MergeOrDefault("Manifest,Notifications", map, m.Notifications, map.NotificationDefinitions, dr.Notifications, + k => new SoundInfo(k.Value)); + + var music = MergeOrDefault("Manifest,Music", map, m.Music, map.NotificationDefinitions, dr.Music, + k => new MusicInfo(k.Key, k.Value)); + + // TODO: Add support for merging custom tileset modifications + var ts = modData.DefaultTileSets[map.Tileset]; + + // TODO: Top-level dictionary should be moved into the Ruleset instead of in its own object + var sequences = map.SequenceDefinitions == null ? modData.DefaultSequences[map.Tileset] : + new SequenceProvider(map, modData, ts, map.SequenceDefinitions); + + // TODO: Add support for custom voxel sequences + ruleset = new Ruleset(actors, weapons, voices, notifications, music, ts, sequences); + }; + + if (modData.IsOnMainThread) + { + modData.HandleLoadingProgress(); + + var loader = new Task(f); + loader.Start(); + + // Animate the loadscreen while we wait + while (!loader.Wait(40)) + modData.HandleLoadingProgress(); + } + else + f(); + + return ruleset; + } } } diff --git a/OpenRA.Game/GameRules/RulesetCache.cs b/OpenRA.Game/GameRules/RulesetCache.cs deleted file mode 100644 index d5d386e3c0..0000000000 --- a/OpenRA.Game/GameRules/RulesetCache.cs +++ /dev/null @@ -1,154 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2016 The OpenRA Developers (see AUTHORS) - * This file is part of OpenRA, which is free software. It is made - * available to you under the terms of the GNU General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. For more - * information, see COPYING. - */ -#endregion - -using System; -using System.Collections.Generic; -using System.Linq; -using OpenRA.FileSystem; -using OpenRA.GameRules; -using OpenRA.Graphics; -using OpenRA.Support; - -namespace OpenRA -{ - public sealed class RulesetCache - { - readonly ModData modData; - - readonly Dictionary actorCache = new Dictionary(); - readonly Dictionary weaponCache = new Dictionary(); - readonly Dictionary voiceCache = new Dictionary(); - readonly Dictionary notificationCache = new Dictionary(); - readonly Dictionary musicCache = new Dictionary(); - - public event EventHandler LoadingProgress; - void RaiseProgress() - { - if (LoadingProgress != null) - LoadingProgress(this, new EventArgs()); - } - - public RulesetCache(ModData modData) - { - this.modData = modData; - } - - public Ruleset Load(IReadOnlyFileSystem fileSystem, - TileSet tileSet, - MiniYaml additionalRules, - MiniYaml additionalWeapons, - MiniYaml additionalVoices, - MiniYaml additionalNotifications, - MiniYaml additionalMusic, - MiniYaml additionalSequences) - { - var m = modData.Manifest; - - Dictionary actors; - Dictionary weapons; - Dictionary voices; - Dictionary notifications; - Dictionary music; - - using (new PerfTimer("Actors")) - actors = LoadYamlRules(fileSystem, actorCache, m.Rules, additionalRules, - k => new ActorInfo(Game.ModData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value)); - - using (new PerfTimer("Weapons")) - weapons = LoadYamlRules(fileSystem, weaponCache, m.Weapons, additionalWeapons, - k => new WeaponInfo(k.Key.ToLowerInvariant(), k.Value)); - - using (new PerfTimer("Voices")) - voices = LoadYamlRules(fileSystem, voiceCache, m.Voices, additionalVoices, - k => new SoundInfo(k.Value)); - - using (new PerfTimer("Notifications")) - notifications = LoadYamlRules(fileSystem, notificationCache, m.Notifications, additionalNotifications, - k => new SoundInfo(k.Value)); - - using (new PerfTimer("Music")) - music = LoadYamlRules(fileSystem, musicCache, m.Music, additionalMusic, - k => new MusicInfo(k.Key, k.Value)); - - var sequences = tileSet != null ? new SequenceProvider(fileSystem, modData, tileSet, additionalSequences) : null; - return new Ruleset(actors, weapons, voices, notifications, music, tileSet, sequences); - } - - /// - /// Cache and return the Ruleset for a given map. - /// If a map isn't specified then return the default mod Ruleset. - /// - public Ruleset Load(IReadOnlyFileSystem fileSystem, Map map = null) - { - return map != null ? Load(fileSystem, modData.DefaultTileSets[map.Tileset], map.RuleDefinitions, - map.WeaponDefinitions,map.VoiceDefinitions, map.NotificationDefinitions, map.MusicDefinitions, - map.SequenceDefinitions) : Load(fileSystem, null, null, null, null, null, null, null); - } - - Dictionary LoadYamlRules(IReadOnlyFileSystem fileSystem, - Dictionary itemCache, - IEnumerable files, MiniYaml mapRules, - Func f) - { - RaiseProgress(); - - if (mapRules != null && mapRules.Value != null) - files = files.Append(FieldLoader.GetValue("value", mapRules.Value)); - - var inputKey = string.Join("|", files); - if (mapRules != null && mapRules.Nodes.Any()) - inputKey += "|" + mapRules.Nodes.WriteToString(); - - Func wrap = wkv => - { - var key = inputKey + wkv.Value.ToLines(wkv.Key).JoinWith("|"); - T t; - if (itemCache.TryGetValue(key, out t)) - return t; - - t = f(wkv); - itemCache.Add(key, t); - - RaiseProgress(); - return t; - }; - - var tree = MiniYaml.Load(fileSystem, files, mapRules).ToDictionaryWithConflictLog( - n => n.Key, n => n.Value, "LoadYamlRules", null, null); - - RaiseProgress(); - - var itemSet = tree.ToDictionary(kv => kv.Key.ToLowerInvariant(), kv => wrap(new MiniYamlNode(kv.Key, kv.Value))); - RaiseProgress(); - return itemSet; - } - - Dictionary LoadTileSets(IReadOnlyFileSystem fileSystem, Dictionary itemCache, string[] files) - { - var items = new Dictionary(); - - foreach (var file in files) - { - TileSet t; - if (itemCache.TryGetValue(file, out t)) - items.Add(t.Id, t); - else - { - t = new TileSet(fileSystem, file); - itemCache.Add(file, t); - items.Add(t.Id, t); - } - } - - return items; - } - } -} diff --git a/OpenRA.Game/Map/Map.cs b/OpenRA.Game/Map/Map.cs index 93d46a5996..b39878377a 100644 --- a/OpenRA.Game/Map/Map.cs +++ b/OpenRA.Game/Map/Map.cs @@ -295,7 +295,7 @@ namespace OpenRA { try { - return modData.RulesetCache.Load(this, this); + return Ruleset.LoadFromMap(modData, this); } catch (Exception e) { @@ -303,7 +303,7 @@ namespace OpenRA Log.Write("debug", "Failed to load rules for {0} with error {1}", Title, e.Message); } - return modData.DefaultRules; + return Ruleset.LoadDefaultsForTileSet(modData, Tileset); }); var tl = new MPos(0, 0).ToCPos(this); diff --git a/OpenRA.Game/ModData.cs b/OpenRA.Game/ModData.cs index 41da95519e..1792eaa620 100644 --- a/OpenRA.Game/ModData.cs +++ b/OpenRA.Game/ModData.cs @@ -30,7 +30,6 @@ namespace OpenRA public readonly ISoundLoader[] SoundLoaders; public readonly ISpriteLoader[] SpriteLoaders; public readonly ISpriteSequenceLoader SpriteSequenceLoader; - public readonly RulesetCache RulesetCache; public ILoadScreen LoadScreen { get; private set; } public VoxelLoader VoxelLoader { get; private set; } public CursorProvider CursorProvider { get; private set; } @@ -63,8 +62,6 @@ namespace OpenRA } WidgetLoader = new WidgetLoader(this); - RulesetCache = new RulesetCache(this); - RulesetCache.LoadingProgress += HandleLoadingProgress; MapCache = new MapCache(this); SoundLoaders = GetLoaders(Manifest.SoundFormats, "sound"); @@ -79,7 +76,7 @@ namespace OpenRA SpriteSequenceLoader = (ISpriteSequenceLoader)ctor.Invoke(new[] { this }); SpriteSequenceLoader.OnMissingSpriteError = s => Log.Write("debug", s); - defaultRules = Exts.Lazy(() => RulesetCache.Load(DefaultFileSystem)); + defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this)); defaultTileSets = Exts.Lazy(() => { var items = new Dictionary(); @@ -104,12 +101,14 @@ namespace OpenRA // HACK: Only update the loading screen if we're in the main thread. int initialThreadId; - void HandleLoadingProgress(object sender, EventArgs e) + internal void HandleLoadingProgress() { - if (LoadScreen != null && System.Threading.Thread.CurrentThread.ManagedThreadId == initialThreadId) + if (LoadScreen != null && IsOnMainThread) LoadScreen.Display(); } + internal bool IsOnMainThread { get { return System.Threading.Thread.CurrentThread.ManagedThreadId == initialThreadId; } } + public void InitializeLoaders(IReadOnlyFileSystem fileSystem) { // all this manipulation of static crap here is nasty and breaks diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 983eeceb19..d75b30c3ee 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -215,7 +215,6 @@ - diff --git a/OpenRA.Mods.Common/UtilityCommands/CheckYaml.cs b/OpenRA.Mods.Common/UtilityCommands/CheckYaml.cs index 943fb2edb2..ee47648cdb 100644 --- a/OpenRA.Mods.Common/UtilityCommands/CheckYaml.cs +++ b/OpenRA.Mods.Common/UtilityCommands/CheckYaml.cs @@ -127,7 +127,6 @@ namespace OpenRA.Mods.Common.UtilityCommands { try { - modData.RulesetCache.Load(map ?? modData.DefaultFileSystem, map); var customRulesPass = (ILintRulesPass)modData.ObjectCreator.CreateBasic(customRulesPassType); customRulesPass.Run(EmitError, EmitWarning, rules); } diff --git a/OpenRA.Mods.Common/UtilityCommands/ExtractLanguageStringsCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ExtractLanguageStringsCommand.cs index 0534c7d6ba..712143d64b 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ExtractLanguageStringsCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ExtractLanguageStringsCommand.cs @@ -30,7 +30,6 @@ namespace OpenRA.Mods.Common.UtilityCommands { // HACK: The engine code assumes that Game.modData is set. Game.ModData = modData; - modData.RulesetCache.Load(modData.DefaultFileSystem); var types = Game.ModData.ObjectCreator.GetTypes(); var translatableFields = types.SelectMany(t => t.GetFields()) diff --git a/OpenRA.Mods.Common/UtilityCommands/RemapShpCommand.cs b/OpenRA.Mods.Common/UtilityCommands/RemapShpCommand.cs index f18f0bc33d..9b892dc179 100644 --- a/OpenRA.Mods.Common/UtilityCommands/RemapShpCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/RemapShpCommand.cs @@ -42,15 +42,13 @@ namespace OpenRA.Mods.Common.UtilityCommands var srcModData = new ModData(srcMod); Game.ModData = srcModData; - var srcRules = srcModData.RulesetCache.Load(srcModData.DefaultFileSystem); - var srcPaletteInfo = srcRules.Actors["player"].TraitInfo(); + var srcPaletteInfo = srcModData.DefaultRules.Actors["player"].TraitInfo(); var srcRemapIndex = srcPaletteInfo.RemapIndex; var destMod = args[2].Split(':')[0]; var destModData = new ModData(destMod); Game.ModData = destModData; - var destRules = destModData.RulesetCache.Load(destModData.DefaultFileSystem); - var destPaletteInfo = destRules.Actors["player"].TraitInfo(); + var destPaletteInfo = destModData.DefaultRules.Actors["player"].TraitInfo(); var destRemapIndex = destPaletteInfo.RemapIndex; var shadowIndex = new int[] { }; diff --git a/OpenRA.Mods.D2k/UtilityCommands/ImportD2kMapCommand.cs b/OpenRA.Mods.D2k/UtilityCommands/ImportD2kMapCommand.cs index e57c34c162..b26530ba71 100644 --- a/OpenRA.Mods.D2k/UtilityCommands/ImportD2kMapCommand.cs +++ b/OpenRA.Mods.D2k/UtilityCommands/ImportD2kMapCommand.cs @@ -30,8 +30,7 @@ namespace OpenRA.Mods.D2k.UtilityCommands // HACK: The engine code assumes that Game.modData is set. Game.ModData = modData; - var rules = modData.RulesetCache.Load(modData.DefaultFileSystem); - + var rules = Ruleset.LoadDefaultsForTileSet(modData, "ARRAKIS"); var map = D2kMapImporter.Import(args[1], modData.Manifest.Mod.Id, args[2], rules); if (map == null)