#region Copyright & License Information /* * Copyright 2007-2015 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. 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 { public interface IGlobalModData { } public sealed class SpriteSequenceFormat : IGlobalModData { public readonly string Type; public readonly IReadOnlyDictionary Metadata; public SpriteSequenceFormat(MiniYaml yaml) { Type = yaml.Value; Metadata = new ReadOnlyDictionary(yaml.ToDictionary()); } } /// Describes what is to be loaded in order to run a mod. public class Manifest { public readonly ModMetadata Mod; public readonly string[] Packages, Rules, ServerTraits, Sequences, VoxelSequences, Cursors, Chrome, Assemblies, ChromeLayout, Weapons, Voices, Notifications, Music, Translations, TileSets, ChromeMetrics, MapCompatibility, Missions; public readonly IReadOnlyDictionary MapFolders; public readonly MiniYaml LoadScreen; public readonly MiniYaml LobbyDefaults; public readonly Dictionary RequiresMods; public readonly Dictionary> Fonts; public readonly string[] SoundFormats = { }; public readonly string[] SpriteFormats = { }; readonly string[] reservedModuleNames = { "Metadata", "Folders", "MapFolders", "Packages", "Rules", "Sequences", "VoxelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons", "Voices", "Notifications", "Music", "Translations", "TileSets", "ChromeMetrics", "Missions", "ServerTraits", "LoadScreen", "LobbyDefaults", "Fonts", "SupportsMapsFrom", "SoundFormats", "SpriteFormats", "RequiresMods" }; readonly TypeDictionary modules = new TypeDictionary(); readonly Dictionary yaml; public Manifest(string modId) { var package = ModMetadata.AllMods[modId].Package; yaml = new MiniYaml(null, MiniYaml.FromStream(package.GetStream("mod.yaml"))).ToDictionary(); Mod = FieldLoader.Load(yaml["Metadata"]); Mod.Id = modId; // TODO: Use fieldloader MapFolders = YamlDictionary(yaml, "MapFolders", true); Packages = YamlList(yaml, "Packages", true); Rules = YamlList(yaml, "Rules", true); Sequences = YamlList(yaml, "Sequences", true); VoxelSequences = YamlList(yaml, "VoxelSequences", true); Cursors = YamlList(yaml, "Cursors", true); Chrome = YamlList(yaml, "Chrome", true); Assemblies = YamlList(yaml, "Assemblies", true); ChromeLayout = YamlList(yaml, "ChromeLayout", true); Weapons = YamlList(yaml, "Weapons", true); Voices = YamlList(yaml, "Voices", true); Notifications = YamlList(yaml, "Notifications", true); Music = YamlList(yaml, "Music", true); Translations = YamlList(yaml, "Translations", true); TileSets = YamlList(yaml, "TileSets", true); ChromeMetrics = YamlList(yaml, "ChromeMetrics", true); Missions = YamlList(yaml, "Missions", true); ServerTraits = YamlList(yaml, "ServerTraits"); if (!yaml.TryGetValue("LoadScreen", out LoadScreen)) throw new InvalidDataException("`LoadScreen` section is not defined."); if (!yaml.TryGetValue("LobbyDefaults", out LobbyDefaults)) throw new InvalidDataException("`LobbyDefaults` section is not defined."); Fonts = yaml["Fonts"].ToDictionary(my => { var nd = my.ToDictionary(); return Pair.New(nd["Font"].Value, Exts.ParseIntegerInvariant(nd["Size"].Value)); }); RequiresMods = yaml["RequiresMods"].ToDictionary(my => my.Value); // Allow inherited mods to import parent maps. var compat = new List { Mod.Id }; if (yaml.ContainsKey("SupportsMapsFrom")) compat.AddRange(yaml["SupportsMapsFrom"].Value.Split(',').Select(c => c.Trim())); MapCompatibility = compat.ToArray(); if (yaml.ContainsKey("SoundFormats")) SoundFormats = FieldLoader.GetValue("SoundFormats", yaml["SoundFormats"].Value); if (yaml.ContainsKey("SpriteFormats")) SpriteFormats = FieldLoader.GetValue("SpriteFormats", yaml["SpriteFormats"].Value); } public void LoadCustomData(ObjectCreator oc) { foreach (var kv in yaml) { if (reservedModuleNames.Contains(kv.Key)) continue; var t = oc.FindType(kv.Key); if (t == null || !typeof(IGlobalModData).IsAssignableFrom(t)) throw new InvalidDataException("`{0}` is not a valid mod manifest entry.".F(kv.Key)); 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[] { kv.Value }); } else { // Automatically load the child nodes using FieldLoader module = oc.CreateObject(kv.Key); FieldLoader.Load(module, kv.Value); } modules.Add(module); } } static string[] YamlList(Dictionary yaml, string key, bool parsePaths = false) { if (!yaml.ContainsKey(key)) return new string[] { }; if (parsePaths) return yaml[key].Nodes.Select(node => Platform.ResolvePath(node.Key, node.Value.Value ?? string.Empty)).ToArray(); return yaml[key].ToDictionary().Keys.ToArray(); } static IReadOnlyDictionary YamlDictionary(Dictionary yaml, string key, bool parsePaths = false) { if (!yaml.ContainsKey(key)) return new ReadOnlyDictionary(); var inner = new Dictionary(); foreach (var node in yaml[key].Nodes) { var line = node.Key; if (node.Value.Value != null) line += ":" + node.Value.Value; // '@' may be used in mod.yaml to indicate extra information (similar to trait @ tags). // Applies to MapFolders (to indicate System and User directories) and Packages (to indicate package annotation). if (line.Contains('@')) { var split = line.Split('@'); inner.Add(parsePaths ? Platform.ResolvePath(split[0]) : split[0], split[1]); } else inner.Add(line, null); } return new ReadOnlyDictionary(inner); } public T Get() where T : IGlobalModData { var module = modules.GetOrDefault(); // Lazily create the default values if not explicitly defined. if (module == null) { module = (T)Game.ModData.ObjectCreator.CreateBasic(typeof(T)); modules.Add(module); } return module; } } }