diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index 848eaa2e4e..6d42f0ef8a 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -191,7 +191,7 @@ namespace OpenRA public static void InitializeSettings(Arguments args) { - Settings = new Settings(Platform.ResolvePath("^", "settings.yaml"), args); + Settings = new Settings(Platform.ResolvePath(Path.Combine("^", "settings.yaml")), args); } internal static void Initialize(Arguments args) diff --git a/OpenRA.Game/Manifest.cs b/OpenRA.Game/Manifest.cs index ee962e4e82..8591017de5 100644 --- a/OpenRA.Game/Manifest.cs +++ b/OpenRA.Game/Manifest.cs @@ -29,7 +29,7 @@ namespace OpenRA } } - // Describes what is to be loaded in order to run a mod + /// Describes what is to be loaded in order to run a mod. public class Manifest { public static readonly Dictionary AllMods = LoadMods(); @@ -61,13 +61,16 @@ namespace OpenRA readonly TypeDictionary modules = new TypeDictionary(); readonly Dictionary yaml; - public Manifest(string mod) + public Manifest(string modId, string modPath = null) { - var path = Platform.ResolvePath(".", "mods", mod, "mod.yaml"); + if (modPath == null) + modPath = ModMetadata.CandidateModPaths[modId]; + + var path = Path.Combine(modPath, "mod.yaml"); yaml = new MiniYaml(null, MiniYaml.FromFile(path)).ToDictionary(); Mod = FieldLoader.Load(yaml["Metadata"]); - Mod.Id = mod; + Mod.Id = modId; // TODO: Use fieldloader Folders = YamlList(yaml, "Folders", true); @@ -106,12 +109,10 @@ namespace OpenRA RequiresMods = yaml["RequiresMods"].ToDictionary(my => my.Value); // Allow inherited mods to import parent maps. - var compat = new List(); - compat.Add(mod); + var compat = new List { Mod.Id }; if (yaml.ContainsKey("SupportsMapsFrom")) - foreach (var c in yaml["SupportsMapsFrom"].Value.Split(',')) - compat.Add(c.Trim()); + compat.AddRange(yaml["SupportsMapsFrom"].Value.Split(',').Select(c => c.Trim())); MapCompatibility = compat.ToArray(); @@ -156,8 +157,10 @@ namespace OpenRA if (!yaml.ContainsKey(key)) return new string[] { }; - var list = yaml[key].ToDictionary().Keys.ToArray(); - return parsePaths ? list.Select(Platform.ResolvePath).ToArray() : list; + 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) @@ -168,15 +171,19 @@ namespace OpenRA 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 (node.Key.Contains('@')) + if (line.Contains('@')) { - var split = node.Key.Split('@'); - inner.Add(split[0], split[1]); + var split = line.Split('@'); + inner.Add(parsePaths ? Platform.ResolvePath(split[0]) : split[0], split[1]); } else - inner.Add(node.Key, null); + inner.Add(line, null); } return new ReadOnlyDictionary(inner); @@ -198,20 +205,16 @@ namespace OpenRA static Dictionary LoadMods() { - var basePath = Platform.ResolvePath(".", "mods"); - var mods = Directory.GetDirectories(basePath) - .Select(x => x.Substring(basePath.Length + 1)); - var ret = new Dictionary(); - foreach (var mod in mods) + foreach (var mod in ModMetadata.CandidateModPaths) { - if (!File.Exists(Platform.ResolvePath(".", "mods", mod, "mod.yaml"))) + if (!File.Exists(Path.Combine(mod.Value, "mod.yaml"))) continue; try { - var manifest = new Manifest(mod); - ret.Add(mod, manifest); + var manifest = new Manifest(mod.Key, mod.Value); + ret.Add(mod.Key, manifest); } catch (Exception ex) { diff --git a/OpenRA.Game/ModMetadata.cs b/OpenRA.Game/ModMetadata.cs index f99f7bb5e8..2d24c3fc73 100644 --- a/OpenRA.Game/ModMetadata.cs +++ b/OpenRA.Game/ModMetadata.cs @@ -17,6 +17,7 @@ namespace OpenRA { public class ModMetadata { + public static readonly Dictionary CandidateModPaths = GetCandidateMods(); public static readonly Dictionary AllMods = ValidateMods(); public string Id; @@ -24,21 +25,19 @@ namespace OpenRA public string Description; public string Version; public string Author; + public string LogoImagePath; + public string PreviewImagePath; public bool Hidden; public ContentInstaller Content; static Dictionary ValidateMods() { - var basePath = Platform.ResolvePath(".", "mods"); - var mods = Directory.GetDirectories(basePath) - .Select(x => x.Substring(basePath.Length + 1)); - var ret = new Dictionary(); - foreach (var m in mods) + foreach (var pair in CandidateModPaths) { try { - var yamlPath = Platform.ResolvePath(".", "mods", m, "mod.yaml"); + var yamlPath = Path.Combine(pair.Value, "mod.yaml"); if (!File.Exists(yamlPath)) continue; @@ -47,22 +46,40 @@ namespace OpenRA if (!nd.ContainsKey("Metadata")) continue; - var mod = FieldLoader.Load(nd["Metadata"]); - mod.Id = m; + var metadata = FieldLoader.Load(nd["Metadata"]); + metadata.Id = pair.Key; if (nd.ContainsKey("ContentInstaller")) - mod.Content = FieldLoader.Load(nd["ContentInstaller"]); + metadata.Content = FieldLoader.Load(nd["ContentInstaller"]); - ret.Add(m, mod); + ret.Add(pair.Key, metadata); } catch (Exception ex) { - Console.WriteLine("An exception occurred when trying to load ModMetadata for `{0}`:".F(m)); + Console.WriteLine("An exception occurred when trying to load ModMetadata for `{0}`:".F(pair.Key)); Console.WriteLine(ex.Message); } } return ret; } + + static Dictionary GetCandidateMods() + { + // Get mods that are in the game folder. + var basePath = Platform.ResolvePath(Path.Combine(".", "mods")); + var mods = Directory.GetDirectories(basePath) + .ToDictionary(x => x.Substring(basePath.Length + 1)); + + // 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.Key, pair.Value); + + return mods; + } } } diff --git a/OpenRA.Game/Platform.cs b/OpenRA.Game/Platform.cs index 62da9a8548..260684d20e 100644 --- a/OpenRA.Game/Platform.cs +++ b/OpenRA.Game/Platform.cs @@ -95,11 +95,19 @@ namespace OpenRA public static string GameDir { get { return AppDomain.CurrentDomain.BaseDirectory; } } - /// Replace special character prefixes with full paths + /// Replaces special character prefixes with full paths. public static string ResolvePath(string path) { path = path.TrimEnd(new char[] { ' ', '\t' }); + // If the path contains ':', chances are it is a package path. + // If it isn't, someone passed an already resolved path, which is wrong. + if (path.IndexOf(":", StringComparison.Ordinal) > 1) + { + var split = path.Split(':'); + return ResolvePath(split[0], split[1]); + } + // paths starting with ^ are relative to the support dir if (path.StartsWith("^")) path = SupportDir + path.Substring(1); @@ -111,7 +119,17 @@ namespace OpenRA return path; } - /// Replace special character prefixes with full paths + /// Replaces package names with full paths. Avoid using this for non-package paths. + public static string ResolvePath(string package, string target) + { + // Resolve mod package paths. + if (ModMetadata.AllMods.ContainsKey(package)) + package = ModMetadata.CandidateModPaths[package]; + + return ResolvePath(Path.Combine(package, target)); + } + + /// Replace special character prefixes with full paths. public static string ResolvePath(params string[] path) { return ResolvePath(path.Aggregate(Path.Combine)); diff --git a/OpenRA.Game/Renderer.cs b/OpenRA.Game/Renderer.cs index 114ddbd98b..6989ec3ca7 100644 --- a/OpenRA.Game/Renderer.cs +++ b/OpenRA.Game/Renderer.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.IO; using System.Linq; using System.Reflection; using OpenRA.Graphics; @@ -49,7 +50,7 @@ namespace OpenRA var resolution = GetResolution(graphicSettings); var rendererName = serverSettings.Dedicated ? "Null" : graphicSettings.Renderer; - var rendererPath = Platform.ResolvePath(".", "OpenRA.Platforms." + rendererName + ".dll"); + var rendererPath = Platform.ResolvePath(Path.Combine(".", "OpenRA.Platforms." + rendererName + ".dll")); Device = CreateDevice(Assembly.LoadFile(rendererPath), resolution.Width, resolution.Height, graphicSettings.Mode); diff --git a/OpenRA.Game/Sound/Sound.cs b/OpenRA.Game/Sound/Sound.cs index 7f49409871..c78e03b00b 100644 --- a/OpenRA.Game/Sound/Sound.cs +++ b/OpenRA.Game/Sound/Sound.cs @@ -33,7 +33,7 @@ namespace OpenRA public Sound(string engineName) { - var enginePath = Platform.ResolvePath(".", "OpenRA.Platforms." + engineName + ".dll"); + var enginePath = Platform.ResolvePath(Path.Combine(".", "OpenRA.Platforms." + engineName + ".dll")); soundEngine = CreateDevice(Assembly.LoadFile(enginePath)); } diff --git a/OpenRA.Mods.Common/Widgets/Logic/ModBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ModBrowserLogic.cs index bab41933bc..81ad20fc94 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ModBrowserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ModBrowserLogic.cs @@ -82,7 +82,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic { try { - using (var preview = new Bitmap(Platform.ResolvePath(".", "mods", mod.Id, "preview.png"))) + using (var preview = new Bitmap(Platform.ResolvePath(mod.PreviewImagePath))) if (preview.Width == 296 && preview.Height == 196) previews.Add(mod.Id, sheetBuilder.Add(preview)); } @@ -90,7 +90,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic try { - using (var logo = new Bitmap(Platform.ResolvePath(".", "mods", mod.Id, "logo.png"))) + using (var logo = new Bitmap(Platform.ResolvePath(mod.LogoImagePath))) if (logo.Width == 96 && logo.Height == 96) logos.Add(mod.Id, sheetBuilder.Add(logo)); } diff --git a/mods/all/mod.yaml b/mods/all/mod.yaml index de3152368c..da73d59a2e 100644 --- a/mods/all/mod.yaml +++ b/mods/all/mod.yaml @@ -17,7 +17,7 @@ Chrome: Assemblies: ./mods/common/OpenRA.Mods.Common.dll ./mods/ra/OpenRA.Mods.RA.dll - ./mods/d2k/OpenRA.Mods.D2k.dll + d2k:OpenRA.Mods.D2k.dll ./mods/cnc/OpenRA.Mods.Cnc.dll ./mods/ts/OpenRA.Mods.TS.dll diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index 17177eae48..ea251587f2 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -3,6 +3,8 @@ Metadata: Description: Join the Global Defense Initiative or the Brotherhood of Nod in our\nrecreation of the classic game that started it all.\n\nTiberian Dawn modernizes the original Command & Conquer gameplay\nby introducing features from later games, including per-factory\nproduction queues, unit veterancy, and capturable tech structures. Version: {DEV_VERSION} Author: the OpenRA Developers + LogoImagePath: ./mods/cnc/logo.png + PreviewImagePath: ./mods/cnc/preview.png RequiresMods: modchooser: {DEV_VERSION} diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index b8171cf7f1..a94f39688c 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -3,96 +3,98 @@ Metadata: Description: Three great houses fight for the precious spice, melange.\nHe who controls the spice controls the universe!\n\nTry to establish a foothold on the desert planet Arrakis\nwith its harsh environmental conditions and protect your\nharvesting operations from giant sandworms as well as\nruthless enemy factions. Version: {DEV_VERSION} Author: the OpenRA Developers + LogoImagePath: d2k:logo.png + PreviewImagePath: d2k:preview.png RequiresMods: modchooser: {DEV_VERSION} Folders: . - ./mods/d2k - ./mods/d2k/bits - ./mods/d2k/bits/tex - ./mods/d2k/bits/xmas - ./mods/d2k/uibits + d2k: + d2k:bits + d2k:bits/tex + d2k:bits/xmas + d2k:uibits ~^Content/d2k ~^Content/d2k/GAMESFX ~^Content/d2k/Movies ~^Content/d2k/Music MapFolders: - ./mods/d2k/maps@System + d2k:maps@System ~^maps/d2k@User Packages: SOUND.RS Rules: - ./mods/d2k/rules/misc.yaml - ./mods/d2k/rules/ai.yaml - ./mods/d2k/rules/player.yaml - ./mods/d2k/rules/world.yaml - ./mods/d2k/rules/palettes.yaml - ./mods/d2k/rules/defaults.yaml - ./mods/d2k/rules/vehicles.yaml - ./mods/d2k/rules/starport.yaml - ./mods/d2k/rules/husks.yaml - ./mods/d2k/rules/structures.yaml - ./mods/d2k/rules/aircraft.yaml - ./mods/d2k/rules/infantry.yaml - ./mods/d2k/rules/arrakis.yaml + d2k:rules/misc.yaml + d2k:rules/ai.yaml + d2k:rules/player.yaml + d2k:rules/world.yaml + d2k:rules/palettes.yaml + d2k:rules/defaults.yaml + d2k:rules/vehicles.yaml + d2k:rules/starport.yaml + d2k:rules/husks.yaml + d2k:rules/structures.yaml + d2k:rules/aircraft.yaml + d2k:rules/infantry.yaml + d2k:rules/arrakis.yaml Sequences: - ./mods/d2k/sequences/aircraft.yaml - ./mods/d2k/sequences/vehicles.yaml - ./mods/d2k/sequences/infantry.yaml - ./mods/d2k/sequences/structures.yaml - ./mods/d2k/sequences/misc.yaml + d2k:sequences/aircraft.yaml + d2k:sequences/vehicles.yaml + d2k:sequences/infantry.yaml + d2k:sequences/structures.yaml + d2k:sequences/misc.yaml TileSets: - ./mods/d2k/tilesets/arrakis.yaml + d2k:tilesets/arrakis.yaml MapGrid: TileSize: 32,32 Type: Rectangular Cursors: - ./mods/d2k/cursors.yaml + d2k:cursors.yaml Chrome: - ./mods/d2k/chrome.yaml + d2k:chrome.yaml Assemblies: ./mods/common/OpenRA.Mods.Common.dll ./mods/cnc/OpenRA.Mods.Cnc.dll - ./mods/d2k/OpenRA.Mods.D2k.dll + d2k:OpenRA.Mods.D2k.dll ChromeLayout: - ./mods/d2k/chrome/ingame.yaml + d2k:chrome/ingame.yaml ./mods/ra/chrome/ingame-chat.yaml ./mods/ra/chrome/ingame-diplomacy.yaml ./mods/ra/chrome/ingame-fmvplayer.yaml - ./mods/d2k/chrome/ingame-menu.yaml + d2k:chrome/ingame-menu.yaml ./mods/ra/chrome/ingame-info.yaml ./mods/ra/chrome/ingame-infoscripterror.yaml ./mods/ra/chrome/ingame-infobriefing.yaml ./mods/ra/chrome/ingame-infoobjectives.yaml - ./mods/d2k/chrome/ingame-infostats.yaml - ./mods/d2k/chrome/ingame-observer.yaml + d2k:chrome/ingame-infostats.yaml + d2k:chrome/ingame-observer.yaml ./mods/ra/chrome/ingame-observerstats.yaml - ./mods/d2k/chrome/ingame-player.yaml + d2k:chrome/ingame-player.yaml ./mods/ra/chrome/ingame-perf.yaml ./mods/ra/chrome/ingame-debug.yaml - ./mods/d2k/chrome/mainmenu.yaml + d2k:chrome/mainmenu.yaml ./mods/ra/chrome/settings.yaml ./mods/ra/chrome/credits.yaml ./mods/ra/chrome/lobby.yaml ./mods/ra/chrome/lobby-mappreview.yaml - ./mods/d2k/chrome/lobby-players.yaml - ./mods/d2k/chrome/lobby-options.yaml + d2k:chrome/lobby-players.yaml + d2k:chrome/lobby-options.yaml ./mods/ra/chrome/lobby-music.yaml ./mods/ra/chrome/lobby-kickdialogs.yaml ./mods/ra/chrome/lobby-globalchat.yaml - ./mods/d2k/chrome/color-picker.yaml + d2k:chrome/color-picker.yaml ./mods/ra/chrome/map-chooser.yaml ./mods/ra/chrome/multiplayer.yaml ./mods/ra/chrome/multiplayer-browser.yaml @@ -100,33 +102,33 @@ ChromeLayout: ./mods/ra/chrome/multiplayer-directconnect.yaml ./mods/ra/chrome/multiplayer-globalchat.yaml ./mods/ra/chrome/connection.yaml - ./mods/d2k/chrome/dropdowns.yaml + d2k:chrome/dropdowns.yaml ./mods/ra/chrome/musicplayer.yaml - ./mods/d2k/chrome/tooltips.yaml + d2k:chrome/tooltips.yaml ./mods/ra/chrome/assetbrowser.yaml - ./mods/d2k/chrome/missionbrowser.yaml + d2k:chrome/missionbrowser.yaml ./mods/ra/chrome/confirmation-dialogs.yaml ./mods/ra/chrome/editor.yaml ./mods/ra/chrome/replaybrowser.yaml Weapons: - ./mods/d2k/weapons.yaml - ./mods/d2k/weapons/debris.yaml + d2k:weapons.yaml + d2k:weapons/debris.yaml Voices: - ./mods/d2k/audio/voices.yaml + d2k:audio/voices.yaml Notifications: - ./mods/d2k/audio/notifications.yaml + d2k:audio/notifications.yaml Music: - ./mods/d2k/audio/music.yaml + d2k:audio/music.yaml Translations: - ./mods/d2k/languages/english.yaml + d2k:languages/english.yaml LoadScreen: LogoStripeLoadScreen - Image: ./mods/d2k/uibits/loadscreen.png + Image: d2k:uibits/loadscreen.png Text: Filling Crates..., Breeding Sandworms..., Fuelling carryalls..., Deploying harvesters..., Preparing 'thopters..., Summoning mentats... ContentInstaller: @@ -161,7 +163,7 @@ LobbyDefaults: TechLevel: Unrestricted ChromeMetrics: - ./mods/d2k/metrics.yaml + d2k:metrics.yaml Fonts: Regular: @@ -171,7 +173,7 @@ Fonts: Font:./mods/common/FreeSansBold.ttf Size:14 Title: - Font:./mods/d2k/Dune2k.ttf + Font:d2k:Dune2k.ttf Size:32 MediumBold: Font:./mods/common/FreeSansBold.ttf @@ -190,7 +192,7 @@ Fonts: Size:10 Missions: - ./mods/d2k/missions.yaml + d2k:missions.yaml SupportsMapsFrom: d2k diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 4b188744e5..1d89a43aef 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -3,6 +3,8 @@ Metadata: Description: In a world where Hitler was assassinated and the Third Reich never\nexisted, the Soviet Union seeks power over all of Europe. Allied\nagainst this Evil Empire, the free world faces a Cold War turned hot.\n\nRed Alert fuses the quick and fun gameplay of the original\nC&C: Red Alert, with balance improvements and new gameplay\nfeatures inspired by modern RTS games. Version: {DEV_VERSION} Author: the OpenRA Developers + LogoImagePath: ./mods/ra/logo.png + PreviewImagePath: ./mods/ra/preview.png RequiresMods: modchooser: {DEV_VERSION} diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index 8429f84ef7..0527483c93 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -3,6 +3,8 @@ Metadata: Description: Developer stub, not yet ready for release! Version: {DEV_VERSION} Author: the OpenRA Developers + LogoImagePath: ./mods/ts/logo.png + PreviewImagePath: ./mods/ts/preview.png RequiresMods: modchooser: {DEV_VERSION} @@ -142,7 +144,7 @@ ChromeLayout: ./mods/ra/chrome/ingame-infobriefing.yaml ./mods/ra/chrome/ingame-infoobjectives.yaml ./mods/ra/chrome/ingame-infostats.yaml - ./mods/d2k/chrome/ingame-observer.yaml + d2k:chrome/ingame-observer.yaml ./mods/ts/chrome/ingame-observerstats.yaml ./mods/ts/chrome/ingame-player.yaml ./mods/ra/chrome/ingame-perf.yaml