Rework mod enumeration / caching.

- Replaced ModMetadata.AllMods with Game.Mods.
- Store / reference mod Manifest instead of ModMetadata.
- Removes engine dependency on ModContent class.
This commit is contained in:
Paul Chote
2016-08-05 17:07:04 +01:00
parent 45a596953e
commit 3df9efb95d
33 changed files with 301 additions and 219 deletions

View File

@@ -13,7 +13,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using OpenRA.Primitives;
namespace OpenRA.FileSystem
@@ -34,9 +33,15 @@ namespace OpenRA.FileSystem
// Mod packages that should not be disposed
readonly List<IReadOnlyPackage> modPackages = new List<IReadOnlyPackage>();
readonly IReadOnlyDictionary<string, Manifest> installedMods;
Cache<string, List<IReadOnlyPackage>> fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
public FileSystem(IReadOnlyDictionary<string, Manifest> installedMods)
{
this.installedMods = installedMods;
}
public IReadOnlyPackage OpenPackage(string filename)
{
if (filename.EndsWith(".mix", StringComparison.InvariantCultureIgnoreCase))
@@ -108,9 +113,9 @@ namespace OpenRA.FileSystem
{
name = name.Substring(1);
ModMetadata mod;
if (!ModMetadata.AllMods.TryGetValue(name, out mod))
throw new InvalidOperationException("Could not load mod '{0}'. Available mods: {1}".F(name, ModMetadata.AllMods.Keys.JoinWith(", ")));
Manifest mod;
if (!installedMods.TryGetValue(name, out mod))
throw new InvalidOperationException("Could not load mod '{0}'. Available mods: {1}".F(name, installedMods.Keys.JoinWith(", ")));
package = mod.Package;
modPackages.Add(package);

View File

@@ -21,6 +21,7 @@ using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using OpenRA.Chat;
using OpenRA.FileSystem;
using OpenRA.Graphics;
using OpenRA.Network;
using OpenRA.Primitives;
@@ -35,6 +36,8 @@ namespace OpenRA
public const int Timestep = 40;
public const int TimestepJankThreshold = 250; // Don't catch up for delays larger than 250ms
public static InstalledMods Mods { get; private set; }
public static ModData ModData;
public static Settings Settings;
public static ICursor Cursor;
@@ -300,22 +303,23 @@ namespace OpenRA
GlobalChat = new GlobalChat();
Mods = new InstalledMods();
Console.WriteLine("Available mods:");
foreach (var mod in ModMetadata.AllMods)
Console.WriteLine("\t{0}: {1} ({2})", mod.Key, mod.Value.Title, mod.Value.Version);
foreach (var mod in Mods)
Console.WriteLine("\t{0}: {1} ({2})", mod.Key, mod.Value.Mod.Title, mod.Value.Mod.Version);
InitializeMod(Settings.Game.Mod, args);
}
public static bool IsModInstalled(string modId)
{
return ModMetadata.AllMods[modId].RequiresMods.All(IsModInstalled);
return Mods.ContainsKey(modId) && Mods[modId].RequiresMods.All(IsModInstalled);
}
public static bool IsModInstalled(KeyValuePair<string, string> mod)
{
return ModMetadata.AllMods.ContainsKey(mod.Key)
&& ModMetadata.AllMods[mod.Key].Version == mod.Value
return Mods.ContainsKey(mod.Key)
&& Mods[mod.Key].Mod.Version == mod.Value
&& IsModInstalled(mod.Key);
}
@@ -347,7 +351,7 @@ namespace OpenRA
ModData = null;
// Fall back to default if the mod doesn't exist or has missing prerequisites.
if (!ModMetadata.AllMods.ContainsKey(mod) || !IsModInstalled(mod))
if (!IsModInstalled(mod))
mod = new GameSettings().Mod;
Console.WriteLine("Loading mod: {0}", mod);
@@ -355,7 +359,7 @@ namespace OpenRA
Sound.StopVideo();
ModData = new ModData(mod, true);
ModData = new ModData(Mods[mod], Mods, true);
using (new PerfTimer("LoadMaps"))
ModData.MapCache.LoadMaps();
@@ -468,7 +472,7 @@ namespace OpenRA
ThreadPool.QueueUserWorkItem(_ =>
{
var mod = ModData.Manifest.Mod;
var directory = Platform.ResolvePath("^", "Screenshots", mod.Id, mod.Version);
var directory = Platform.ResolvePath("^", "Screenshots", ModData.Manifest.Id, mod.Version);
Directory.CreateDirectory(directory);
var filename = TimestampedFilename();

View File

@@ -16,10 +16,12 @@ namespace OpenRA
public class Utility
{
public readonly ModData ModData;
public readonly InstalledMods Mods;
public Utility(ModData modData)
public Utility(ModData modData, InstalledMods mods)
{
ModData = modData;
Mods = mods;
}
}

View File

@@ -0,0 +1,117 @@
#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;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Primitives;
namespace OpenRA
{
public class InstalledMods : IReadOnlyDictionary<string, Manifest>
{
readonly Dictionary<string, Manifest> mods;
public InstalledMods()
{
mods = GetInstalledMods();
}
static IEnumerable<Pair<string, string>> GetCandidateMods()
{
// Get mods that are in the game folder.
var basePath = Platform.ResolvePath(Path.Combine(".", "mods"));
var mods = Directory.GetDirectories(basePath)
.Select(x => Pair.New(x.Substring(basePath.Length + 1), x))
.ToList();
foreach (var m in Directory.GetFiles(basePath, "*.oramod"))
mods.Add(Pair.New(Path.GetFileNameWithoutExtension(m), m));
// Get mods that are in the support folder.
var supportPath = Platform.ResolvePath(Path.Combine("^", "mods"));
if (!Directory.Exists(supportPath))
return mods;
foreach (var pair in Directory.GetDirectories(supportPath).ToDictionary(x => x.Substring(supportPath.Length + 1)))
mods.Add(Pair.New(pair.Key, pair.Value));
foreach (var m in Directory.GetFiles(supportPath, "*.oramod"))
mods.Add(Pair.New(Path.GetFileNameWithoutExtension(m), m));
return mods;
}
static Manifest LoadMod(string id, string path)
{
IReadOnlyPackage package = null;
try
{
if (Directory.Exists(path))
package = new Folder(path);
else
{
try
{
using (var fileStream = File.OpenRead(path))
package = new ZipFile(fileStream, path);
}
catch
{
throw new InvalidDataException(path + " is not a valid mod package");
}
}
if (!package.Contains("mod.yaml"))
throw new InvalidDataException(path + " is not a valid mod package");
// Mods in the support directory and oramod packages (which are listed later
// in the CandidateMods list) override mods in the main install.
return new Manifest(id, package);
}
catch (Exception)
{
if (package != null)
package.Dispose();
return null;
}
}
static Dictionary<string, Manifest> GetInstalledMods()
{
var ret = new Dictionary<string, Manifest>();
foreach (var pair in GetCandidateMods())
{
var mod = LoadMod(pair.First, pair.Second);
// Mods in the support directory and oramod packages (which are listed later
// in the CandidateMods list) override mods in the main install.
if (mod != null)
ret[pair.First] = mod;
}
return ret;
}
public Manifest this[string key] { get { return mods[key]; } }
public int Count { get { return mods.Count; } }
public ICollection<string> Keys { get { return mods.Keys; } }
public ICollection<Manifest> Values { get { return mods.Values; } }
public bool ContainsKey(string key) { return mods.ContainsKey(key); }
public IEnumerator<KeyValuePair<string, Manifest>> GetEnumerator() { return mods.GetEnumerator(); }
public bool TryGetValue(string key, out Manifest value) { return mods.TryGetValue(key, out value); }
IEnumerator IEnumerable.GetEnumerator() { return mods.GetEnumerator(); }
}
}

View File

@@ -34,6 +34,8 @@ namespace OpenRA
/// <summary> Describes what is to be loaded in order to run a mod. </summary>
public class Manifest
{
public readonly string Id;
public readonly IReadOnlyPackage Package;
public readonly ModMetadata Mod;
public readonly string[]
Rules, ServerTraits,
@@ -60,14 +62,15 @@ namespace OpenRA
readonly TypeDictionary modules = new TypeDictionary();
readonly Dictionary<string, MiniYaml> yaml;
public Manifest(string modId)
{
var package = ModMetadata.AllMods[modId].Package;
bool customDataLoaded;
public Manifest(string modId, IReadOnlyPackage package)
{
Id = modId;
Package = package;
yaml = new MiniYaml(null, MiniYaml.FromStream(package.GetStream("mod.yaml"), "mod.yaml")).ToDictionary();
Mod = FieldLoader.Load<ModMetadata>(yaml["Metadata"]);
Mod.Id = modId;
// TODO: Use fieldloader
MapFolders = YamlDictionary(yaml, "MapFolders");
@@ -106,7 +109,7 @@ namespace OpenRA
RequiresMods = yaml["RequiresMods"].ToDictionary(my => my.Value);
// Allow inherited mods to import parent maps.
var compat = new List<string> { Mod.Id };
var compat = new List<string> { Id };
if (yaml.ContainsKey("SupportsMapsFrom"))
compat.AddRange(yaml["SupportsMapsFrom"].Value.Split(',').Select(c => c.Trim()));
@@ -147,6 +150,8 @@ namespace OpenRA
modules.Add(module);
}
customDataLoaded = true;
}
static string[] YamlList(Dictionary<string, MiniYaml> yaml, string key, bool parsePaths = false)
@@ -171,8 +176,12 @@ namespace OpenRA
return modules.Contains<T>();
}
/// <summary>Load a cached IGlobalModData instance.</summary>
public T Get<T>() where T : IGlobalModData
{
if (!customDataLoaded)
throw new InvalidOperationException("Attempted to call Manifest.Get() before loading custom data!");
var module = modules.GetOrDefault<T>();
// Lazily create the default values if not explicitly defined.
@@ -184,5 +193,36 @@ namespace OpenRA
return module;
}
/// <summary>
/// Load an uncached IGlobalModData instance directly from the manifest yaml.
/// This should only be used by external mods that want to query data from this mod.
/// </summary>
public T Get<T>(ObjectCreator oc) where T : IGlobalModData
{
MiniYaml data;
var t = typeof(T);
if (!yaml.TryGetValue(t.Name, out data))
{
// Lazily create the default values if not explicitly defined.
return (T)oc.CreateBasic(t);
}
IGlobalModData module;
var ctor = t.GetConstructor(new[] { typeof(MiniYaml) });
if (ctor != null)
{
// Class has opted-in to DIY initialization
module = (IGlobalModData)ctor.Invoke(new object[] { data.Value });
}
else
{
// Automatically load the child nodes using FieldLoader
module = oc.CreateObject<IGlobalModData>(t.Name);
FieldLoader.Load(module, data);
}
return (T)module;
}
}
}

View File

@@ -33,7 +33,7 @@ namespace OpenRA
public ILoadScreen LoadScreen { get; private set; }
public VoxelLoader VoxelLoader { get; private set; }
public CursorProvider CursorProvider { get; private set; }
public FS ModFiles = new FS();
public FS ModFiles;
public IReadOnlyFileSystem DefaultFileSystem { get { return ModFiles; } }
readonly Lazy<Ruleset> defaultRules;
@@ -45,10 +45,14 @@ namespace OpenRA
readonly Lazy<IReadOnlyDictionary<string, SequenceProvider>> defaultSequences;
public IReadOnlyDictionary<string, SequenceProvider> DefaultSequences { get { return defaultSequences.Value; } }
public ModData(string mod, bool useLoadScreen = false)
public ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false)
{
Languages = new string[0];
Manifest = new Manifest(mod);
ModFiles = new FS(mods);
// Take a local copy of the manifest
Manifest = new Manifest(mod.Id, mod.Package);
ModFiles.LoadFromManifest(Manifest);
ObjectCreator = new ObjectCreator(Manifest, ModFiles);

View File

@@ -9,118 +9,14 @@
*/
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Primitives;
namespace OpenRA
{
public class ModMetadata
{
public static readonly Dictionary<string, ModMetadata> AllMods = ValidateMods();
public string Id;
public string Title;
public string Description;
public string Version;
public string Author;
public bool Hidden;
public Dictionary<string, string> RequiresMods;
public ModContent ModContent;
public IReadOnlyPackage Package;
static Dictionary<string, ModMetadata> ValidateMods()
{
var ret = new Dictionary<string, ModMetadata>();
foreach (var pair in GetCandidateMods())
{
IReadOnlyPackage package = null;
try
{
if (Directory.Exists(pair.Second))
package = new Folder(pair.Second);
else
{
try
{
using (var fileStream = File.OpenRead(pair.Second))
package = new ZipFile(fileStream, pair.Second);
}
catch
{
throw new InvalidDataException(pair.Second + " is not a valid mod package");
}
}
if (!package.Contains("mod.yaml"))
{
package.Dispose();
continue;
}
var yaml = new MiniYaml(null, MiniYaml.FromStream(package.GetStream("mod.yaml"), "mod.yaml"));
var nd = yaml.ToDictionary();
if (!nd.ContainsKey("Metadata"))
{
package.Dispose();
continue;
}
var metadata = FieldLoader.Load<ModMetadata>(nd["Metadata"]);
metadata.Id = pair.First;
metadata.Package = package;
if (nd.ContainsKey("RequiresMods"))
metadata.RequiresMods = nd["RequiresMods"].ToDictionary(my => my.Value);
else
metadata.RequiresMods = new Dictionary<string, string>();
if (nd.ContainsKey("ModContent"))
metadata.ModContent = FieldLoader.Load<ModContent>(nd["ModContent"]);
// Mods in the support directory and oramod packages (which are listed later
// in the CandidateMods list) override mods in the main install.
ret[pair.First] = metadata;
}
catch (Exception ex)
{
if (package != null)
package.Dispose();
Console.WriteLine("An exception occurred when trying to load ModMetadata for `{0}`:".F(pair.First));
Console.WriteLine(ex.Message);
}
}
return ret;
}
static IEnumerable<Pair<string, string>> GetCandidateMods()
{
// Get mods that are in the game folder.
var basePath = Platform.ResolvePath(Path.Combine(".", "mods"));
var mods = Directory.GetDirectories(basePath)
.Select(x => Pair.New(x.Substring(basePath.Length + 1), x))
.ToList();
foreach (var m in Directory.GetFiles(basePath, "*.oramod"))
mods.Add(Pair.New(Path.GetFileNameWithoutExtension(m), m));
// Get mods that are in the support folder.
var supportPath = Platform.ResolvePath(Path.Combine("^", "mods"));
if (!Directory.Exists(supportPath))
return mods;
foreach (var pair in Directory.GetDirectories(supportPath).ToDictionary(x => x.Substring(supportPath.Length + 1)))
mods.Add(Pair.New(pair.Key, pair.Value));
foreach (var m in Directory.GetFiles(supportPath, "*.oramod"))
mods.Add(Pair.New(Path.GetFileNameWithoutExtension(m), m));
return mods;
}
}
}

View File

@@ -38,14 +38,14 @@ namespace OpenRA.Network
{
FieldLoader.Load(this, yaml);
ModMetadata mod;
Manifest mod;
var modVersion = Mods.Split('@');
if (modVersion.Length == 2 && ModMetadata.AllMods.TryGetValue(modVersion[0], out mod))
if (modVersion.Length == 2 && Game.Mods.TryGetValue(modVersion[0], out mod))
{
ModId = modVersion[0];
ModVersion = modVersion[1];
ModLabel = "{0} ({1})".F(mod.Title, modVersion[1]);
IsCompatible = Game.Settings.Debug.IgnoreVersionMismatch || ModVersion == mod.Version;
ModLabel = "{0} ({1})".F(mod.Mod.Title, modVersion[1]);
IsCompatible = Game.Settings.Debug.IgnoreVersionMismatch || ModVersion == mod.Mod.Version;
}
else
ModLabel = "Unknown mod: {0}".F(Mods);

View File

@@ -44,8 +44,8 @@ namespace OpenRA.Network
void StartSavingReplay(byte[] initialContent)
{
var filename = chooseFilename();
var mod = Game.ModData.Manifest.Mod;
var dir = Platform.ResolvePath("^", "Replays", mod.Id, mod.Version);
var mod = Game.ModData.Manifest;
var dir = Platform.ResolvePath("^", "Replays", mod.Id, mod.Mod.Version);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);

View File

@@ -132,13 +132,13 @@ namespace OpenRA.Network
case "HandshakeRequest":
{
// Switch to the server's mod if we need and are able to
var mod = Game.ModData.Manifest.Mod;
var mod = Game.ModData.Manifest;
var request = HandshakeRequest.Deserialize(order.TargetString);
ModMetadata serverMod;
Manifest serverMod;
if (request.Mod != mod.Id &&
ModMetadata.AllMods.TryGetValue(request.Mod, out serverMod) &&
serverMod.Version == request.Version)
Game.Mods.TryGetValue(request.Mod, out serverMod) &&
serverMod.Mod.Version == request.Version)
{
var replay = orderManager.Connection as ReplayConnection;
var launchCommand = replay != null ?
@@ -170,7 +170,7 @@ namespace OpenRA.Network
{
Client = info,
Mod = mod.Id,
Version = mod.Version,
Version = mod.Mod.Version,
Password = orderManager.Password
};

View File

@@ -245,6 +245,7 @@
<Compile Include="FileSystem\ZipFolder.cs" />
<Compile Include="Primitives\float3.cs" />
<Compile Include="ModContent.cs" />
<Compile Include="InstalledMods.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="FileSystem\D2kSoundResources.cs" />

View File

@@ -158,7 +158,7 @@ namespace OpenRA.Server
foreach (var t in serverTraits.WithInterface<INotifyServerStart>())
t.ServerStarted(this);
Log.Write("server", "Initial mod: {0}", ModData.Manifest.Mod.Id);
Log.Write("server", "Initial mod: {0}", ModData.Manifest.Id);
Log.Write("server", "Initial map: {0}", LobbyInfo.GlobalSettings.Map);
var timeout = serverTraits.WithInterface<ITick>().Min(t => t.TickTimeout);
@@ -262,7 +262,7 @@ namespace OpenRA.Server
// Dispatch a handshake order
var request = new HandshakeRequest
{
Mod = ModData.Manifest.Mod.Id,
Mod = ModData.Manifest.Id,
Version = ModData.Manifest.Mod.Version,
Map = LobbyInfo.GlobalSettings.Map
};
@@ -327,7 +327,7 @@ namespace OpenRA.Server
else
client.Color = HSLColor.FromRGB(255, 255, 255);
if (ModData.Manifest.Mod.Id != handshake.Mod)
if (ModData.Manifest.Id != handshake.Mod)
{
Log.Write("server", "Rejected connection from {0}; mods do not match.",
newConn.Socket.RemoteEndPoint);

View File

@@ -176,7 +176,7 @@ namespace OpenRA
gameInfo = new GameInformation
{
Mod = Game.ModData.Manifest.Mod.Id,
Mod = Game.ModData.Manifest.Id,
Version = Game.ModData.Manifest.Mod.Version,
MapUid = Map.Uid,